1. 程式人生 > >RecyclerView之ItemDecoration詳解(下)

RecyclerView之ItemDecoration詳解(下)

在上一篇文章中,我和大家一起簡單講解了關於RecyclerView的ItemDecoration抽象類的用法,不過既然文章名叫做《RecyclerView之ItemDecoration詳解》,那麼沒有從原始碼的角度去分析實現原理顯然是稱不上”全”的。因此本篇文章我將帶領大家在上篇文章的程式碼基礎上改進,加入對GridLayoutManager的支援。

由於GridLayoutManager的divider需要橫線和豎線雙向的繪製,所以比單純的LinearLayoutManager複雜多了,一開始我也是拿來主義,首先去網上找了別人寫的demo來進行分析,首先就是著名的【張鴻洋的部落格】中的這篇文章

Android RecyclerView 使用完全解析 體驗藝術般的控制元件,相信很多人都看過,但是在我使用的過程中,我發現很多地方滿足不了我的需求,比如九宮格類似的邊界線的繪製等等,下面我帶大家一起來看下我改進後的:

package com.binbin.divideritemdecoration;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import
android.support.annotation.DrawableRes; import android.support.v4.content.ContextCompat; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import
android.view.View; /** * RecyclerView分割線 * 暫時對StaggeredGridLayoutManager錯序不支援,其他情況均支援 * 自定義LayoutManager暫不做考慮 */ public class DividerItemDecorationTest extends RecyclerView.ItemDecoration { private static final String TAG = "tianbin"; private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; private Drawable mDivider; public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; /** * 設定是否顯示邊界線,即上下左右分割線 * 當為網格佈局時上下左右均有效,當為線性佈局時,只有上下或者左右分別有效 */ private boolean drawBorderLine = false; public DividerItemDecorationTest(Context context) { final TypedArray a = context.obtainStyledAttributes(ATTRS); mDivider = a.getDrawable(0); a.recycle(); } /** * 自定義分割線 * * @param context * @param drawableId 分割線圖片 */ public DividerItemDecorationTest(Context context, @DrawableRes int drawableId) { mDivider = ContextCompat.getDrawable(context, drawableId); } /** * 垂直滾動,item寬度充滿,高度自適應 * 水平滾動,item高度充滿,寬度自適應 * 在itemView繪製完成之前呼叫,也就是說此方法draw出來的效果將會在itemView的下面 * onDrawOver方法draw出來的效果將疊加在itemView的上面 * @param c * @param parent * @param state */ @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { drawHorizontal(c, parent); drawVertical(c, parent); } /** * 滾動方向為垂直(VERTICAL_LIST),畫水平分割線 * @param c * @param parent */ public void drawVertical(Canvas c, RecyclerView parent) { int spanCount = getSpanCount(parent); int allChildCount = parent.getAdapter().getItemCount(); for (int i = 0; i < parent.getChildCount(); i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); int top=0,bottom=0,left=0,right=0; left = child.getLeft() - params.leftMargin; right = child.getRight() + params.rightMargin; if(drawBorderLine){ if(isFirstRaw(parent,params.getViewLayoutPosition(),spanCount)){ top=child.getTop()-params.topMargin-mDivider.getIntrinsicHeight(); bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } }else{ if(isLastRaw(parent,params.getViewLayoutPosition(),spanCount,allChildCount)){ continue; } } top = child.getBottom() + params.bottomMargin; bottom = top + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } /** * 滾動方向為水平,畫垂直分割線 * @param c * @param parent */ public void drawHorizontal(Canvas c, RecyclerView parent) { int spanCount = getSpanCount(parent); int allChildCount = parent.getAdapter().getItemCount(); final int childCount = parent.getChildCount(); for (int i = 0; i < childCount; i++) { final View child = parent.getChildAt(i); final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child .getLayoutParams(); int left=0,right=0,top=0,bottom=0; top=child.getTop()-params.topMargin; bottom=child.getBottom()+params.bottomMargin; if(drawBorderLine){ //加上第一條 if(isFirstColumn(parent,params.getViewLayoutPosition(),spanCount)){ left=child.getLeft()-params.leftMargin-mDivider.getIntrinsicWidth(); right = left + mDivider.getIntrinsicWidth(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } }else{ if(isLastColum(parent,params.getViewLayoutPosition(),spanCount,allChildCount)){ continue; } } left = child.getRight() + params.rightMargin; right = left + mDivider.getIntrinsicHeight(); mDivider.setBounds(left, top, right, bottom); mDivider.draw(c); } } /** * @param outRect * @param view * @param parent * @param state */ @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int spanCount = getSpanCount(parent); int childCount = parent.getAdapter().getItemCount(); int itemPosition=((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition(); boolean isLastRaw=isLastRaw(parent, itemPosition, spanCount, childCount); boolean isLastColum=isLastColum(parent, itemPosition, spanCount, childCount); boolean isFirstRaw=isFirstRaw(parent,itemPosition,spanCount); boolean isFirstColumn=isFirstColumn(parent,itemPosition,spanCount); int left=0,top=0,right=0,bottom=0; //畫線的規則是按照右邊,下邊,所以每個item預設只需設定右邊,下邊,邊框處理按以下規則 right=mDivider.getIntrinsicWidth(); bottom=mDivider.getIntrinsicHeight(); if(drawBorderLine){ if(isFirstRaw){/**第一行:分為第一列和最後一列兩種情況*/ top=mDivider.getIntrinsicHeight(); } if(isFirstColumn){ left=mDivider.getIntrinsicWidth(); } }else{ if(isLastColum){ right=0;//不畫線,最後一列右邊不留偏移 } if(isLastRaw){ bottom=0;//不畫線,最後一行底部不留偏移 } } outRect.set(left,top,right,bottom); } private boolean isFirstRaw(RecyclerView parent, int pos, int spanCount){ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) { int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation(); if (orientation == GridLayoutManager.VERTICAL) { if(pos<spanCount){ return true; } }else{ if(pos%spanCount==0){ return true; } } }else if(layoutManager instanceof LinearLayoutManager){ int orientation = ((LinearLayoutManager) layoutManager).getOrientation(); if (orientation == LinearLayoutManager.VERTICAL) { if(pos==0){ return true; } }else{ //每一個都是第一行,也是最後一行 return true; } } return false; } private boolean isFirstColumn(RecyclerView parent, int pos, int spanCount){ RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) { int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation(); if (orientation == GridLayoutManager.VERTICAL) { if(pos%spanCount==0){ return true; } }else{ if(pos<spanCount){ return true; } } }else if(layoutManager instanceof LinearLayoutManager){ int orientation = ((LinearLayoutManager) layoutManager).getOrientation(); if (orientation == LinearLayoutManager.VERTICAL) { //每一個都是第一列,也是最後一列 return true; }else{ if(pos==0){ return true; } } } return false; } private boolean isLastColum(RecyclerView parent, int pos, int spanCount, int childCount) { RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) { int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation(); if (orientation == GridLayoutManager.VERTICAL) { //最後一列或者不能整除的情況下最後一個 if ((pos + 1) % spanCount == 0 /**|| pos==childCount-1*/){// 如果是最後一列 return true; } }else{ if(pos>=childCount-spanCount && childCount%spanCount==0){ //整除的情況判斷最後一整列 return true; }else if(childCount%spanCount!=0 && pos>=spanCount*(childCount/spanCount)){ //不能整除的情況只判斷最後幾個 return true; } // if(pos>=childCount-spanCount){ // return true; // } } }else if(layoutManager instanceof LinearLayoutManager){ int orientation = ((LinearLayoutManager) layoutManager).getOrientation(); if (orientation == LinearLayoutManager.VERTICAL) { //每一個都是第一列,也是最後一列 return true; }else{ if(pos==childCount-1){ return true; } } } return false; } private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, int childCount) { RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager) { int orientation = (layoutManager instanceof GridLayoutManager)?((GridLayoutManager) layoutManager).getOrientation():((StaggeredGridLayoutManager)layoutManager).getOrientation(); if (orientation == GridLayoutManager.VERTICAL) { if(pos>=childCount-spanCount && childCount%spanCount==0){ //整除的情況判斷最後一整行 return true; }else if(childCount%spanCount!=0 && pos>=spanCount*(childCount/spanCount)){ //不能整除的情況只判斷最後幾個 return true; } // if(pos>=childCount-spanCount){ // return true; // } }else{ //最後一行或者不能整除的情況下最後一個 if ((pos + 1) % spanCount == 0 /**|| pos==childCount-1*/){// 如果是最後一行 return true; } } }else if(layoutManager instanceof LinearLayoutManager){ int orientation = ((LinearLayoutManager) layoutManager).getOrientation(); if (orientation == LinearLayoutManager.VERTICAL) { if(pos==childCount-1){ return true; } }else{ //每一個都是第一行,也是最後一行 return true; } } return false; } private int getSpanCount(RecyclerView parent) { // 列數 int spanCount = -1; RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); if (layoutManager instanceof GridLayoutManager) { spanCount = ((GridLayoutManager) layoutManager).getSpanCount(); } else if (layoutManager instanceof StaggeredGridLayoutManager) { spanCount = ((StaggeredGridLayoutManager) layoutManager).getSpanCount(); } return spanCount; } public boolean isDrawBorderLine() { return drawBorderLine; } public void setDrawBorderLine(boolean drawBorderLine) { this.drawBorderLine = drawBorderLine; } }

整體思路:首先去處理getItemOffsets,按照right,bottom偏移的規則,去留出空隙,然後處理橫向和豎向兩種情況,分別判斷出首行,末行,首列,末列,根據是否繪製邊界線(drawBorderLine)來進行處理偏移,這樣得出的效果如圖:

咦?怎麼會這樣呢。。。這裡就涉及到交叉處的處理,因為沒有加上divider的寬或者高,所以交叉的地方沒有被繪製(RecyclerView預設背景色就是交叉處的那個紅色),其實如果divider很細,是看不出來這個bug的,但是我們要追求完美,所以解決方案如下:只需要在drawVertical或者drawHorizontal的其中一個加入mDivider的寬或高即可(如果兩個裡面都加的話就會出現重疊繪製交叉處的bug)

這裡寫圖片描述

再來執行,效果如下:

怎麼樣?看上去還不錯吧。。。下面是無邊框的情況(紅色處是背景色):

這個時候心裡終於舒坦了,完美解決,但仔細看發現好像有什麼地方不對,每個item的寬度不是均分的,為了驗證,把item的寬度寫死,執行效果果然證實了我的猜想,效果如下:

沒錯,果然沒有充滿,背景色露出來了。。。這是怎麼回事?繼續尋找原因!

猜想是不是繪製的時候計算錯誤呢,於是遮蔽掉那塊程式碼,只留出偏移,發現仍然如此,當我把偏移也遮蔽掉的時候發現item均分了,於是我猜想是在計算偏移的時候出了問題,跟蹤到原始碼裡去檢視:發現呼叫getItemOffsets的地方只有一個getItemDecorInsetsForChild(View child):

Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

發現我們所計算的偏移被加入到了insets中,而呼叫這個函式的地方我們發現有三個,分別是:

  • measureChild(View child, int widthUsed, int heightUsed)
  • measureChildWithMargins(View child, int widthUsed, int heightUsed)
  • calculateItemDecorationsForChild(View child, Rect outRect)

我們發現,第一個暫時沒有被呼叫的地方,第二個是被LinearLayoutManager呼叫的,暫且不管,最後一個是被GridLayoutManager中的layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result)方法呼叫的,而這個方法是用來layout每個item的,下面來分析這個方法:

@Override
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        final int otherDirSpecMode = mOrientationHelper.getModeInOther();
        final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
        final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
        // if grid layout's dimensions are not specified, let the new row change the measurements
        // This is not perfect since we not covering all rows but still solves an important case
        // where they may have a header row which should be laid out according to children.
        if (flexibleInOtherDir) {
            updateMeasurements(); //  reset measurements
        }
        final boolean layingOutInPrimaryDirection =
                layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
        int count = 0;
        int consumedSpanCount = 0;
        int remainingSpan = mSpanCount;
        if (!layingOutInPrimaryDirection) {
            int itemSpanIndex = getSpanIndex(recycler, state, layoutState.mCurrentPosition);
            int itemSpanSize = getSpanSize(recycler, state, layoutState.mCurrentPosition);
            remainingSpan = itemSpanIndex + itemSpanSize;
        }
        while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
            int pos = layoutState.mCurrentPosition;
            final int spanSize = getSpanSize(recycler, state, pos);
            if (spanSize > mSpanCount) {
                throw new IllegalArgumentException("Item at position " + pos + " requires " +
                        spanSize + " spans but GridLayoutManager has only " + mSpanCount
                        + " spans.");
            }
            remainingSpan -= spanSize;
            if (remainingSpan < 0) {
                break; // item did not fit into this row or column
            }
            View view = layoutState.next(recycler);
            if (view == null) {
                break;
            }
            consumedSpanCount += spanSize;
            mSet[count] = view;
            count++;
        }

        if (count == 0) {
            result.mFinished = true;
            return;
        }

        int maxSize = 0;
        float maxSizeInOther = 0; // use a float to get size per span

        // we should assign spans before item decor offsets are calculated
        assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
        for (int i = 0; i < count; i++) {
            View view = mSet[i];
            if (layoutState.mScrapList == null) {
                if (layingOutInPrimaryDirection) {
                    addView(view);
                } else {
                    addView(view, 0);
                }
            } else {
                if (layingOutInPrimaryDirection) {
                    addDisappearingView(view);
                } else {
                    addDisappearingView(view, 0);
                }
            }
            calculateItemDecorationsForChild(view, mDecorInsets);

            measureChild(view, otherDirSpecMode, false);
            final int size = mOrientationHelper.getDecoratedMeasurement(view);
            if (size > maxSize) {
                maxSize = size;
            }
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) /
                    lp.mSpanSize;
            if (otherSize > maxSizeInOther) {
                maxSizeInOther = otherSize;
            }
        }
        if (flexibleInOtherDir) {
            // re-distribute columns
            guessMeasurement(maxSizeInOther, currentOtherDirSize);
            // now we should re-measure any item that was match parent.
            maxSize = 0;
            for (int i = 0; i < count; i++) {
                View view = mSet[i];
                measureChild(view, View.MeasureSpec.EXACTLY, true);
                final int size = mOrientationHelper.getDecoratedMeasurement(view);
                if (size > maxSize) {
                    maxSize = size;
                }
            }
        }

        // Views that did not measure the maxSize has to be re-measured
        // We will stop doing this once we introduce Gravity in the GLM layout params
        for (int i = 0; i < count; i ++) {
            final View view = mSet[i];
            if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
                final Rect decorInsets = lp.mDecorInsets;
                final int verticalInsets = decorInsets.top + decorInsets.bottom
                        + lp.topMargin + lp.bottomMargin;
                final int horizontalInsets = decorInsets.left + decorInsets.right
                        + lp.leftMargin + lp.rightMargin;
                final int totalSpaceInOther = getSpaceForSpanRange(lp.mSpanIndex, lp.mSpanSize);
                final int wSpec;
                final int hSpec;
                if (mOrientation == VERTICAL) {
                    wSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
                            horizontalInsets, lp.width, false);
                    hSpec = View.MeasureSpec.makeMeasureSpec(maxSize - verticalInsets,
                            View.MeasureSpec.EXACTLY);
                } else {
                    wSpec = View.MeasureSpec.makeMeasureSpec(maxSize - horizontalInsets,
                            View.MeasureSpec.EXACTLY);
                    hSpec = getChildMeasureSpec(totalSpaceInOther, View.MeasureSpec.EXACTLY,
                            verticalInsets, lp.height, false);
                }
                measureChildWithDecorationsAndMargin(view, wSpec, hSpec, true);
            }
        }

        result.mConsumed = maxSize;

        int left = 0, right = 0, top = 0, bottom = 0;
        if (mOrientation == VERTICAL) {
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                bottom = layoutState.mOffset;
                top = bottom - maxSize;
            } else {
                top = layoutState.mOffset;
                bottom = top + maxSize;
            }
        } else {
            if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
                right = layoutState.mOffset;
                left = right - maxSize;
            } else {
                left = layoutState.mOffset;
                right = left + maxSize;
            }
        }
        for (int i = 0; i < count; i++) {
            View view = mSet[i];
            LayoutParams params = (LayoutParams) view.getLayoutParams();
            if (mOrientation == VERTICAL) {
                if (isLayoutRTL()) {
                    right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
                    left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
                } else {
                    left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
                    right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
                }
            } else {
                top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
                bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
            // We calculate everything with View's bounding box (which includes decor and margins)
            // To calculate correct layout position, we subtract margins.
            layoutDecoratedWithMargins(view, left, top, right, bottom);
            if (DEBUG) {
                Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                        + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                        + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
                        + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
            }
            // Consume the available space if the view is not removed OR changed
            if (params.isItemRemoved() || params.isItemChanged()) {
                result.mIgnoreConsumed = true;
            }
            result.mFocusable |= view.isFocusable();
        }
        Arrays.fill(mSet, null);
    }

