前言
抽絲剝繭RecyclerView
系列文章的目的在于幫助Android開發(fā)者提高對RecyclerView的認知,本文是整個系列的第二篇媳友。
LayoutManager
是RecyclerView
中的重要一環(huán)迂曲,使用LayoutManager
就跟玩捏臉蛋的游戲一樣,即使好看的五官(好看的子View)都具備了邓深,也不一定能捏出漂亮的臉蛋粤攒,好在RecyclerView
為我們提供了默認的模板:LinearLayoutManager
、GridLayoutManager
和StaggeredGridLayoutManager
尖坤。
說來慚愧稳懒,如果不是看了GridLayoutManager
的源碼,我還真不知道GridLayoutManager
竟然可以這么使用慢味,圖片來自網(wǎng)絡(luò):
不過呢场梆,今天我們講解的源碼不是來自GridLayoutManager
佛致,而是線性布局LinearLayoutManager
(GridLayoutManager
也是繼承自LinearLayoutManager
),分析完源碼辙谜,我還將給大家?guī)韺崙?zhàn),完成以下的效果:
時間軸的效果來自TimeLine感昼,自己稍微處理了一下装哆,現(xiàn)在開始進入正題。
代碼地址:https://github.com/mCyp/Orient-Ui
目錄
一定嗓、源碼分析
本著認真負責的精神蜕琴,我把RecyclerView
中用到LayoutManager
的地方大致看了一遍,發(fā)現(xiàn)其負責的主要業(yè)務(wù):
- 回收和復(fù)用子View(當然宵溅,這會交給
Recyler
處理)凌简。 - 測量和布局子View。
- 關(guān)于滑動的處理恃逻。
回收和復(fù)用子View
顯然不是LayoutManager
實際完成的雏搂,不過,子View
的新增和刪除都是LayoutManager
通知的寇损,除此以外凸郑,滑動處理的本質(zhì)還是對子View
進行管理,所以矛市,本文要討論的只有測量和布局子View
的芙沥。
測量和布局子View
發(fā)生在RecyclerView
三大工作流程,又...又回到了最初的起點浊吏?這是我們在上篇討論過的而昨,如果不涉及到LayoutManager
的知識,我們將一筆帶過即可找田。
1. 自動測量機制
在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);
// 未復(fù)寫的情況下默認調(diào)用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
長或?qū)捴杏?code>WrapContent)。 -
RecyclerView
設(shè)置的LayoutManger
的isAutoMeasureEnabled
返回為true
底桂。
當設(shè)置自動測量機制的時候植袍,我們的流程如下:
從上圖可以看出,是否使用自動測量機制帶來的差距還是挺明顯的籽懦,使用自動測量機制需要經(jīng)歷那么多流程于个,反正都要使用
LayoutManager#onMeasure
方法,還不如不使用測量機制呢暮顺!
顯然厅篓,這種想法是不對的秀存,因為官方是這么說的,如果不使用自動測量機制羽氮,需要在自定義LayoutManager
過程中復(fù)寫LayoutManager#onMeasure
方法或链,所以呢,這個方法應(yīng)該是包括自動測量機制的全部過程档押,包括:測量父布局
-布置子View
-重新測量子View
-重新測量父布局
澳盐,而使用自動測量機制是不需要復(fù)寫這個方法的令宿,該方法默認測量父布局叼耙。
需要提及的是,我們平時使用的三大LayoutManager
都開啟了自動測量機制粒没。
2. onLayoutChildren
即使RecyclerView
在onMeasure
方法中逃過了布局子View
筛婉,那么在onLayout
中也不可避免,在上一篇博客中癞松,我們了解到RecyclerView
通過LayoutManager#onLayoutChildren
方法實現(xiàn)給子View布局爽撒,我們以LinearLayoutManager
為例,看看其中的奧秘拦惋。
在正式開始之前匆浙,我們先看看LinearLayoutManager
中幾個重要的類:
重要的類 | 解釋 |
---|---|
LinearLayoutManager |
這個大家都懂,線性布局厕妖。 |
AnchorInfo |
繪制子View的時候首尼,記錄其位置、偏移量言秸、方向等基礎(chǔ)信息软能。 |
LayoutChunkResult |
加載子View結(jié)果情況的記錄,比如已經(jīng)填充的子View 的數(shù)量举畸。 |
LayoutState |
當前加載的狀態(tài)記錄查排,比如當前繪制的偏移量,屏幕還剩余多少空間等 |
直接看最重要的LinearLayoutManager#onLayoutChildren
抄沮,代碼被我一刪再刪后如下:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//... 省略的代碼為:數(shù)據(jù)為0的情況下移除所有的子View跋核,將子View加入到緩存
// 第一步:初始化LayoutState 配置LayoutState參數(shù)
ensureLayoutState();
mLayoutState.mRecycle = false;
// ...
// 第二步:尋找焦點子View
final View focused = getFocusedChild();
// ...
// 第三步:移除界面中已經(jīng)存在的子View,并放入緩存
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// 第四步:更新LayoutSatete叛买,填充子View
// 填充也分為兩步:1.從錨點處向結(jié)束方向填充 2.從錨點處向開始方向填充
// fill towards end 往結(jié)束方向填充子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);
// ...
}
}
// ...
// 第五步:整理一些參數(shù)砂代,以及做一下結(jié)束處理
// 不是預(yù)布局的狀態(tài)下結(jié)束給子View布局,否則率挣,重置錨點信息
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
//...
}
整個onLayoutChildren
可以分為如下五個過程:
- 第一步:創(chuàng)建
LayoutState
- 第二步:獲取焦點
子View
- 第三步:移除視圖中已經(jīng)存在的
View
刻伊,回收ViewHolder
- 第四步:填充子
View
- 第五步:填充結(jié)束后的處理
2.1 第一步、第二步
第一步是創(chuàng)建LayoutState
,第二步是獲取屏幕中的焦點子View
捶箱,代碼比較簡單智什,感興趣的同學(xué)們可以自己查詢。
2.2 第三步
在填充子View
前丁屎,如果當前已經(jīng)存在子View
并將繼續(xù)存在的時候荠锭,會先從屏幕中暫時移除,將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
如何保存,我們在上一篇博客中已經(jīng)討論過础爬,這里不再贅述。
2.3 第四步
最復(fù)雜的就是子View
的填充過程吼鳞,回到LinearLayoutManager#onLayoutChildren
方法看蚜,我們假設(shè)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) {
// ...
// 滑動發(fā)生時回收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之后處理狀態(tài)信息
}
// 返回消費的空間
return start - layoutState.mAvailable;
}
最核心的就是while
循環(huán)里面的LinearLayoutManager#layoutChunk
音诫,最后來看一下該方法如何實現(xiàn)的:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 利用緩存策略獲取 與Recycler相關(guān)
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);
// ... 設(shè)置LayoutChunkResult參數(shù)
}
首先,View view = layoutState.next(recycler);
就是我們在上一節(jié)中討論利用緩存Recycler
去獲取ViewHolder
雪位,接著獲取ViewHolder
中綁定的子View
竭钝,給它添加進父布局RecyclerView
,然后給子View測量一下寬高雹洗,最后香罐,有了寬高信息,給它放置到具體的位置就完事了时肿,過程清晰明了庇茫。
回到上個方法LinearLayoutManager#fill
,在While循環(huán)
并且有數(shù)據(jù)的情況下螃成,不斷的將子View
填充至RecyclerView
中旦签,直至該方向填滿。
再回到一開始的LinearLayoutManager#onLayoutChildren
方法寸宏,除了調(diào)用了我們第四步一開始介紹的LinearLayoutManager#updateLayoutStateToFillEnd
宁炫,還調(diào)用了LinearLayoutManager#updateLayoutStateToFillStart
,所以從整體上來看击吱,它是先填充錨點至結(jié)束的方向淋淀,再填充錨點至開始的方向(不絕對),如果用一圖表示,我覺得可以是這樣:
先從錨點向下填充朵纷,再從錨點向上填充炭臭,不過,也有可能是先向上袍辞,再向下鞋仍,由一些參數(shù)決定。
第五步
第五步就是對之前的子View
的填充結(jié)果做一些處理搅吁,不做過多介紹威创。
二、實戰(zhàn)
看了Vivian的TimeLine谎懦,你可能會這么吐槽肚豺,人家的庫借助StaggeredGridLayoutManager
就可以實現(xiàn)時間軸,為何還要多此一舉界拦,使用我的TwoSideLayoutManager
(我給實現(xiàn)的布局方式起名叫TwoSideLayoutManager
)呢吸申?因為使用瀑布流StaggeredGridLayoutManager
想要在時間軸上實現(xiàn)子View平均分布的效果還是比較困難的,但是享甸,使用TwoSideLayoutManager
實現(xiàn)起來就簡單多了截碴。
那么我們?nèi)绾螌崿F(xiàn)RecyclerView
的兩側(cè)布局呢?一張圖來打開思路:
顯然蛉威,
TwoSideLayoutManager
的布局實現(xiàn)可以利用LinearLayoutManager
的實現(xiàn)方式日丹,僅需要修改添加子View
以后的測量邏輯和布局邏輯即可。
上面我們提到過蚯嫌,添加子View
哲虾,給子View
測量,布局都在LinearLayoutManager#layoutChunk
中實現(xiàn)择示,那我們完全可以照搬LinearLayoutManager
的填充邏輯妒牙,稍微改幾處代碼,限于篇幅对妄,我們就看一下核心方法TwoSideLayoutManager#layoutChunk
:
private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
// 沒有更多的數(shù)據(jù)用來生成子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();
}
整體邏輯在注釋中已經(jīng)寫得很清楚了湘今,挨個看一下主要方法。
1. measureChild
測量子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;
// 設(shè)置測量的長度為可用空間的一半
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
一樣剪菱,寬度控制在屏幕可用空間的一半摩瞎。
2. layoutChild
布局子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;
// 根據(jù)位置 奇偶位來進行布局
// 如果起始位置為左側(cè),那么偶數(shù)位為左側(cè)孝常,奇數(shù)位為右側(cè)
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, ![總結(jié).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
測量完寬高之后旗们,根據(jù)奇偶位
和初始設(shè)置的一側(cè)mStartSide
布局子View
。如果需要顯示時間軸的結(jié)束節(jié)點构灸,那么需要在創(chuàng)建TwoSideLayoutManager
對象的時候設(shè)置lastViewOffset
上渴,預(yù)留最后位置的空間,不過,需要注意的是稠氮,如果設(shè)置了時間軸的結(jié)束節(jié)點曹阔,那么,最后一個子View
最好還是不要回收隔披,不然赃份,最后一個子View
回收給其他數(shù)據(jù)使用的時候還得處理Margin
。只要在回收的時候稍稍處理就行了奢米,具體的代碼不再貼出了抓韩。
三、總結(jié)
寫這個布局花的時間還挺多的鬓长,說明自己需要提升的地方還很多谒拴,有的時候代碼雖然能看懂,自己卻不一定能寫出來涉波,下周需要提升效率彪薛,保證每周產(chǎn)出。本人水平有限怠蹂,難免有誤,歡迎指出喲少态。
如果你對本系列文章感興趣: