Android RecyclerView瀑布流中Item宽度异常的问题(源码分析)

问题描述

通过RecyclerView配合StaggeredGridLayoutManager可以很方便的实现瀑布流效果,一般情况下会把作为Item的子View宽度设置为MATCH_PARENT,那么子View将根据列数(假定是垂直排列)平均分配RecyclerView的宽度。但是如果我们为子View的width设置一个确切的值(记为x),并且为RecyclerView添加ItemDecoration(为了设置Item的间距),最终Item的宽度将会被预期的要窄(小于x),本文将从源码的角度分析这种结果的产生的原因。

原因分析

经过分析,发现StaggeredGridLayoutManager会通过measureChildWithDecorationsAndMargin方法测量子View的宽高,该方法的关键代码如下:

private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp,
            boolean alreadyMeasured) {
        if (lp.mFullSpan) { // 如果Item需要占据整行时执行这里的逻辑
            .......
        } else { // 正常情况下的逻辑
            if (mOrientation == VERTICAL) {
                measureChildWithDecorationsAndMargin(child,
                        getChildMeasureSpec(mSizePerSpan, getWidthMode(), 0, lp.width, false),
                        getChildMeasureSpec(getHeight(), getHeightMode(), 0, lp.height, true),
                        alreadyMeasured);// 调用另一个版本的重载方法
            } else {
                .......
            }
        }
    }

这里的getChildMeasureSpec方法是用于确认子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 (canScroll) {
                ......
            } else {// 针对不可滑动的情况,比如现在水平方向就是不可滑动的
                if (childDimension >= 0) { // View的width是一个确切的值时
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } 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);
        }

这里的childDimension就是子View在LayoutParams中的width,其实也就是XML文件中设置的layout_width。此时childDimension>0,根据代码逻辑现在子View的宽度就等于XML文件中设置的layout_width了。随后,将返回的MeasureSpec作为参数,调用了另一个重载版本的measureChildWithDecorationsAndMargin方法,这个方法的关键代码如下:

private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
            int heightSpec, boolean alreadyMeasured) {
        calculateItemDecorationsForChild(child, mTmpRect); // 通过ItemDecoration获取Item的左右间距
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left,
                lp.rightMargin + mTmpRect.right); // 重新计算Item的宽度(减去左右间距)
        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top,
                lp.bottomMargin + mTmpRect.bottom);
        final boolean measure = alreadyMeasured
                ? shouldReMeasureChild(child, widthSpec, heightSpec, lp)
                : shouldMeasureChild(child, widthSpec, heightSpec, lp);
        if (measure) {
            child.measure(widthSpec, heightSpec); // 更新子View的宽度
        }
    }

注意这里的calculateItemDecorationsForChild方法,主要是通过ItemDecoration获取Item的左右间距,并保存在mTmpRect这个对象中。此后,通过updateSpecWithExtra更新Item的宽度(减去左右间距)。最后将最终的宽高设置给子View。

到这里情况已经很清晰了,由于我们在XML文件中为子View设置的宽度在测量中减去了子View左右间距的距离(根据ItemDecoration获得),导致Item的实际宽度小于我们设置的宽度。

解决方案

如果我们确实希望为Item指定一个确切的宽度,并且希望这个宽度不被ItemDecoration影响,只需要在子View的外面套一层ViewGroup就行了。比如在子View外面嵌套一层FrameLayout,并将FrameLayout宽度设置为MATCH_PARENT或者WRAP_CONTENT(最好为MATCH_PARENT),就可以保证Item的宽度被正确测量了。

原理也很简单,由于现在瀑布流的Item实际上是FrameLayout,那么在测量的时候就是去测量FrameLayout的宽度。此时只会对FrameLayout的原始宽度(一列的宽度)减去左右间距,并不影响FrameLayout中子View的宽度,因此Item的最终宽度就不会出现问题了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值