RecyclerView之ItemDecoration详解(下)

RecyclerView中ItemDecoration的基础,粘性头部及点击事件 在工作中遇到ItemDecoration相关需求,发现自己以前学习的太过粗糙,特此重新学习记录相关知识。本文介绍了RecyclerView中ItemDecoration的相关方法,从基础的onDraw()和getItemOffsets()到进阶的onDrawOver(),最后还实现了ItemDecoration的点击事件的监听。 阅读详情

转载请注明出处:http://blog.youkuaiyun.com/binbinqq86/article/details/53102441

在上一篇文章中,我和大家一起简单讲解了关于RecyclerView的ItemDecoration抽象类的用法,不过既然文章名叫做《RecyclerView之ItemDecoration详解》,那么没有从源码的角度去分析实现原理显然是称不上”全”的。因此本篇文章我将带领大家在上篇文章的代码基础上改进,加入对GridLayoutManager的支持。

如果还没有看过我上一篇文章,请抓紧去阅读一下 RecyclerView之ItemDecoration详解(上)

由于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

ColumnLeftRight
0155
11010
2515

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不支持小数,所以这里算是追求完美的过程中留下的一个遗憾吧,也可能我没有想到好的方法,希望有解决方案的朋友可以留言。。。

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里

RecyclerView 源码分析(九)ItemDecoration深入解析 转载Carson_Ho RecyclerView 源码分析(九)深入解析 RecyclerView.ItemDecoration类前言目录1. ItemDecoration类 简介1.1 定义1.2 作用2. 具体使用2.1 getItemOffsets()2.1.1 作用2.1.2 具体使用2.1.3 源码分析总结2.2 onDraw()2.2.1 作用2.2.2 具体使用2.2.3 特别注意2.2.4 应用场景2.2.5 实例讲解2.2.6 结果展示2.2.7 源码地址2.3 onDrawOver()2 阅读详情

相关推荐

iSLIP算法实战:如何用Python模拟交换机优先级匹配(附完整代码)

本文详细介绍了如何使用Python实现iSLIP算法,模拟交换机优先级匹配过程。通过核心的指针滑动逻辑和迭代循环策略,构建了一个完整的iSLIP算法模拟器,并加入可视化模块以直观展示匹配过程。文章还提供了完整的代码框架和性能优化建议,适合网络工程师和Python开发者直接集成到测试环境中。

weixin_29313239的博客 251

ItemDecoration用法

1、ItemDecoration概念 在使用RecyclerView显示列表的时候需要分隔线隔开item,此时则可以使用Recycler的内部类ItemDecoration。ItemDecoration是android系统提供的基类,用于绘制RecyclerView的分隔线,我们可以通过继承ItemDecoration实现丰富的分隔线效果。(系统也提供了实现好的DividerItemDecoration使用。) 2、ItemDecoration的使用 使用ItemDecoration时,需继承此类并覆写其中

wh义华的专栏 4522

原神真端,原神服务端,虚拟机启动部署傻瓜式教学

没玩过抽卡 游戏想体验一下抽卡的感觉,听说原神有真端了,所以弄一个局域网的服务体验一下,这里要感谢一些服务端的分享者。

azyouyouyou的博客 1万+

RecyclerView 之 ItemDecoration 讲解及高级特性实践

RecyclerView 之 ItemDecoration

QG 2120

GEE入门之路(从【初学者】角度,大白话)——附海面风速插值代码

非常初级的GEE入门教程!大白话!有手就行!附海面风速插值代码,克里金插值,时间插值。

weixin_46812066的博客 4086

芯片低功耗设计的两种常用EDA流程

如题。

EricWZY2008的专栏 5558

小甜点,RecyclerView 之 ItemDecoration 讲解及高级特性实践