這個方法雖然很長,但主要的功能就是用來measure和layout的,跟我們常見的自定義view流程是一樣的。我們看主要的部分,首先是第69行,找到這個方法:

/**
         * Calculates the item decor insets applied to the given child and updates the provided
         * Rect instance with the inset values.
         * <ul>
         *     <li>The Rect's left is set to the total width of left decorations.</li>
         *     <li>The Rect's top is set to the total height of top decorations.</li>
         *     <li>The Rect's right is set to the total width of right decorations.</li>
         *     <li>The Rect's bottom is set to total height of bottom decorations.</li>
         * </ul>
         * <p>
         * Note that item decorations are automatically calculated when one of the LayoutManager's
         * measure child methods is called. If you need to measure the child with custom specs via
         * {@link View#measure(int, int)}, you can use this method to get decorations.
         *
         * @param child The child view whose decorations should be calculated
         * @param outRect The Rect to hold result values
         */
        public void calculateItemDecorationsForChild(View child, Rect outRect) {
            if (mRecyclerView == null) {
                outRect.set(0, 0, 0, 0);
                return;
            }
            Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
            outRect.set(insets);
        }

這是RecyclerView中的一個方法,裡面呼叫了上面我們提到的getItemDecorInsetsForChild,這裡就是用來計算我們設定的偏移,並傳入到GridLayoutManager中的layoutChunk方法中,去用來測量每個item加上偏移值和padding margin之後的大小,網上有人說這個值算入了padding中,但我感覺並沒有,而且官方也解釋了這個值的含義:
Retrieve any offsets for the given item. Each field of outRect specifies the number of pixels that the item view should be inset by, similar to padding or margin.The default implementation sets the bounds of outRect to 0 and returns
只是一個與padding和margin相似的東西,並不能算入任何一個當中。

