RecyclerView:LayoutManager

[toc]android

LayoutManager

原由是這樣一張UI美眉給的設計圖:git

起初是有作自定義 View 的想法,後來發現這個想法多多少少有點不成熟,由於沒有時間目前也沒有能力去完成這樣一個控件。github

好在後來找到了 SpannedGridLayoutManager 由做者 Arasthel 完成的一個跨行跨列的 GridLayoutManager 佈局。做者思路很值得借鑑。數組

在這段時間的開發完成以後,對 LayoutManager 產生了一些好奇。它是如何產生這些效果的呢?若是要實現一個相似 LayoutManager 須要完成什麼步驟呢?緩存

GridLayoutManager

由於是從此次的佈局開發產生的興趣,那麼首先下手的地方就是 GridLayoutManager 了。查看 GridLayoutManager 源碼能夠發現你,它存在三個內部類:markdown

  • LayoutParams
  • SpanSizeLookup
  • DefaultSpanSizeLookup
LayoutParams

LayoutParams 都很熟悉了,它提供給咱們佈局屬性,最基本的 widthheight,更詳細一點的 margin 屬性,等等。不少容器組件都有其 LayoutParams 的子類。那 GridLayoutManagerLayoutParams 中增長了兩個新的變量:mSpanIndexmSpanSize。他們是作什麼的呢?來繼續看。app

SpanSizeLookup

SpanSizeLookup 是一個幫助類,用於提供每項 itemview 所佔用跨度數,默認值爲 1。 它是一個抽象類,來看下其內部都定義了那些方法:ide

public abstract static class SpanSizeLookup {
    /**
     * @param position item 在 adapter 中的位置
     * @return position 指代的 item 所佔用的列數
     */
    public abstract int getSpanSize(int position);

    /**
     * itemview 佔據跨度的下標
     */
    public int getSpanIndex(int position, int spanCount){}
}
複製代碼

getSpanIndex()getSpanSize() 是比較重要的兩個方法,除了這兩個方法外還有一些緩存的方法,用於緩存 position 對應 SpanSize 的計算結果。oop

getSpanIndex()getSpanSize() 的含義和做用結合 DefaultSpanSizeLookup 更好理解。佈局

DefaultSpanSizeLookup

DefaultSpanSizeLookupSpanSizeLookup 的默認實現:

public static final class DefaultSpanSizeLookup extends SpanSizeLookup {
    @Override
    public int getSpanSize(int position) {
        return 1;
    }
    
    @Override
    public int getSpanIndex(int position, int spanCount) {
        return position % spanCount;
    }
}
複製代碼

能夠看到 getSpanSize() 返回值默認爲 1getSpanIndex() 返回值爲 當前位置 % 總列數。若是把 GridLayoutMananger 看作一個個小格子,每一個格子下標從 0 至 SpanCount,一個 SpanCount = 4 的佈局中每一個小格子的下標以下:

0,1,2,3
0,1,2,3
0,1,2,3
...
複製代碼

那麼 positionn = 0itemviewspanIndex = 0, positionn = 2itemviewspanIndex = 2positionn = 4itemviewspanIndex = 0

總結一下,在每一行中,SpanIndex 表明 itemview 佔據跨度的起始下標,spanSize 表明 itemview 佔據了多少跨度。若是設置 spanSize = spanCount

val gridLayoutManager = GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return 3
    }
}
複製代碼

你會獲得一個使用 GridLayoutManager 繪製的 LinearLayoutManager 佈局。

佈局實現

瞭解了 SpanSizeSpanIndex 的含義咱們能夠理所應當的猜想 GridLayoutManager 就是經過它們來肯定每一個 itemview 在容器中的位置的。

如何驗證猜想的真實性呢?固然是去三兄弟 onMeasure()onLayout()onDraw() 中找了。由於是容器組件因此對於 子View 的測量和佈局確定是在 onLayoutChildren() 裏了。找到 GridLayoutManageronLayoutChildren() 方法:

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    super.onLayoutChildren(recycler, state);
    ...
}
複製代碼

