抽絲剝繭RecyclerView
系列文章的目的在於幫助Android開發者提升對RecyclerView的認知,本文是整個系列的第二篇。git
前文回顧:github
LayoutManager
是RecyclerView
中的重要一環,使用LayoutManager
就跟玩捏臉蛋的遊戲同樣,即便好看的五官(好看的子View)都具有了,也不必定能捏出漂亮的臉蛋,好在RecyclerView
爲咱們提供了默認的模板:LinearLayoutManager
、GridLayoutManager
和StaggeredGridLayoutManager
。bash
說來慚愧,若是不是看了GridLayoutManager
的源碼,我還真不知道GridLayoutManager
居然能夠這麼使用,圖片來自網絡: 網絡
不過呢,今天咱們講解的源碼不是來自GridLayoutManager
,而是線性佈局LinearLayoutManager
(GridLayoutManager
也是繼承自LinearLayoutManager
),分析完源碼,我還將給你們帶來實戰,完成如下的效果: app
代碼地址:github.com/mCyp/Orient…ide
本着認真負責的精神,我把RecyclerView
中用到LayoutManager
的地方大體看了一遍,發現其負責的主要業務:源碼分析
Recyler
處理)。回收和複用子View
顯然不是LayoutManager
實際完成的,不過,子View
的新增和刪除都是LayoutManager
通知的,除此之外,滑動處理的本質仍是對子View
進行管理,因此,本文要討論的只有測量和佈局子View
的。佈局
測量和佈局子View
發生在RecyclerView
三大工做流程,又...又回到了最初的起點?這是咱們在上篇討論過的,若是不涉及到LayoutManager
的知識,咱們將一筆帶過便可。post
在RecyclerView#onMeasure
方法中,LayoutManager
是否支持自動測量會走不一樣的流程:
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 未複寫的狀況下默認調用RecyclerView#defaultOnMeasure方法
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final Boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// 長和寬的MeasureSpec都爲EXACTLY的狀況下會return
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// 1. 計算寬度和長度等
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// 2. 佈局子View
dispatchLayoutStep2();
// 3. 測量子View的寬和高,並再次測量父佈局
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
if (mLayout.shouldMeasureTwice()) {
// 再走一遍1,2,3
}
} else {
// ...
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// ....
}
}
複製代碼
從代碼上來看,使用自動測量機制須要具有:
RecyclerView
佈局的長和寬的SpecMode
不能是MeasureSpec.EXACTLY
(大機率指的是佈局中RecyclerView
長或寬中有WrapContent
)。RecyclerView
設置的LayoutManger
的isAutoMeasureEnabled
返回爲true
。當設置自動測量機制的時候,咱們的流程以下:
LayoutManager#onMeasure
方法,還不如不使用測量機制呢!
顯然,這種想法是不對的,由於官方是這麼說的,若是不使用自動測量機制,須要在自定義LayoutManager
過程當中複寫LayoutManager#onMeasure
方法,因此呢,這個方法應該是包括自動測量機制的所有過程,包括:測量父佈局
-佈置子View
-從新測量子View
-從新測量父佈局
,而使用自動測量機制是不須要複寫這個方法的,該方法默認測量父佈局。
須要說起的是,咱們平時使用的三大LayoutManager
都開啓了自動測量機制。
即便RecyclerView
在onMeasure
方法中逃過了佈局子View
,那麼在onLayout
中也不可避免,在上一篇博客中,咱們瞭解到RecyclerView
經過LayoutManager#onLayoutChildren
方法實現給子View佈局,咱們以LinearLayoutManager
爲例,看看其中的奧祕。
在正式開始以前,咱們先看看LinearLayoutManager
中幾個重要的類:
重要的類 | 解釋 |
---|---|
LinearLayoutManager |
這個你們都懂,線性佈局。 |
AnchorInfo |
繪製子View的時候,記錄其位置、偏移量、方向等基礎信息。 |
LayoutChunkResult |
加載子View結果狀況的記錄,好比已經填充的子View 的數量。 |
LayoutState |
當前加載的狀態記錄,好比當前繪製的偏移量,屏幕還剩餘多少空間等 |
直接看最重要的LinearLayoutManager#onLayoutChildren
,代碼被我一刪再刪後以下:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//... 省略的代碼爲:數據爲0的狀況下移除全部的子View,將子View加入到緩存
// 第一步:初始化LayoutState 配置LayoutState參數
ensureLayoutState();
mLayoutState.mRecycle = false;
// ...
// 第二步:尋找焦點子View
final View focused = getFocusedChild();
// ...
// 第三步:移除界面中已經存在的子View,並放入緩存
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// 第四步:更新LayoutSatete,填充子View
// 填充也分爲兩步:1.從錨點處向結束方向填充 2.從錨點處向開始方向填充
// fill towards end 往結束方向填充子View
// 更新LayoutState
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
//...
// fill towards start 往開始方向填充子View
// 更新LayoutState等信息
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
if (mLayoutState.mAvailable > 0) {
// 若是還有剩餘空間
updateLayoutStateToFillEnd(lastElement, endOffset);
fill(recycler, mLayoutState, state, false);
// ...
}
}
// ...
// 第五步:整理一些參數,以及作一下結束處理
// 不是預佈局的狀態下結束給子View佈局,不然,重置錨點信息
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
//...
}
複製代碼
整個onLayoutChildren
能夠分爲以下五個過程:
LayoutState
子View
View
,回收ViewHolder
View
第一步是建立LayoutState
,第二步是獲取屏幕中的焦點子View
,代碼比較簡單,感興趣的同窗們能夠本身查詢。
在填充子View
前,若是當前已經存在子View
並將繼續存在的時候,會先從屏幕中暫時移除,將ViewHolder
暫存在Recycler的一級緩存mAttachedScrap
中:
/**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
*
* @param recycler Recycler to scrap views into
*/
public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderint(view);
if (viewHolder.shouldIgnore()) {
return;
}
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
// 無效的ViewHolder會被添加進RecyclerPool
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
// 添加進一級緩存
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
複製代碼
上面的英文註釋其實就是我開始所說的,暫時保存被detach
的ViewHolder
,至於Recycler
如何保存,咱們在上一篇博客中已經討論過,這裏再也不贅述。
最複雜的就是子View
的填充過程,回到LinearLayoutManager#onLayoutChildren
方法,咱們假設mAnchorInfo.mLayoutFromEnd
爲false
,那麼LinearLayoutManager
會先從錨點處往下填充,直至填滿,往下填充前,會先更新LayoutState
:
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
// mAvailable:能夠填充的距離
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
// 填充方向
mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
LayoutState.ITEM_DIRECTION_TAIL;
// 當前位置
mLayoutState.mCurrentPosition = itemPosition;
mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
// 當前位置的偏移量
mLayoutState.mOffset = offset;
mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}
複製代碼
更新完LayoutState
之後,就是子View
的真實填充過程LinearLayoutManager#fill
:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, Boolean stopOnFocusable) {
// 獲取可使用的空間
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// ...
// 滑動發生時回收ViewHolder
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 核心加載過程
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//... 省略的是:加載一個ViewHolder以後處理狀態信息
}
// 返回消費的空間
return start - layoutState.mAvailable;
}
複製代碼
最核心的就是while
循環裏面的LinearLayoutManager#layoutChunk
,最後來看一下該方法如何實現的:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 利用緩存策略獲取 與Recycler相關
View view = layoutState.next(recycler);
// 添加或者刪除 最後會通知父佈局新增或者移除子View
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
// 測量子View
measureChildWithMargins(view, 0, 0);
// 佈局子View
layoutDecoratedWithMargins(view, left, top, right, bottom);
// ... 設置LayoutChunkResult參數
}
複製代碼
首先,View view = layoutState.next(recycler);
就是咱們在上一節中討論利用緩存Recycler
去獲取ViewHolder
,接着獲取ViewHolder
中綁定的子View
,給它添加進父佈局RecyclerView
,而後給子View測量一下寬高,最後,有了寬高信息,給它放置到具體的位置就完事了,過程清晰明瞭。
回到上個方法LinearLayoutManager#fill
,在While循環
而且有數據的狀況下,不斷的將子View
填充至RecyclerView
中,直至該方向填滿。
再回到一開始的LinearLayoutManager#onLayoutChildren
方法,除了調用了咱們第四步一開始介紹的LinearLayoutManager#updateLayoutStateToFillEnd
,還調用了LinearLayoutManager#updateLayoutStateToFillStart
,因此從總體上來看,它是先填充錨點至結束的方向,再填充錨點至開始的方向(不絕對),若是用一圖表示,我以爲能夠是這樣:
第五步就是對以前的子View
的填充結果作一些處理,不作過多介紹。
看了Vivian的TimeLine,你可能會這麼吐槽,人家的庫藉助StaggeredGridLayoutManager
就能夠實現時間軸,爲什麼還要畫蛇添足,使用個人TwoSideLayoutManager
(我給實現的佈局方式起名叫TwoSideLayoutManager
)呢?由於使用瀑布流StaggeredGridLayoutManager
想要在時間軸上實現子View平均分佈的效果仍是比較困難的,可是,使用TwoSideLayoutManager
實現起來就簡單多了。
那麼咱們如何實現RecyclerView
的兩側佈局呢?一張圖來打開思路:
TwoSideLayoutManager
的佈局實現能夠利用
LinearLayoutManager
的實現方式,僅須要修改添加子
View
之後的測量邏輯和佈局邏輯便可。
上面咱們提到過,添加子View
,給子View
測量,佈局都在LinearLayoutManager#layoutChunk
中實現,那咱們徹底能夠照搬LinearLayoutManager
的填充邏輯,稍微改幾處代碼,限於篇幅,咱們就看一下核心方法TwoSideLayoutManager#layoutChunk
:
private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
// 沒有更多的數據用來生成子View
result.mFinished = true;
return;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
// 添加進RecyclerView
if (layoutState.mLayoutDirection != LayoutState.LAYOUT_START) {
addView(view);
} else {
addView(view, 0);
}
// 第一遍測量子View
measureChild(view);
// 佈局子View
layoutChild(view, result, params, layoutState, state);
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
複製代碼
總體邏輯在註釋中已經寫得很清楚了,挨個看一下主要方法。
測量子View
:
private void measureChild(View view) {
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
int verticalUsed = lp.bottomMargin + lp.topMargin;
int horizontalUsed = lp.leftMargin + lp.rightMargin;
// 設置測量的長度爲可用空間的一半
final int availableSpace = (getWidth() - (getPaddingLeft() + getPaddingRight())) / 2;
int widthSpec = getChildMeasureSpec(availableSpace, View.MeasureSpec.EXACTLY
, horizontalUsed, lp.width, true);
int heightSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
verticalUsed, lp.height, true);
measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec, false);
}
複製代碼
高度的使用方式跟LinearLayoutManager
同樣,寬度控制在屏幕可用空間的一半。
佈局子View
:
private void layoutChild(View view, LayoutChunkResult result
, RecyclerView.LayoutParams params, LayoutState layoutState, RecyclerView.State state) {
final int size = mOrientationHelper.getDecoratedMeasurement(view);
final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
result.mConsumed = size;
int left, top, right, bottom;
int num = params.getViewAdapterPosition() % 2;
// 根據位置 奇偶位來進行佈局
// 若是起始位置爲左側,那麼偶數位爲左側,奇數位爲右側
if (isLayoutRTL()) {
if (num == mStartSide) {
right = (getWidth() - getPaddingRight()) / 2;
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view) - (getWidth() - getPaddingRight()) / 2;
}
} else {
if (num == mStartSide) {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft() + (getWidth() - getPaddingRight()) / 2;
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
if (mLayoutState.mCurrentPosition == state.getItemCount() && lastViewOffset != 0) {
lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin + lastViewOffset);
view.setLayoutParams(lp);
bottom += lastViewOffset;
}
}
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, ![總結.png](https://upload-images.jianshu.io/upload_images/9271486-9440574ea525a11a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
int right, int bottom) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin);
}
複製代碼
給子View
測量完寬高以後,根據奇偶位
和初始設置的一側mStartSide
佈局子View
。若是須要顯示時間軸的結束節點,那麼須要在建立TwoSideLayoutManager
對象的時候設置lastViewOffset
,預留最後位置的空間,不過,須要注意的是,若是設置了時間軸的結束節點,那麼,最後一個子View
最好仍是不要回收,否則,最後一個子View
回收給其餘數據使用的時候還得處理Margin
。只要在回收的時候稍稍處理就好了,具體的代碼再也不貼出了。