在完成測量之後,看第164行,進而又對item進行了layout,跟蹤進去:

/**
         * Lay out the given child view within the RecyclerView using coordinates that
         * include any current {@link ItemDecoration ItemDecorations} and margins.
         *
         * <p>LayoutManagers should prefer working in sizes and coordinates that include
         * item decoration insets whenever possible. This allows the LayoutManager to effectively
         * ignore decoration insets within measurement and layout code. See the following
         * methods:</p>
         * <ul>
         *     <li>{@link #layoutDecorated(View, int, int, int, int)}</li>
         *     <li>{@link #measureChild(View, int, int)}</li>
         *     <li>{@link #measureChildWithMargins(View, int, int)}</li>
         *     <li>{@link #getDecoratedLeft(View)}</li>
         *     <li>{@link #getDecoratedTop(View)}</li>
         *     <li>{@link #getDecoratedRight(View)}</li>
         *     <li>{@link #getDecoratedBottom(View)}</li>
         *     <li>{@link #getDecoratedMeasuredWidth(View)}</li>
         *     <li>{@link #getDecoratedMeasuredHeight(View)}</li>
         * </ul>
         *
         * @param child Child to lay out
         * @param left Left edge, with item decoration insets and left margin included
         * @param top Top edge, with item decoration insets and top margin included
         * @param right Right edge, with item decoration insets and right margin included
         * @param bottom Bottom edge, with item decoration insets and bottom margin included
         *
         * @see View#layout(int, int, int, int)
         * @see #layoutDecorated(View, int, int, int, int)
         */
        public void layoutDecoratedWithMargins(View child, int left, int top, int right,
                int bottom) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Rect insets = lp.mDecorInsets;
            child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                    right - insets.right - lp.rightMargin,
                    bottom - insets.bottom - lp.bottomMargin);
        }

