RecyclerView 原始碼分析(七) - 自定義LayoutManager及其相關元件的原始碼分析
對於使用 ReccyclerView
的我們來說, LayoutManager
早已非常熟悉。可是,有沒有想過我們所說的熟悉是哪種熟悉?對的,就是會使用而已,這其中包括谷歌爸爸幫我們實現的幾種 LayoutManager
,例如: LinearLayoutManager
, GridLayoutManager
等等。
仔細想一想,我們使用 LayoutManager
就像我們當初初學Android時使用各種基礎控制元件,我們處於只會使用的階段,如果後續有一些特殊的要求,系統的實現已經不能滿足我們自身的需求,此時自定義 LayoutManager
就必須出手了。同時,如果想要自定義 LayoutManager
,我們就必須瞭解它相關的原理。所以,學習 LayoutManager
的原始碼是至關重要的。
本文參考資料:
- RecyclerView系列(7)—自定義LayoutManager(上),視覺上定義一個具備上下邊界的RecyclerView.layoutMnager
- RecyclerView系列(7)—自定義LayoutManager(上),視覺上定義一個具備上下邊界的RecyclerView.layoutMnager
- LayoutManagerGroup
介於 LayoutManger
的特殊性,我們不可能將 LayoutManager
及其所有子類的程式碼都分析一遍,所以本文的原始碼分析重點是,從原始碼角度來解釋為什麼這樣自定義 LayoutManager
。自定義 LayoutManager
要求的門檻相對較高,它不是簡單的照著模板來寫,而是需要了解它內部的原理,這其中包括回收機制(這個我們在分析 RecyclerView
的三大流程時已經從 LinearLayoutManager
內部看到了),滑動機制等等。所以,在自定義 LayoutManager
時,我預設大家都懂得這些原理,如果還有同學不懂的話,可以參考我的文章:
- RecyclerView 原始碼分析(一) - RecyclerView的三大流程
- RecyclerView 原始碼分析(二) - RecyclerView的滑動機制
- RecyclerView 原始碼分析(三) - RecyclerView的快取機制
本文打算從如下幾個角度來分析 LayoutManager
:
- 知識儲備--相關方法的解釋,這裡的相關方法主要是自定義涉及到的方法
- 自定義一個
LayoutManager
-
SnapHelper
基本使用、原始碼分析和自定義SnapHelper
1. 概述
在正式分析 LayoutManager
之前,我們先來對 LayoutManager
及其它的相關元件做一個簡單的概述。
我們都知道 LayoutManager
就是一個佈局管理器,主要負責 RecyclerView
的 ItemView
測量和佈局,所以自定義 LayoutManager
的過程跟自定義 View
的過程非常的相似。本文打算從一個Demo開始來介紹怎麼自定義一個 LayoutManager
,效果如下:

