RecyclerView添加自定义ItemDecoration实现(2)

1.看过第一篇文章的都会注意到在设置RecyclerView的GridLayoutManager的Item时设置的是正方形的布局。可是当设置自定义分割线的时候出现了一些问题。如果对GridLayoutManager添加分割线有疑惑的可以查看上一篇文章。

RecyclerView添加自定义ItemDecoration实现(1)

具体情况如下图所示:

GridLayotManager_bug_5dp.png

问题:从图中可能不能很好的看出存在的问题。我们不妨加大分割线的宽度之后再观察。所以我们将之前分割线的宽度改为20dp。效果如下:

drawable_item_decoration_20dp.png
GridLayoutManager_bug_20dp.png

现在可以很明显的看出之前遗留下来的bug:第一列和第二列的形状和第三列的不同。那么为什么会出现这种情况呢?我们下面从源码分析一下添加ItemDecoration的原理。

2.在RecyclerView中有如下一部分代码

/**
 * Measure a child view using standard measurement policy, taking the padding
 * of the parent RecyclerView and any added item decorations into account.
 *
 * <p>If the RecyclerView can be scrolled in either dimension the caller may
 * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
 *
 * @param child Child view to measure
 * @param widthUsed Width in pixels currently consumed by other views, if relevant
 * @param heightUsed Height in pixels currently consumed by other views, if relevant
 */
public void measureChild(View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();

    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
            getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
            getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}



/**
 * Calculate a MeasureSpec value for measuring a child view in one dimension.
 *
 * @param parentSize Size of the parent view where the child will be placed
 * @param parentMode The measurement spec mode of the parent
 * @param padding Total space currently consumed by other elements of parent
 * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
 *                       Generally obtained from the child view's LayoutParams
 * @param canScroll true if the parent RecyclerView can scroll in this dimension
 *
 * @return a MeasureSpec value for the child view
 */
public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
        int childDimension, boolean canScroll) {
    int size = Math.max(0, parentSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    if (childDimension >= 0) {
        resultSize = childDimension;
        resultMode = MeasureSpec.EXACTLY;
    } else if (canScroll) {
         if (childDimension == LayoutParams.MATCH_PARENT){
            switch (parentMode) {
                case MeasureSpec.AT_MOST:
                case MeasureSpec.EXACTLY:
                    resultSize = size;
                    resultMode = parentMode;
                    break;
                case MeasureSpec.UNSPECIFIED:
                    resultSize = 0;
                    resultMode = MeasureSpec.UNSPECIFIED;
                    break;
            }
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = 0;
            resultMode = MeasureSpec.UNSPECIFIED;
        }
    } else {
        if (childDimension == LayoutParams.MATCH_PARENT) {
            resultSize = size;
            resultMode = parentMode;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            resultSize = size;
            if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
                resultMode = MeasureSpec.AT_MOST;
            } else {
                resultMode = MeasureSpec.UNSPECIFIED;
            }

        }
    }
    //noinspection WrongConstant
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

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;
}
  • 首先我们先看 getChildMeasureSpec 方法。 当 childDimension >= 0 时让结果为 childDimension,否则为 Math.max(0, parentSize - padding)。这个的意思就是如果子view的尺寸如果是精确值的话,那么就是精确值的结果,如果是 MATCH_PARENT 或者 WRAP_CONTENT 的话就开始计算结果。
  • 其次我们从 measureChild 方法中可以看出,widthSpec 的 getChildMeasureSpec 中可以得到

     paddingg =  getPaddingLeft() + getPaddingRight() + widthUsed;
    

     final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
     widthUsed += insets.left + insets.right;
    
  • 最后我们可以从 getItemDecorInsetsForChild 方法中可以看出 Rect insets就是在自定义 ItemDecoration 时
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {} 中的outRect。所以计算 widthUsed 时候,与 outRect 有关,而我们在 getItemOffsets 方法中有这么一段代码

     if (!isLastColumn(currentItemPosition, mSpanCount)) {
          outRect.right = mDrawable.getIntrinsicWidth();
     } else {
         outRect.right = 0;
     }
    

    如果是最后一列的话,outRect.right = 0;这使得第三列子view的 widthUsed 与前两列的不同,所以第三列子view的 padding 就会比前两列的少一个分割线的宽度。

3.解决

那我们应该怎么解决这个问题呢?问题的关键就是分割线的宽度分配不均导致的,我们只要均匀分配宽度即可。

statement.png

如图所示,我们对于有三列的情况来说,averWidth = (2 * dividerWidth)/3, 将两个分割线的宽度,均分给3个子 view 的 outRect。所以为了适配其他情况得到如下式子 :

 averWidth = [(spanCount - 1 ) * dividerWidth ]/spanCount ;

现在是最重要的步骤,如果将averWidth分给每个子 view,就会存在如下情况:

leftN + rightN = averWidth;
left(N+1) + rigthN = dividerWidth;
(注:leftN为第N个view的左偏移值,rightN为第N个View的右偏移值,计数从0开始)

对于 rightN 存在如下的情况:

 right0 = averWidth - left0;           
 right1 = averWidth - left1;           
 right2 = averWidth - left2;           
 .
 .
 .
 rightN = averWidth - leftN;

对于 leftN 存在如下的情况:

left0 = 0;                
left1 =  dividerWidth - right0;
left2 =  dividerWidth - right1;
.
.  
.
leftN = dividerWidth - right(N-1);

从上面的式子可以看出,我们只要得到 leftN 的就能很快的得到 rightN,所以将 rightN 式子合并到 leftN 中得到如下的结果:

  left0 = 0;

  left1 = dividerWidth - (averWidth - left0) 
        = dividerWidth - averWidth + left0
        = dividerWidth - averWidth 
  
  left2 = dividerWidth - (averWidth  - left1) 
        = dividerWidth -[averWidth  - (dividerWidth - averWidth)] 
        = 2 * (dividerWidth - averWidth)    
  .
  .
  .
  leftN = N * (dividerWidth - averWidth)

所以,根据化简后的结果更改代码,修改完后的 getItemOffsets 方法具体如下:

/**
 * @param outRect 用于规定分割线的范围
 * @param view    进行分割线操作的子view
 * @param parent  父view
 * @param state   (这里暂时不使用)
 */
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    //notification的时候要获取
    mTotalItems = parent.getAdapter().getItemCount();
    if (0 == mSpanCount) {
        RecyclerView.LayoutManager layoutManager = parent.getLayoutManager();
        //判断是否为GridLayoutManager
        if (layoutManager instanceof GridLayoutManager) {
            GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            mSpanCount = gridLayoutManager.getSpanCount();
        } else {
            mSpanCount = 1;
        }
    }
    //在源码中有一个过时的方法,里面有获取当前ItemPosition
    int currentItemPosition = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
    //
    if (!isLastRow(currentItemPosition, mTotalItems, mSpanCount))
        outRect.bottom = mDrawable.getIntrinsicWidth();
    else
        outRect.bottom = 0;
    //将isLastColumn注释,添加下面代码
    //        if (!isLastColumn(currentItemPosition, mSpanCount)) {
    //            outRect.right = mDrawable.getIntrinsicWidth();
    //        } else {
    //            outRect.right = 0;
    //        }
    int averWidth = (mSpanCount - 1) * mDrawable.getIntrinsicWidth() / mSpanCount;
    int dX = mDrawable.getIntrinsicWidth() - averWidth;
    outRect.left = (currentItemPosition % mSpanCount) * dX;
    outRect.right = averWidth - outRect.left;
}

结果截图:

final.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容