其內依然使用的是其父類 onLayoutChildren 的實現,它的父類是誰呢: LinearLayoutManager

這裏插一句,不少前輩都說過,最好帶着問題去看源碼,個人理解是若是隻是想弄明白某個問題,那麼最好的作法是快速定位到具體的代碼實現中,而不是去關注太多目前不須要去理解的代碼,它會分散你的經歷而且影響你的思路。

能夠看到 LinearLayoutManager - onLayoutChildren() 中的代碼茫茫多。想要找到 子View 是如何測量和佈局的,真不知道如何下手。怎麼定位對咱們有用的代碼呢?關鍵詞 子View ,先要有 子View 而後才能談到測量和佈局吧,在 RecyclerView子View 怎麼來的? 那不就是 onCreateViewHolder() 嘛。

onCreateViewHolder() 一級一級向上查找,能夠獲得以下的執行邏輯:

....
-> LinearLayoutManager.onLayoutChild()
-> LinearLayoutManager.fill()
-> LinearLayoutManager.layoutChunk() / GridLayoutManager.layoutChunk()
-> LinearLayoutManager.LayoutState.next()
-> RecyclerView.Recycler.getViewForPosition()
-> RecyclerView.Recycler.tryGetViewHolderForPositionByDeadline()
-> RecyclerView.Adapter.createViewHolder()
-> onCreateViewHolder()
複製代碼

在這條線上去找能夠比較輕鬆的定位到 子View 是用在 layoutChunk() 方法中的,基本到這裏就能夠肯定 itemview 測量和佈局的實現代碼就是在 layoutChunk() 裏了,名字就叫佈局塊嘛。

在開始 layoutChunk() 以前,先了解一個會對理解接下來的流程頗有幫助的點,咱們知道 LinearLayoutManagerGridLayoutManager 的父類,而且 GridLayoutManager 重寫了 layoutChunk() ,這也就說明 GridLayout 也是按行繪製的,只是它把每行都分紅了 SpanCount 列,而後在每一個單元格都繪製一個 itemview 。這就是 GridLayoutManager 的繪製邏輯。如圖:

接下來看 layoutChunk() 中具體的實現:

@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    
    int count = 0;
    int consumedSpanCount = 0;
    int remainingSpan = mSpanCount;

    // 獲取某行中可添加的全部 子View
    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++;
    }

    if (count == 0) {
        result.mFinished = true;
        return;
    }

    //一行中全部 itemview 最大的寬度
    int maxSize = 0;
    //一行中全部 itemview 最大的高度, 通常是一格的高度
    float maxSizeInOther = 0; // use a float to get size per span

    // we should assign spans before item decor offsets are calculated
    assignSpans(recycler, state, count, layingOutInPrimaryDirection);
    //在這裏對 子View 進行測量
    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);
            }
        }
        // 這裏是 ItemDecorations 主要計算 itemview 之間的間隔
        calculateItemDecorationsForChild(view, mDecorInsets);

		// 測量子View	
        measureChild(view, otherDirSpecMode, false);

        //獲取 View 作佔據佈局的寬度, 包括 marigin + padding + width
        final int size = mOrientationHelper.getDecoratedMeasurement(view);
        if (size > maxSize) {
            maxSize = size;
        }
        //獲取 View 作佔據佈局的高度, 包括 marigin + padding + width
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view)
                / lp.mSpanSize;
        if (otherSize > maxSizeInOther) {
            maxSizeInOther = otherSize;
        }
    }

    //一個佈局塊消耗的最大高度
    result.mConsumed = maxSize;

    //這裏對 子View 進行佈局,這四個值所表明的就是 view 的佈局區域
    int left = 0, right = 0, top = 0, bottom = 0;
    if (mOrientation == VERTICAL) {
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            bottom = layoutState.mOffset;
            top = bottom - maxSize;
        } else {
            top = layoutState.mOffset;
            bottom = top + maxSize;
        }
    } else {
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
            right = layoutState.mOffset;
            left = right - maxSize;
        } else {
            left = layoutState.mOffset;
            right = left + maxSize;
        }
    }
    for (int i = 0; i < count; i++) {
        View view = mSet[i];
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (mOrientation == VERTICAL) {
            if (isLayoutRTL()) {
                right = getPaddingLeft() + mCachedBorders[mSpanCount - params.mSpanIndex];
                left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
            } else {
                left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
                right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
            }
        } else {
            top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
            bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
        // We calculate everything with View's bounding box (which includes decor and margins)
        // To calculate correct layout position, we subtract margins.
        // 對 ziView 佈局
        layoutDecoratedWithMargins(view, left, top, right, bottom);
        if (DEBUG) {
            Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
                    + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
                    + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)
                    + ", span:" + params.mSpanIndex + ", spanSize:" + params.mSpanSize);
        }
        // Consume the available space if the view is not removed OR changed
        if (params.isItemRemoved() || params.isItemChanged()) {
            result.mIgnoreConsumed = true;
        }
        result.mFocusable |= view.hasFocusable();
    }
    Arrays.fill(mSet, null);
}
複製代碼