LayoutManager
相關的兩個元件--
SnapHelper
和
SmoothScroller
。這個其中
SnapHelper
主要負責來調整
RecyclerView
的滑動距離,比如想要在滑動結束之後,
ItemView
停留在
RecyclerView
正中央,可以依靠
SnapHelper
;
SmoothScroller
主要是用來實現緩慢滑動的。關於這兩個元件,在後面我會簡單的分析一下,相對於來,這兩個元件還比較簡單。
2. LayoutManager的相關方法
我們在自定義 LayoutManager
之前,先來看一下 LayoutManager
的幾個方法。
方法名 | 作用 |
---|---|
generateDefaultLayoutParams | 抽象方法,必須實現。這個方法的作用主要是給 RecyclerView 的 ItemView 生成 LayoutParams |
onMeasure | 用來測量 RecyclerView 的大小的。通常不用重寫此方法,但是在一種情況下必須重寫,就是 LayouytManager 不支援自動測量,這種情況下 RecyclerView 不會進行自我測量,會呼叫 LayoutManager 的 onMeasure 方法來測量。 |
onLayoutChildren | 此方法的作用是佈局 ItemView 。此方法就像是 ViewGroup 的 onLayout 方法, RecyclerView 內部的 ItemView 怎麼佈局,全看這個方法怎麼實現。 |
canScrollHorizontally | 設定該 LayoutManager 的 RecyclerView 是否可以水平滑動。與之對應的還有 canScrollVertically ,用來設定 RecyclerView 是否垂直滑動 |
scrollHorizontallyBy | 水平可以滑動的距離。此方法帶一個dx引數,表示 RecyclerView 已經產生了 dx 的滑動距離,此時我們需要做的是呼叫相關方法,進行重新佈局。同時此方法的返回值表示水平可以滑動的距離。與之對應的方法是 scrollVerticallyBy 。 |
3. 自定義LayoutManager
簡單的瞭解了自定義 LayoutManager
的幾個方法,現在我將帶領來實現一個Demo,具體的效果就是上面的gif動圖,我們來看看怎麼自己實現一個 LayoutMananger
。
(1). 重寫generateDefaultLayoutParams方法
首先,自定義 LayoutManager
的第一步就是重寫 generateDefaultLayoutParams
方法,這個方法的作用在上面我已經介紹了,在這裡就不介紹了。通常來說,我們這樣來實現 generateDefaultLayoutParams
方法就行了:
@Override public RecyclerView.LayoutParams generateDefaultLayoutParams() { return new RecyclerView.LayoutParams(RecyclerView.LayoutParams.WRAP_CONTENT, RecyclerView.LayoutParams.WRAP_CONTENT); }
我們這裡沒有特殊的要求,所以讓每個 ItemView
的自適應就行了。
(2). onLayoutChildren方法
然後,第二步就是重寫 onLayoutChildren
方法,也是最複雜的一步。在這一步,我們主要完成兩步:
- 定位每個ItemView的位置,然後佈局。
- 適配滑動和縮放的效果。
我們先來結合圖片來分析一下這個效果。