小甜点,RecyclerView 之 ItemDecoration 讲解及高级特性实践毫无疑问,RecyclerView 是现在 Android 世界中最重要的系统组件之一,它的出现就是为了高效代替 ListView 和 GridView。当时它的出现解决了我一个大的需求,这个需求就是在电视盒子界面上横向加载应用列表,由于 ListView 没有横向加载的功能,而网络上开源的那些 Horizonta

frank 的专栏 5万+

在MCUXpresso上通过J-link烧写KW45

背景: KW45B是nxp 的ble npi产品,其使用的IDE为MCUXpressso或者IAR,MCUXpressso有一些bug,但是由于MCUXpressso阅读查找代码相对IAR容易,所以我经常使用的MCUXpressso进行开发。当然IAR也有其优势,比如编译、烧写速度相对MCUXpressso更快,是比较通用的一个平台,而MCUXpressso是NXP独有的。 Kw45有两种烧写方式,一是使用MCUlink,二是使用Jlink。NXP ble 产品线提供的IDE资料里有最新的15.0版本的

weixin_41889258的博客 2859

RecyclerView之ItemDecoration详解

转载地址:http://blog.youkuaiyun.com/shangmingchao【商明超的博客】 关于RecyclerView的ItemView装饰,之前一直用官方Demo的DividerItemDecoration,并没有认真地去理解ItemDecoration的用法,也没能体会到ItemDecoration的强大,直到要用到横向的RecyclerView,而且最左边的和最右边的Item

qq_15796477的博客 423

Simulink仿真模块 - Signal Conversion

目录 说明 实例 创建总线信号的连续副本 ​将虚拟总线转换为非虚拟总线​ 将非虚拟总线转换为虚拟总线 将Muxed信号转换为矢量 模块特性 Signal Conversion:将信号转换为新类型,而不改变信号值 在仿真库中的位置为: Simulink / Signal Attributes HDL Coder / Signal Attributes 模型为: 说明 Signal Conversion模块可将信号从一种类型转换为另一种类型。要选择要...

jk_101的博客 3421

华为OD机试2025C卷 - 字符串解密 (C++ & Python & JAVA & JS & GO)

华为OD机试 2025C卷 字符串解密,提供Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解

qq_45776114的博客 977

艾尔登法环存档完整指南:改个 SteamID 15 分钟把百时角色平移新电脑

你刚重装完系统,打开艾尔登法环,角色列表空空如也。几百小时的进度全在旧机器那个 .sl2 文件里。这篇用 ER-Save-Editor 把存档里登记的账号 ID 改写成新账号的 SteamID,让游戏认账放行。15 分钟跑完,全程不用手写一行十六进制。 ## 能力边界:只改 ID,不碰线上 它能改写 PC 版 .sl2 存档的 SteamID,写新文件时顺手重算对应校验和;也能改角色名、属性、

gitblog_00879的博客 334

如何通过插值法计算cell delay?

插值法的公式:Z = A + B * X + C * Y + D * X * Y,其中X为input transition,Y为output load,A、B、C、D为四个参数,Z为delay值。从而得到Z=-0.086176 + 11.5493 * x + 0.65917 * y - 24.859 * x * y,当X=0.014930、Y=0.296311时,得到Z=0.1716ns。从而得到A = 0.08075,B = 3.47539,C = 0.04008,D = 0.01648。

芯冰乐 1076

RecyclerView之ItemDecoration详解(上)

转载请注明出处:http://blog.youkuaiyun.com/binbinqq86/article/details/54090829RecyclerView的出现让许多人眼前一亮,当然在整个开发者圈子里面也拥有很不错的口碑,相比ListView而言,它高度灵活的使用方式也让很多新手感到不知所措,虽然做Android开发也有些时间了,但也是最近才转到RecyclerView上,不用不知道,一用就发现它真的

Water的博客 3732

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

recyclerView-自定义itemDecoration详解