看過 layoutChunk() 方法的實現,其總共分爲三步:

  • 獲取某行中可放置的全部 子View
  • 測量 子View
  • 佈局 子View

能夠看到在 layoutChunk() 代碼的開始就執行了一個 while 循環用於獲取當前行中可存放的全部 子View,用白話解釋就是 」一個 SpanCount = 4 的佈局,一行中可存放四個 SpanSize = 1 的子View,或者兩個 SpanSize = 2 的子View ....「

接下來調用 measureChild() 對全部 子View 進行寬高測量(建議看源碼),主要獲取了 子View 的佈局間隔、margin 並經過這些值計算獲得 ziView 寬高的 MeasureSpace 值。

其中 getSpaceForSpanRange() 比較很差理解,它是用於獲取在垂直方向上 itemview 的寬度。getSpaceForSpanRange() 以下:

/** 
 * @params startSpan itemview 佔據單元格起始下標
 * @params spanSize itemview 佔據單元格總數
 */
int getSpaceForSpanRange(int startSpan, int spanSize) {
    if (mOrientation == VERTICAL && isLayoutRTL()) {
        return mCachedBorders[mSpanCount - startSpan]
                - mCachedBorders[mSpanCount - startSpan - spanSize];
    } else {
        return mCachedBorders[startSpan + spanSize] - mCachedBorders[startSpan];
    }
}
複製代碼

方法內使用了一個名爲 mCachedBorders 的數組,mCachedBorders 是一個大小爲 spanCount + 1 的數組,數組內存儲的是將 RecyclerView 分紅 spanCount 列的 spanCount + 1 條線的 X 軸座標。數組第一位的值老是 0

mCachedBorders 的計算方法以下:

static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
    if (cachedBorders == null || cachedBorders.length != spanCount + 1
            || cachedBorders[cachedBorders.length - 1] != totalSpace) {
        cachedBorders = new int[spanCount + 1];
    }
    cachedBorders[0] = 0;
    int sizePerSpan = totalSpace / spanCount;
    int sizePerSpanRemainder = totalSpace % spanCount;
    int consumedPixels = 0;
    int additionalSize = 0;
    for (int i = 1; i <= spanCount; i++) {
        int itemSize = sizePerSpan;
        additionalSize += sizePerSpanRemainder;
        if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
            itemSize += 1;
            additionalSize -= spanCount;
        }
        consumedPixels += itemSize;
        cachedBorders[i] = consumedPixels;
    }
    return cachedBorders;
}
複製代碼

舉例,若是 RecyclerView.width = 1080, SpanCount = 3,那麼經計算獲得 mCachedBorder 的值就爲 [0, 360, 720, 1080]

