1. 程式人生 > >Android RecyclerView瀑布流中Item寬度異常的問題(原始碼分析)

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的最終寬度就不會出現問題了。