GridLayoutManager 高度與設定不一致的坑

最近在使用GridLayoutManager的時候,效果什麼的都是好好的,突然在一臺設備上發現item高度和設定值不一樣。找了好久的原因發現是老版本GridLayoutManager的坑。特作此篇做個記錄,並簡單分析GridLayoutManager的源碼。

GridLayoutManager繼承自LinearLayoutManager,LinearLayoutManager初始化的時候默認方向是垂直的。GridLayoutManager測量child的主要重寫layoutChunk方法。因此,我們重點分析一下layoutChunk方法。

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++;
        }

這個方法是來處理GridLayoutManager跨列問題的,GridLayoutManager給我們提供了SpanSizeLookup這個類來實現item佔用spanSize的問題。比如設置mSpanCount等於4,mSpanSizeLookup.getSpanSize(adapterPosition); 來獲取adapterPosition佔用的列數。當佔用的列數累加超過mSpanCount就跳出,並把當行的View存儲到mSet[]中。

// 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);
                }
            }

            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final int spec = View.MeasureSpec.makeMeasureSpec(
                    mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
                            mCachedBorders[lp.mSpanIndex],
                    View.MeasureSpec.EXACTLY);
            if (mOrientation == VERTICAL) {
                measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height), false);
            } else {
                measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec, false);
            }
            final int size = mOrientationHelper.getDecoratedMeasurement(view);
            if (size > maxSize) {
                maxSize = size;
            }
        }

測量child的核心邏輯在這裏,仍以默認的方向爲VERTICAL爲例,每個child測量前的寬度spec是利用mCachedBorders這個數組計算的。

private void calculateItemBorders(int totalSpace) {
        if (mCachedBorders == null || mCachedBorders.length != mSpanCount + 1
                || mCachedBorders[mCachedBorders.length - 1] != totalSpace) {
            mCachedBorders = new int[mSpanCount + 1];
        }
        mCachedBorders[0] = 0;
        int sizePerSpan = totalSpace / mSpanCount;
        int sizePerSpanRemainder = totalSpace % mSpanCount;
        int consumedPixels = 0;
        int additionalSize = 0;
        for (int i = 1; i <= mSpanCount; i++) {
            int itemSize = sizePerSpan;
            additionalSize += sizePerSpanRemainder;
            if (additionalSize > 0 && (mSpanCount - additionalSize) < sizePerSpanRemainder) {
                itemSize += 1;
                additionalSize -= mSpanCount;
            }
            consumedPixels += itemSize;
            mCachedBorders[i] = consumedPixels;
        }
    }

calculateItemBorders就是爲mCachedBorders方法,實際上就是把寬度均分mSpanCount,如果不能整除做下簡單調整。所以回到上一步,得到的寬度上的spec就是均分後的值。我們看看高度方向是怎麼設置spec的。

measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height), false);

高度上的spec是通過getMainDirSpec(lp.height)獲取的,我們看一下這個方法。

private int getMainDirSpec(int dim) {
        if (dim < 0) {
            return MAIN_DIR_SPEC;
        } else {
            return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
        }
    }

可以看到,如果設置的是精確值那就會得到給定height的spec。坑爹的地方就在measureChildWithDecorationsAndMargin方法裏了。

private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
            boolean capBothSpecs) {
        calculateItemDecorationsForChild(child, mDecorInsets);
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        if (capBothSpecs || mOrientation == VERTICAL) {
            widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
                    lp.rightMargin + mDecorInsets.right);
        }
        if (capBothSpecs || mOrientation == HORIZONTAL) {
            heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
                    lp.bottomMargin + mDecorInsets.bottom);
        }
        child.measure(widthSpec, heightSpec);
    }

capBothSpecs傳進來的是false,但是mOrientation == VERTICAL,所以可以看到child在拿到widthSpec的時候,parentsize是均分後減去inset和margin。而高度上的parentsize則直接是佈局設定好的值。(寫死高度情況下)。

坑爹地方來了:

在support-v7-21.0.0中我們看這個源碼:

private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
        calculateItemDecorationsForChild(child, mDecorInsets);
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mDecorInsets.left,
                lp.rightMargin + mDecorInsets.right);
        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
                lp.bottomMargin + mDecorInsets.bottom);
        child.measure(widthSpec, heightSpec);
    }

傳進來的時候不再判斷,child在高度方向的parentsize也會被減去inset和margin,導致child測量後的高比我們設定的值小,雖然後面GridLayoutManager修正了這個bug,但是還是真不容易發現這個坑啊。

如何解決:
老版本在高度設置爲wrapcontent和matchparent時,getMainDirSpec得到的測量模式會轉爲UNSPECIFIED.

private int updateSpecWithExtra(int spec, int startInset, int endInset) {
        if (startInset == 0 && endInset == 0) {
            return spec;
        }
        final int mode = View.MeasureSpec.getMode(spec);
        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
            return View.MeasureSpec.makeMeasureSpec(
                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
        }
        return spec;
    }

updateSpecWithExtra方法中,當mode ==UNSPECIFIED時不會減去inset和margin。所以我們可以採用wrap_content實現這坑爹的邏輯進行兼容。或者採用內容內部加margin或padding的模式來兼容。

發佈了47 篇原創文章 · 獲贊 3 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章