到這裏就能夠肯定 RecyclerView 是經過 mCachedBorder 搭配 SpanSizeLookup 來計算每一個 itemview 的寬度的了。

到這裏測量的過程其實就結束了,也獲得了每一個 itemview 的寬高了。

接下來就是佈局階段了,這裏比較簡單,就是獲得,四個值 left、top、right、bottom,有一個比較重要的值 layoutState.mOffset 它是某行開始繪製的像素偏移量:

layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
複製代碼

layoutChunkResult.mConsumed 是每一個佈局塊所佔用的最大高度,layoutState.mLayoutDirection 佈局填充的方向:

  • LAYOUT_START:-1 從下往上填充佈局
  • LAYOUT_END:1 從上往下填充佈局

若是 GridLayoutManager 是垂直佈局而且從上往下填充佈局的話,layoutState.mOffset 的值就是繪製某行時其繪製起始點的 Y 軸座標。

這裏有一個疑問不太懂,既然在以前 itemview 的測量中已經得到了它的寬高,爲何確認佈局位置的時候仍是用的 mCachedBorders,不知道這是基於那些考慮。

到這裏 GridLayoutMananger 的佈局邏輯就介紹完了。咱們也知道了經過 GridLayoutManager 來實現UI美眉的設計圖也是不行的了,由於它只支持跨列而不支持跨行,萬幸,有前輩作了 SpannedGridLayoutManager 讚美開源,讚美做者 ^_^

SpannedGridLayoutManager

在講解 SpannedGridLayoutManager 的具體實現前,先說明一下它的實現邏輯,有助於理解做者的思路。

LayoutManager 中最大的難點是肯定 itemview 的佈局屬性,在 GridLayoutMananger 中是經過:

  • layoutState.mOffset:偏移量
  • SpanSize:跨度大小
  • SpanIndex:跨度起始下標
  • mCachedBorders:邊框位置
  • 等等

來肯定某個 position 所對應 itemview 的佈局屬性的。

那來看下 SpannedGridLayoutManager 的做者是經過什麼思路來解決這個難題的呢?

好比要實現上面那張UI美眉給的設計圖(如下都是按垂直方向佈局):

首先,做者定義了 SpanSize 類,定義 itemview 佔據單元格的行數和列數:

/**
 * Helper to store width and height spans
 */
class SpanSize(val width: Int, val height: Int)
複製代碼

而後,定義了一個列表 freeRects,用於存儲 RecyclerView 所佔據的整個空間中全部可用矩形範圍:

初始狀態下 freeRects 中只包含一個 Rect,它的值爲:

Rect(0, 0 - 3, 2147483647)
複製代碼

Rect 的值中能夠發現,做者在 Rect 中保存的並非具體的像素距離值,而是單元格數。如圖紅色框區域就是其表明可用的範圍,固然它的底部範圍是很大的,最多能夠佔據 Int.MAX_VALUE 個單元格,這裏我以爲畫上邊線會更好解釋一點,本身內心清楚就行了。

下面咱們來放第一個 itemview,它的寬高都佔據兩個單元格:

能夠看到當第一個 itemview 被添加到圖上以後,其將可用範圍分紅了兩個,將這兩個可用範圍按照必定規律進行排序。而後添加第二個 itemview,他的寬高只佔據一個單元格,正好能夠添加到黃色框的部分,添加並更新可用範圍,以下:

再來添加第三個 itemview,寬高還是佔據一個單元格,仍是添加到黃色框區域:

這時能夠發現,黃色框區域徹底包裹在紅色框區域內,那就能夠將表明黃色框的 Rect 去掉,只保留紅色框區域了,以下:

依次類推,經過這種方式,在不超過可用範圍的狀況下能夠隨意設置任意 itemview 的跨行和跨列。以上僅是做者的基本思路,具體的代碼實現邏輯仍是有些許的不一樣的,下面就來看具體是怎麼作的吧。