我們可以看到在測量和佈局的過程中,均對我們設定的偏移值進行了加入並處理,並且每個item所佔大小是包含了padding、margin、insets三個值的,由於我們設定偏移的時候並沒有去進行均分處理,所以造成了上面出現的bug。找到原因了,就好解決了!下面給出解決方案:
在getItemOffsets中(保證每個Item的insets.left+insets.right相等,這樣才能達到均分的目的)

以我們的demo為例(手機為1080*1920):有邊界總偏移值為5dp*4=20dp=60px;每個item的左右偏移總和為60/3=20px

Column Left Right
0 15 5
1 10 10
2 5 15
GridLayoutManager.SpanSizeLookup spanSizeLookup ((GridLayoutManager)layoutManager).getSpanSizeLookup();
//左邊的跨度索引值[0,spanCount)之間
int spanIndexLeft = spanSizeLookup.getSpanIndex(itemPosition, spanCount);
//右邊的跨度索引值[0,spanCount)之間
int spanIndexRight = spanIndexLeft - 1 + spanSizeLookup.getSpanSize(itemPosition);
if(drawBorderLine){
    left=dividerWidth * (spanCount - spanIndexLeft) / spanCount;
    right=dividerWidth * (spanIndexRight + 1) / spanCount;
}else{
    left = dividerWidth * spanIndexLeft / spanCount;
    right = dividerWidth * (spanCount - spanIndexRight - 1) / spanCount;
}

我們可以根據SpanSizeLookup去計算偏移值,這樣也支援了不同spanSize的item,而判斷首末行列的方法也要根據這個來進行變化。修改後的效果如下:

另外我加入了不同構造方法,支援傳入drawable和顏色值,直接設定divider樣式,並且加入了drawBorderLeftAndRight和drawBorderTopAndBottom兩個變數分別去控制左右或者上下邊界線的繪製。同時把LinearLayoutManager和GridLayoutManager加入支援,還有onlySetItemOffsetsButNoDraw變數去控制只留偏移不畫線,橫向和縱向佈局都支援自動識別,這樣就打造了一個萬能的ItemDecoration

注:使用過程中避免使用小數dp值,因為getItemOffsets中在做均分的時候小數會出現不能整除的情況,而Rect不支援小數,所以這裡算是追求完美的過程中留下的一個遺憾吧,也可能我沒有想到好的方法,希望有解決方案的朋友可以留言。。。

好了,今天的講解到此結束,有疑問的朋友請在下面留言。