public class SectionDecoration extends RecyclerView.ItemDecoration { @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { super.onDraw(c, parent, st

musk6的博客 1853

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

RecyclerView.ItemDecoration的详解使用(RecyclerView系列二)

前言 在很早很早以前(long long ago),ListView鼎盛的时代有一个属性叫做divider。但是在RecycleView上面就是找不到他,那怎么办呢???直到后来有一天发现他变身了,变成了ItemDecoration。实在是扯不下去了,直接开始吧! 这篇博客酝酿了好长时间,希望不会让各位看官失望。 任务 了解ItemDecoration的原理,自己可以添加分割线,每个 ItemVi...

weixin_33774883的博客 431

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

android_基础_recyclerview之ItemDecoration详解

转载自:https://www.jishudog.com/9055/html 一、简述 说到RecyclerView大家都很熟悉了,相比于ListView,它具有高度解耦、性能优化等优势,而且现在大多数安卓开发者都已经将RecyclerView用来完全替代ListView和GridView,因为它功能十分强大,但往往功能强大的东西,反而不太好控制,例如今天要说的这个ItemDecoration,ItemDecoration是条目装饰,下面来看看它的强大吧。 二、使用ItemDecoration绘制分割线.

写bug的蚊子的博客 2326

RecyclerView (二) ItemDecoration 详解

RecyclerView 已经推出了n年了,日常开发中也已经彻底从 ListView 迁移到了 RecyclerView,但前两天有人在一个安卓群里面问了个关于最顶上的 item view 加蒙层的问题,被人用 ItemDecoration 完美解决。此时我发现自己对 RecyclerView 的使用一直太过基本,更深入更强大的功能完全没有涉及,像 ItemDecoration, ItemAnim...

深耕技术 1234

从头开始学 RecyclerView(五) ItemDecoration 详解

前言RecyclerView.ItemDecoration,通过名字来看,它就是用来装饰Item的。 在类ListView的视图中,可能需要绘制分隔线;在类GridView的网格视图中,可能需要绘制单元格样式… 这些都可以由重写RecyclerView.ItemDecoration来进行定制。 然后调用mRecyclerView.addItemDecoration(itemDecoration)

打渔还是晒网 —— stone 3790

RecyclerView详解——(二)优劣,ItemDecoration,SnapHelper

本文主要讲述RecyclerView和ListView的区别,ItemDecoration实现分割线,边距和背景,以及SnapHelper的使用。

Android 领域开发程序员,希望通过写博客的方式不断的总结提升自己,大家共同进步哦! 1235

RecyclerView详解(二):ItemDecoration使用(kotlin)

我们知道listview可以通过在布局文件中设置 android:divider=”#ffff0000” android:dividerHeight=”10dp” 来控制item之间的间隔和颜色,但是recyclerview是不行的。 recyclerview提供了一个ItemDecoration类供开发者扩展使用,再通过RecyclerView.addItemDecoration(ItemDecoration)的方法,即可以完成对item间间隔的定义。其实ItemDecoration相当于可以自定义

stonezry的专栏 1580

android RecyclerView (二) ItemDecoration 详解

RecyclerView 已经推出了一年多了,日常开发中也已经彻底从 ListView 迁移到了 RecyclerView,但前两天有人在一个安卓群里面问了个关于最顶上的 item view 加蒙层的问题,被人用 ItemDecoration 完美解决。此时我发现自己对 RecyclerView 的使用一直太过基本,更深入更强大的功能完全没有涉及,像 I...

weixin_34279061的博客 270

RecyclerView源码详解(第二篇ItemDecoration源码详解)

看源码之前,先看一下ItemDecoration能给我们实现的效果图: 看静态图标记的这条分割线,每个子itemView如果想空开距离或者想产生明显的边界的话,就应该写ItemDecoration类的实现类,也就是说RecyclerView没有像ListView一样为我们实现了分割线功能,先看一下实现的代码 mRecyclerView.addItemDecoratio

xiatiandefeiyu的博客 1255
上一篇: RecyclerView之ItemDecoration详解(上)
下一篇: RecyclerView轻松实现悬浮头部分组列表
binbinqq86
博客等级 码龄17年 89粉丝 42原创
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值