首先看代碼結構,它包含兩個內部類:

  • SpanSize:定義 itemview 佔據單元格的行數和列數。
  • RectHelper:其內實現的就是對可用矩形區域的查找、緩存和更新。

能夠看到 SpannedGridLayoutManagerGridLayoutManager 不一樣,它集成的是 RecyclerView.LayoutMananger。不要緊依然用上述 GridLayoutManager 的方法找到最接近答案的節點。

protected open fun makeView(position: Int, direction: Direction, recycler: RecyclerView.Recycler): View {
    val view = recycler.getViewForPosition(position)
    measureChild(position, view)
    layoutChild(position, view)

    return view
}
複製代碼

就很清晰,拿到 itemview,測量,佈局。

首先來看 measure 過程:

protected open fun measureChild(position: Int, view: View) {

    val freeRectsHelper = this.rectsHelper

    val itemWidth = freeRectsHelper.itemSize
    val itemHeight = freeRectsHelper.itemSize

    val spanSize = spanSizeLookup?.getSpanSize(position) ?: SpanSize(1, 1)

    val usedSpan = if (orientation == Orientation.HORIZONTAL) spanSize.height else spanSize.width

    if (usedSpan > this.spans || usedSpan < 1) {
        throw InvalidSpanSizeException(errorSize = usedSpan, maxSpanSize = spans)
    }

    // This rect contains just the row and column number - i.e.: [0, 0, 1, 1]
    val rect = freeRectsHelper.findRect(position, spanSize)

    // Multiply the rect for item width and height to get positions
    val left = rect.left * itemWidth
    val right = rect.right * itemWidth
    val top = rect.top * itemHeight
    val bottom = rect.bottom * itemHeight

    val insetsRect = Rect()
    calculateItemDecorationsForChild(view, insetsRect)

    // Measure child
    val width = right - left - insetsRect.left - insetsRect.right
    val height = bottom - top - insetsRect.top - insetsRect.bottom
    val layoutParams = view.layoutParams
    layoutParams.width = width
    layoutParams.height = height
    measureChildWithMargins(view, width, height)

    // Cache rect
    childFrames[position] = Rect(left, top, right, bottom)
}
複製代碼

這部分代碼就很好理解,首先獲得 position 對應的 SpanSize,再根據經過 RectHelper.findRect() 獲得適合 itemview 存放的矩形區域,調用 RecyclerView.measureChildWithMargins() 進行對 itemview 進行測量。能夠看到在 RectHelper.findRect()Rect 是存放在 rectsCache 數組中的。在 onLayoutChildren() 方法中能夠找到一個 for 循環,它的做用就是獲得 itemCountitemview 的佈局區域並存儲在 rectsCache 數組中。

而後就是 layout 過程:

protected open fun layoutChild(position: Int, view: View) {
    val frame = childFrames[position]

    if (frame != null) {
        val scroll = this.scroll

        val startPadding = getPaddingStartForOrientation()

        if (orientation == Orientation.VERTICAL) {
            layoutDecorated(view,
                    frame.left + paddingLeft,
                    frame.top - scroll + startPadding,
                    frame.right + paddingLeft,
                    frame.bottom - scroll + startPadding)
        } else {
            layoutDecorated(view,
                    frame.left - scroll + startPadding,
                    frame.top + paddingTop,
                    frame.right - scroll + startPadding,
                    frame.bottom + paddingTop)
        }
    }

    // A new child was layouted, layout edges change
    updateEdgesWithNewChild(view)
}
複製代碼

這部分代碼也很簡單,可是這裏面加入了 scroll 的處理,因爲這篇文章不涉及 scroll,將其排除在外的話理解起來仍是沒有難度的。

其實整個 SpannedGridLayoutManager 中最重要的是對於 itemview 佈局區域的獲取,其大部分代碼都存在於 subtract() 方法中。理解了這個方法也就理解了做者對於佈局的處理。