整個效果我們可以這麼來考慮, ItemView
是從左往右開始佈局,不過我們得從從右往左計算每個 ItemView
的寬高,因為最右邊的 ItemView
寬高是最原始,同時它的left位置也是最容易的計算( RecyclerView
的水平空閒空間減去 ItemView
的 width
就行。)。
然後我們可以設定一個 offset
,後面的 ItemView
根據這個offset來重新定位。我們通過之前看 LinearLayoutManager
原始碼的經驗,發現 LinearLayoutManager
計算位置通過一個 remainSpace
變數來實現的。 remainSpace
表示當前 RecyclerView
的剩餘空間,每佈局一個 ItemView
, remainSpace
減去小消耗的距離就OK!
下面我結合程式碼來具體分析:
@Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { if (state.getItemCount() == 0 || state.isPreLayout()) return; removeAndRecycleAllViews(recycler); if (!mHasChild) { mItemViewHeight = getVerticalSpace(); mItemViewWidth = (int) (mItemViewHeight / mItemHeightWidthRatio); mHasChild = true; } mItemCount = getItemCount(); mScrollOffset = makeScrollOffsetWithinRange(mScrollOffset); fill(recycler); }
在 onLayoutChildren
方法裡面,我們初始化了幾個變數,其中 mItemViewHeight
和 mItemViewWidth
兩個變數分別表示 ItemView
的高和寬。其次就是 mScrollOffset
的初始化:
private int makeScrollOffsetWithinRange(int scrollOffset) { return Math.min(Math.max(mItemViewWidth, scrollOffset), mItemCount * mItemViewWidth); }
第一次呼叫 onLayoutChildren
方法來初始化 mScrollOffset
時, mScrollOfffet
的值被設定為 mItemCount * mItemViewWidth
。這有什麼意義呢?我待會會解釋。
在 onLayoutChidlren
方法的最後,呼叫 fill
方法。 fill
方法才是真正計算每個 ItemView
的位置,我們來看看:
private void fill(RecyclerView.Recycler recycler) { // 1.初始化基本變數 int bottomVisiblePosition = mScrollOffset / mItemViewWidth; final int bottomItemVisibleSize = mScrollOffset % mItemViewWidth; final float offsetPercent = bottomItemVisibleSize * 1.0f / mItemViewWidth; final int space = getHorizontalSpace(); int remainSpace = space; final int defaultOffset = mItemViewWidth / 2; final List<ItemViewInfo> itemViewInfos = new ArrayList<>(); // 2.計算每個ItemView的位置資訊(left和scale) for (int i = bottomVisiblePosition - 1, j = 1; i >= 0; i--, j++) { double maxOffset = defaultOffset * Math.pow(mScale, j - 1); int start = (int) (remainSpace - offsetPercent * maxOffset - mItemViewWidth); ItemViewInfo info = new ItemViewInfo(start, (float) (Math.pow(mScale, j - 1) * (1 - offsetPercent * (1 - mScale)))); itemViewInfos.add(0, info); remainSpace -= maxOffset; if (remainSpace < 0) { info.setLeft((int) (remainSpace + maxOffset - mItemViewWidth)); info.setScale((float) Math.pow(mScale, j - 1)); break; } } // 3.新增最右邊ItemView的相關資訊 if (bottomVisiblePosition < mItemCount) { final int left = space - bottomItemVisibleSize; itemViewInfos.add(new ItemViewInfo(left, 1.0f)); } else { bottomVisiblePosition -= 1; } // 4.回收其他位置的View final int layoutCount = itemViewInfos.size(); final int startPosition = bottomVisiblePosition - (layoutCount - 1); final int endPosition = bottomVisiblePosition; final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View childView = getChildAt(i); final int position = convert2LayoutPosition(i); if (position > endPosition || position < startPosition) { detachAndScrapView(childView, recycler); } } // 5.先回收再佈局 detachAndScrapAttachedViews(recycler); for (int i = 0; i < layoutCount; i++) { fillChild(recycler.getViewForPosition(convert2AdapterPosition(startPosition + i)), itemViewInfos.get(i)); } }
在分析上面的程式碼之前,我先來對幾個變數做一個統一的解釋。
變數名 | 含義 |
---|---|
bottomVisiblePosition | 表示此時 RecyclerView 最右邊能看見的 ItemView 的 position 。例如說,初始情況下, bottomVisiblePosition 就等於 ItemCount ,當然此時 bottomVisiblePosition 的結果肯定是不對的,後面在使用時會根據情況來調整。 |
bottomItemVisibleSize | 這個變數沒有特殊意義,主要的用來計算 offsetPercent |
offsetPercent | 滑動的百分比,從1.0f~0.0f變化。 |
defaultOffset | 每個 ItemView 偏移的值(預設所有的 ItemView 都是左對齊) |
然後就是計算每個 ItemView
的位置了。這裡需要注意一個問題,就是 bottomVisiblePosition == mItemCount
的情況。
當bottomVisiblePosition == mItemCount時,也是最初的狀態,這種情況下,第二步就是直接將最右邊的 ItemView
的位置資訊計算出來。
當bottomVisiblePosition < mItemCoun時(沒有大於的情況)時,也是在滑動的時,是在第三步時將最右邊的 ItemView
的位置資訊計算出來。
關於位置資訊的計算,這裡就不討論了,都是一些常規的計算邏輯。
最後就是佈局,呼叫的是 fillChild
方法:
private void fillChild(View view, ItemViewInfo itemViewInfo) { addView(view); measureChildWithExactlySize(view); final int top = getPaddingTop(); layoutDecoratedWithMargins(view, itemViewInfo.getLeft(), top, itemViewInfo.getLeft() + mItemViewWidth, top + mItemViewHeight); view.setScaleX(itemViewInfo.getScale()); view.setScaleY(itemViewInfo.getScale()); }
fillChild
方法沒有解釋的必要,熟悉自定義 View
的同學應該都懂。
到這裡 onLayoutChildren
方法算是重新完畢了,這個過程中,比較難以理解的是位置資訊的計算,這個我也不知道怎麼解釋,大家就自己發揮想象力吧。
(3). 水平滑動
接下來就是讓 RecyclerView
支援水平滑動。要想支援水平滑動,我們必須重寫 canScrollHorizontally
方法和 scrollHorizontallyBy
方法,我們來看看:
@Override public boolean canScrollHorizontally() { return true; } public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { int pendingScrollOffset = mScrollOffset + dx; mScrollOffset = makeScrollOffsetWithinRange(pendingScrollOffset); fill(recycler); return mScrollOffset - pendingScrollOffset + dx; }
這個過程中,需要特別注意的是 scrollHorizontallyBy
方法,我們不能直接讓 mScrollOffset
加上 dx
,因為 mScrollOffset
的範圍在 [mItemViewWidth,mItemCount * mItemViewWidth]
,所以在每次滑動之後需要調整,得再一次呼叫 makeScrollOffsetWithinRange
方法。
(3). 滑動之後最右邊的ItemView都能完整顯示
這個需求就非常的簡單,自我實現一個 SnaHelper
,然後這樣使用就OK了:
private final SnapHelper mSnapHelper = new CustomSnapHelper(); @Override public void onAttachedToWindow(RecyclerView view) { super.onAttachedToWindow(view); mSnapHelper.attachToRecyclerView(view); }
這裡面具體的含義這裡先不解釋,待會在分析 SnaHelper
時會詳細的解釋。
(5). 原始碼
整個 LayoutManager
的自定義過程就OK了,具體的效果就是上面的動圖效果。
還有不懂的同學可以我的github去下載原始碼: LayoutManagerDemo 。特別感謝: LayoutManagerGroup ,本文自定義的LayoutManager大部分思路和原始碼都來至於它。
4. SnapHelper
SnaHelper
的存在對於 RecyclerView
來說,可謂是如虎添翼。 SnaHelper
可見幫助我們實現一些特殊的效果,比如說,我們可以使用 RecyclerView
和 SnapHelper
去實現 ViewPager
的效果。
通常來說,我們在日常開發中,使用 RecyclerView
很少遇到的 SnapHelper
,不過,如果你想要自定義 LayoutManager
來實現一些特殊效果,很大的可能性會遇到 SnapHelper
。那麼 SnapHelper
到底是什麼呢?是怎麼使用的呢?它的實現原理又是什麼呢?這是本文需要解答的三個問題。
簡單來說, SnapHelper
就是一個Helper類,只是它的內部有兩個監聽介面: OnFlingListener
和 OnScrollListener
,分別用來監聽 RecyclerView
的scroll事件和fling事件。
而 SnapHelper
的使用也是非常的簡單,就是在 LayoutManager
的 onAttachedToWindow
方法呼叫 SnapHelper
的 attachToRecyclerView
方法即可。我們就從 attachToRecyclerView
方法為入口來分析 SnapHelper
的原始碼。
(1). SnapHelper的原始碼分析
SnapHelper
的原理實際上是非常的簡單,大家不要害怕。我們在分析 SnapHelper
原始碼之前,先來了解 SnapHelper
幾個比較重要的方法:
方法名 | 返回型別 | 含義 |
---|---|---|
calculateDistanceToFinalSnap | int[] | 計算 RecyclerView 最終滑動的距離。返回的是一個長度為2的陣列,其中0位置表示水平滑動的滑動距離,1位置表示垂直滑動的距離。 |
findTargetSnapPosition | int | 這個方法表示fling操作最終能滑動到I的temView的position。這個position稱為 targetSnapPosition ,位置上對應的 View 就是 targetSnapView 。如果找不到position,就返回 RecyclerView.NO_POSITION |
findSnapView | View | 最終滑動位置對應的ItemView |
在這裡,我們必須區分一下 findTargetSnapPosition
方法和 calculateDistanceToFinalSnap
、 findSnapView
方法的區別。
- findTargetSnapPosition :此方法表示fling滑動能滑到的位置。
- calculateDistanceToFinalSnap和findSnapView :這兩個方法表示正常滑動的能到達位置,其中
calculateDistanceToFinalSnap
表示距離,這個過程涉及到因為對齊操作而進行的距離重新調整;findSnapView
方法表示正常滑動能到達的位置對應的ItemView
。
所以,我們在自定義 SnapHelper
時,為了簡單起見,不可以處理fling操作,也就是 findTargetSnapPosition
返回為 RecyclerView.NO_POSITION
,然後讓 RecyclerView
自己進行 fling
,等待滑動結束之後,會回撥我們的 calculateDistanceToFinalSnap
和 findSnapView
來進行位置對齊。這樣做的好處就是,我們不用既考慮fling又考慮普通滑動。
A.attachToRecyclerView方法
準備的差不多了,接下來我們正式分析 SnapHelper
的原始碼。我們來看看 attachToRecyclerView
方法:
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) throws IllegalStateException { if (mRecyclerView == recyclerView) { return; // nothing to do } if (mRecyclerView != null) { destroyCallbacks(); } mRecyclerView = recyclerView; if (mRecyclerView != null) { setupCallbacks(); mGravityScroller = new Scroller(mRecyclerView.getContext(), new DecelerateInterpolator()); snapToTargetExistingView(); } }
attachToRecyclerView
非常的簡單,就是設定給 RecyclerView
設定了兩個監聽介面:
private void setupCallbacks() throws IllegalStateException { if (mRecyclerView.getOnFlingListener() != null) { throw new IllegalStateException("An instance of OnFlingListener already set."); } mRecyclerView.addOnScrollListener(mScrollListener); mRecyclerView.setOnFlingListener(this); }
然後 RecyclerView
開心的滑動,就會回撥到我們的兩個監聽事件裡面來。
B.OnScrollListener
我們先來看看 OnScrollListener
介面的實現,看看它做了哪些事情:
private final RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() { boolean mScrolled = false; @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) { mScrolled = false; snapToTargetExistingView(); } } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (dx != 0 || dy != 0) { mScrolled = true; } } };
我們發現,當 RecyclerView
滑動結束之後,就會呼叫 snapToTargetExistingView
方法。那 snapToTargetExistingView
方法是幹嘛的呢?其實就是保證對齊的。我們來看看:
void snapToTargetExistingView() { if (mRecyclerView == null) { return; } LayoutManager layoutManager = mRecyclerView.getLayoutManager(); if (layoutManager == null) { return; } View snapView = findSnapView(layoutManager); if (snapView == null) { return; } int[] snapDistance = calculateDistanceToFinalSnap(layoutManager, snapView); if (snapDistance[0] != 0 || snapDistance[1] != 0) { mRecyclerView.smoothScrollBy(snapDistance[0], snapDistance[1]); } }
我們發現,在這裡先是呼叫了 findSnapView
方法找到滑動的最終 ItemView
,然後根據找到的 SnapView
,呼叫 calculateDistanceToFinalSnap
方法來計算滑動的距離,最後呼叫相關方法來進行對齊。整個過程就是這麼的簡單。
C. OnFlingListener
SnapHelper
內部本身沒有一個 OnFingListener
介面物件,而是自身實現了 OnFingListener
,所以當 RecyclerView
在fling時,會回撥此 onFling
方法。我們來看看:
@Override public boolean onFling(int velocityX, int velocityY) { LayoutManager layoutManager = mRecyclerView.getLayoutManager(); if (layoutManager == null) { return false; } RecyclerView.Adapter adapter = mRecyclerView.getAdapter(); if (adapter == null) { return false; } int minFlingVelocity = mRecyclerView.getMinFlingVelocity(); return (Math.abs(velocityY) > minFlingVelocity || Math.abs(velocityX) > minFlingVelocity) && snapFromFling(layoutManager, velocityX, velocityY); }
首先,我們要明白一個東西,如果 RecyclerView
有一個 OnFlingListener
處理fling事件的話,那麼 RecyclerView
就不會再處理fling事件。
所以 SnapHelper
是否處理fling事件,還需要看它的 snapFromFling
方法。我們來看看:
private boolean snapFromFling(@NonNull LayoutManager layoutManager, int velocityX, int velocityY) { if (!(layoutManager instanceof ScrollVectorProvider)) { return false; } SmoothScroller smoothScroller = createScroller(layoutManager); if (smoothScroller == null) { return false; } int targetPosition = findTargetSnapPosition(layoutManager, velocityX, velocityY); if (targetPosition == RecyclerView.NO_POSITION) { return false; } smoothScroller.setTargetPosition(targetPosition); layoutManager.startSmoothScroll(smoothScroller); return true; }
在上面的程式碼中,我們發現, findTargetSnapPosition
如果返回為 RecyclerView.NO_POSITION
,那麼 SnapHelper
就不會處理fling事件。而如果 SnapHelper
要處理fling事件的話,會通過 LayoutManager
的 startSmoothScroll
方法。這裡面的原理實際上還是呼叫到 RecyclerView
的 ViewFlinger
裡面去了。
整個 SnapHelper
的原理就是這樣,非常的簡單,接下來我們結合實際來看看怎麼自定義一個 SnapHelper
。
(2).自定義SnapHelper
通常來說,我們自定義 SnapHelper
,實現三個抽象方法就已經差不多,分別是 calculateDistanceToFinalSnap
方法、 findTargetSnapPosition
方法和 findSnapView
方法就已經夠了。我麼來看看我們自己實現的 CustomSnapHelper
:
public class CustomSnapHelper extends SnapHelper { @Override public int[] calculateDistanceToFinalSnap( @NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) { if (layoutManager instanceof CustomLayoutManger) { int[] out = new int[2]; if (layoutManager.canScrollHorizontally()) { out[0] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition( layoutManager.getPosition(targetView)); out[1] = 0; } else { out[0] = 0; out[1] = ((CustomLayoutManger) layoutManager).calculateDistanceToPosition( layoutManager.getPosition(targetView)); } return out; } return null; } @Override public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) { return RecyclerView.NO_POSITION; } @Override public View findSnapView(RecyclerView.LayoutManager layoutManager) { if (layoutManager instanceof CustomLayoutManger) { int pos = ((CustomLayoutManger) layoutManager).getFixedScrollPosition(); if (pos != RecyclerView.NO_POSITION) { return layoutManager.findViewByPosition(pos); } } return null; } }
方法的具體含義我這裡就不再解釋了,大家可以我的Demo專案和上面對三個方法的解釋來進行理解,總之來說, SnapHelper
還是比較簡單的。
5. 總結
到這裡,我們對 LayoutManager
相關分析就差不多,在最後,我做一個小小的總結。
- 自定義LayoutManager需要注意四點:1.重寫
generateDefaultLayoutParams
方法;2.重寫onLayoutChildren
方法,對ItemView
進行佈局;3. 處理滑動,例如水平滑動需要重寫canScrollHorizontally
和scrollHorizontallyBy
;4. 如果需要處理對齊問題,可以使用SnapHelper
。 - 自定義
SnapHelper
我們只需要重寫它的三個抽象方法即可,分別是:calculateDistanceToFinalSnap
、findTargetSnapPosition
和findSnapView
。需要注意的是,為了簡單起見,我們可以直接在findTargetSnapPosition
內部返回RecyclerView.NO_POSITION
,讓RecyclerView
來幫助我們處理fling事件。
如果不出意外的話,接下來我將分析 ItemAnimator
。