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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章