到這裏也就介紹完了,再次感嘆做者的思路,讚美開源精神。讀完了 GridLayoutManagerSpannedGridLayoutManager 的源碼,發現 RecyclerView 對於 itemview 的佈局就好像是在玩兒拼圖遊戲的感受,從左至右,從上到下,依次排列每個 子View,其實和咱們在作相似拼圖遊戲的時候規定按從左至右,從上到下的規則排列大小不等的塊的處理邏輯是否是很像。那 GridLayoutManagerSpannedGridLayoutManager 的做者在實現的時候是否是就是將這種咱們大腦處理相似問題的邏輯給翻譯成代碼了呢。

自定義 LayoutManager

如今來看下若是要自定一個 LayoutManager 的話須要作些什麼,或者須要重寫那些方法。

class MyLayoutManager extends RecyclerView.LayoutManager {
    //==============================================================================================
    //  * 必須重寫
    //  ~ 惟一 abstract 方法
    //==============================================================================================
    /**
     * * 必須重寫
     * 它是 `LayoutManager` 中惟一的 `abstract` 方法, 爲子類提供 LayoutParams,
     * 可使用 RecyclerView.LayoutParams() 對象也能夠自定義的 ViewGroup.LayoutParams 對象.
     * 若是自定義話須要重寫以下方法:
     * checkLayoutParams(LayoutParams)
     * generateLayoutParams(android.view.ViewGroup.LayoutParams)
     * generateLayoutParams(android.content.Context,android.util.AttributeSet)
     */
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return null;
    }

    //==============================================================================================
    //  * 必須重寫
    //  ~ 佈局相關
    //==============================================================================================

    /**
     * * 必須重寫
     * 主要實現 itemview 的 measure 和 layout 過程
     */
    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        super.onLayoutChildren(recycler, state);
    }

    //==============================================================================================
    //  * 必須重寫
    //  ~ 滑動相關
    //==============================================================================================

    /**
     * 是否能夠水平滑動
     */
    @Override
    public boolean canScrollHorizontally() {
        return super.canScrollHorizontally();
    }

    /**
     * 是否能夠垂直滑動
     */
    @Override
    public boolean canScrollVertically() {
        return super.canScrollVertically();
    }

    /**
     * 控制在水平方向上的滑動距離
     */
    @Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        return super.scrollHorizontallyBy(dx, recycler, state);
    }

    /**
     * 控制在垂直方向上的滑動距離
     */
    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        return super.scrollVerticallyBy(dy, recycler, state);
    }

    /**
     * 滑動到適配器指定位置
     */
    @Override
    public void scrollToPosition(int position) {
        super.scrollToPosition(position);
    }

    /**
     * 平滑滾動到適配器指定位置
     * 建立 SmoothScroller 實例並調用 startSmoothScroll()
     */
    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        super.smoothScrollToPosition(recyclerView, state, position);
    }

    //==============================================================================================
    //  ~ 狀態相關
    //==============================================================================================

    @Nullable
    @Override
    public Parcelable onSaveInstanceState() {
        return super.onSaveInstanceState();
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
    }
}

複製代碼

若是想支持滾動條須要重寫這些方法:

public int computeHorizontalScrollExtent(@NonNull State state) {
    return 0;
}

public int computeHorizontalScrollOffset(@NonNull State state) {
    return 0;
}

public int computeHorizontalScrollRange(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollExtent(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollOffset(@NonNull State state) {
    return 0;
}

public int computeVerticalScrollRange(@NonNull State state) {
    return 0;
}
複製代碼

最重要的其實仍是 onLayoutChildren() 方法了,其餘方法均可以對照 LinearLayoutManager 或者 GridLayoutManager 來寫,或者乾脆跟 GridLayoutManager 同樣,繼承於 LinearLayoutManager 專心於 onLayoutChildren() 實現想要的佈局也是能夠的。

若是對你有幫助的話留下贊吧 ^_^

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息