上一節(jié)RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果也已經(jīng)寫完了拴泌,希望有看到我文章的同學(xué)能有一些收獲熟嫩。layoutManager可以說(shuō)是一個(gè)重中之重欣鳖,代碼量非常多,且涉及到復(fù)用機(jī)制的調(diào)用等等熄驼。等源碼分析過(guò)后蛤售,同學(xué)們應(yīng)該可以通過(guò)自定義LayoutManager打造奇形怪狀的奇葩的UI需求了。
· RecyclerView(1)- Decoration源碼解析
· RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果
· RecyclerView(3)- LayoutMagager源碼解析,LinearLayoutManager
· RecyclerView(4)- 核心喂江、Recycler復(fù)用機(jī)制_1
· RecyclerView(4)- 核心召锈、Recycler復(fù)用機(jī)制_2
· RecyclerView(5)- 自定義LayoutManager(布局、復(fù)用)
· RecyclerView(6)- 自定義ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章开呐、MultiTypeAdapter Github地址
文章視頻地址:鏈接:http://pan.baidu.com/s/1hssvXC4 密碼:18v1
分析LinearLayoutManager烟勋,先羅列幾個(gè)想弄明白的問(wèn)題
· 1、如何擺位置筐付;
· 2卵惦、按需加載布局,位置擺放的規(guī)則
· 3瓦戚、滑動(dòng)時(shí)itemview的位置改變遵循的規(guī)則
LayoutManager如何擺位置沮尿;
先從 測(cè)量 onMeasure
開(kāi)始
RecyclerView.class
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout.mAutoMeasure) { // 自動(dòng)測(cè)量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);// 調(diào)用一次recyclerview的測(cè)量
//....
dispatchLayoutStep2();
}
else{//自定義測(cè)量規(guī)則
}
}
private void dispatchLayoutStep2() {
//....
mState.mItemCount = mAdapter.getItemCount();
//.... 自定義擺放位置
mLayout.onLayoutChildren(mRecycler, mState);
//....
}
可以看到在recyclerview的 onMeasure 調(diào)用了 mLayout的onLayoutChildren
方法 并將Recycler
與 包含了 適配器一些信息的包裝成一個(gè) State
參數(shù) 傳入mLayout的onLayoutChildren
。
接下來(lái)看一下LinearLayoutManager
LinearLayoutManager.class
//初始化
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
setAutoMeasureEnabled(true);
}
public void setAutoMeasureEnabled(boolean enabled) {
mAutoMeasure = enabled;
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
// create layout state
if (DEBUG) {
Log.d(TAG, "is pre layout:" + state.isPreLayout());
}
if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
if (state.getItemCount() == 0) {
// 如果沒(méi)有項(xiàng)目 移除布局
removeAndRecycleAllViews(recycler);
return;
}
}
// ...
this.updateLayoutStateToFillStart(this.mAnchorInfo);
this.mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
//...
this.updateLayoutStateToFillEnd(this.mAnchorInfo);
this.mLayoutState.mExtra = extraForEnd;
this.mLayoutState.mCurrentPosition += this.mLayoutState.mItemDirection;
this.fill(recycler, this.mLayoutState, state, false);
}
// 填充view
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//.. 擺位置
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//... 布局回收
this.recycleByLayoutState(recycler, layoutState);
}
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
//...
addView(view); // -> addView(View child, int index) -> addViewInt(View child, int index, boolean disappearing)
//計(jì)算位置
this.measureChildWithMargins(view, 0, 0);
result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
int left;
int top;
int right;
int bottom;
if(this.mOrientation == 1) {
if(this.isLayoutRTL()) {
right = this.getWidth() - this.getPaddingRight();
left = right - this.mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = this.getPaddingLeft();
right = left + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if(layoutState.mLayoutDirection == -1) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = this.getPaddingTop();
bottom = top + this.mOrientationHelper.getDecoratedMeasurementInOther(view);
if(layoutState.mLayoutDirection == -1) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
//布局
layoutDecoratedWithMargins(view, left, top, right, bottom);
//...
}
private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);//依附在parent 這邊是 parent 是 recyclerView
// 對(duì)holder 做一些變量改變
}
RecyclerView.class
static class LayoutState {
View next(RecyclerView.Recycler recycler) {
//..
//從 recyler 中取出view
final View view = recycler.getViewForPosition(mCurrentPosition);
//...
return view;
}
}
一開(kāi)始的注釋說(shuō)了整個(gè)大概的流程:
1、先檢查children和其它變量畜疾,找到一個(gè)錨點(diǎn)坐標(biāo)與錨點(diǎn)(如果實(shí)在線性布局中赴邻,相當(dāng)于找到當(dāng)前界面內(nèi)第一個(gè)VIew,與第一個(gè)view的坐標(biāo)點(diǎn))
2啡捶、填充從底部開(kāi)始堆疊
3姥敛、從頂部填充到端部
4、計(jì)算是否還有滾動(dòng)瞎暑,添加各種變量彤敛,創(chuàng)建布局。
其實(shí)原理還是挺簡(jiǎn)單的了赌,循環(huán)判斷是否超出邊界墨榄,測(cè)量view,添加view 布局view勿她,判定值改變 在跳到上面循環(huán)袄秩。好多人看不懂是因?yàn)間oogle工程師將這些判斷數(shù)據(jù)抽取封裝了起來(lái),而其中字段非常多逢并,讓人眼花繚亂之剧。
一些總結(jié):
開(kāi)始調(diào)用 onLayoutChildren() 調(diào)用fill() 擺放位置
fill():判斷相應(yīng)規(guī)則 回收一部分view, 調(diào)用layoutChunk() 獲取 view、填充筒狠、擺放view;
layoutChunk(): 獲取view,計(jì)算 view的位置
layoutDecoratedWithMargins():拿到裝飾器設(shè)置的偏移量猪狈,擺放view箱沦;
2辩恼、按需加載布局,位置擺放的規(guī)則
上面也可以知道 在fill()方法內(nèi)我們先去判斷位置擺放的規(guī)則 在決定是否加載下一個(gè)/上一個(gè)View(也就是調(diào)用 layoutChunk),那么我們就來(lái)看一下判斷規(guī)則是怎么樣的吧
while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
int start = layoutState.mAvailable;
if(layoutState.mScrollingOffset != -2147483648) {
if(layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
this.recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LinearLayoutManager.LayoutChunkResult layoutChunkResult = this.mLayoutChunkResult;
layoutChunkResult.resetInternal();
this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
if(layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
if(layoutState.mScrollingOffset != -2147483648) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if(layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
this.recycleByLayoutState(recycler, layoutState);
}
if(stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
可以看到while關(guān)鍵字內(nèi)谓形,有l(wèi)auoutState.mInfinite remainingspace layoutState.haseMore(state)變量灶伊,就是這三個(gè)變量控制著我們的擺放view的數(shù)量。
字面意思可以猜出來(lái):無(wú)窮寒跳、可用空間>0; 是否有更多
那我們來(lái)一個(gè)一個(gè)看一下聘萨,這幾個(gè)變量都經(jīng)歷了什么。
2.1童太、layoutState.mInfinite
字面意思 mInfinite是無(wú)窮的意思...
this.mLayoutState.mInfinite = this.resolveIsInfinite();
boolean resolveIsInfinite() {
return this.mOrientationHelper.getMode() == 0 && this.mOrientationHelper.getEnd() == 0;
}
new OrientationHelper(layoutManager) {
public int getEnd() {
return this.mLayoutManager.getHeight();
}
public int getMode() {
return this.mLayoutManager.getHeightMode();
}
}
可以看到 mInfinite 的值與 recyclerview的高度 與規(guī)格有關(guān)米辐,判斷==0 ? 這是什么鬼书释! 看起來(lái)一點(diǎn)卵用都沒(méi)有 ?_?(是在下輸了)
2.1翘贮、remainingspace
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
第一次 layoutState.mAvailable
的取值
private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
}
private boolean updateAnchorFromPendingData(State state, LinearLayoutManager.AnchorInfo anchorInfo) {
//.....
if(anchorInfo.mLayoutFromEnd) {
anchorInfo.mCoordinate = this.mOrientationHelper.getEndAfterPadding() - this.mPendingSavedState.mAnchorOffset;
} else {
anchorInfo.mCoordinate = this.mOrientationHelper.getStartAfterPadding() + this.mPendingSavedState.mAnchorOffset;
}
//.........
}
private void updateLayoutStateToFillStart(LinearLayoutManager.AnchorInfo anchorInfo) {
this.updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillStart(int itemPosition, int offset) {
this.mLayoutState.mAvailable = offset - this.mOrientationHelper.getStartAfterPadding();
}
可以看到 第一次 layoutState.mAvailable的值是通過(guò) 錨點(diǎn) AnchorInfo來(lái)計(jì)算的。
其中還判斷了mLayoutFromEnd ...
看到這里我是暈的爆惧,變量太多了狸页。。扯再。
第一次 layoutState.mExtra
的值
extra意思是額外...
2.3芍耘、layoutstate.hasMore(state)
boolean hasMore(State state) {
return this.mCurrentPosition >= 0 && this.mCurrentPosition < state.getItemCount();
}
就是判斷 position址遇;
2.4、擺完一個(gè)布局以后斋竞,干了些什么
布局是一個(gè)一個(gè)添加的倔约,那么在加載完一個(gè)children view后在加載了什么呢?
int fill(Recycler recycler, LinearLayoutManager.LayoutState layoutState, State state, boolean stopOnFocusable) {
while((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
this.layoutChunk(recycler, state, layoutState, layoutChunkResult);
if(layoutChunkResult.mFinished) {
break;
}
// 加上偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if(!layoutChunkResult.mIgnoreConsumed || this.mLayoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 減去剩余空間
remainingSpace -= layoutChunkResult.mConsumed;
}
if(layoutState.mScrollingOffset != -2147483648) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if(layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
this.recycleByLayoutState(recycler, layoutState);
}
if(stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
//....
}
}
void layoutChunk(Recycler recycler, State state, LinearLayoutManager.LayoutState layoutState, LinearLayoutManager.LayoutChunkResult result){
//......
//得到 消耗的高度/寬度
result.mConsumed = this.mOrientationHelper.getDecoratedMeasurement(view);
// 布局
addView(view);
layout
// 判斷是否被隱藏忽略
if(params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
// 焦點(diǎn)
result.mFocusable = view.isFocusable();
}
mOrientationHelper.calss
public int getDecoratedMeasurement(View view) {
LayoutParams params = (LayoutParams)view.getLayoutParams();
return this.mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
}
可以看到 layoutChunk()中賦值layoutChunkResult坝初,在之后偏移量添加layoutState.mOffset+=...跺株,剩余空間減去了view消耗的數(shù)量 remainingSpace-= layoutChunkResult.mConsumed。
還是回到了剛開(kāi)始的注釋: 先找到錨點(diǎn)脖卖,計(jì)算第一個(gè)坐標(biāo)乒省, 在循環(huán)添加view、計(jì)算偏移量確定擺放位置畦木。
2.5 布局的一些總結(jié)
來(lái)一點(diǎn)總結(jié)吧:
涉及到的主要類
1袖扛、 LinearLayoutManager.LayoutState:布局狀態(tài)類(有布局方向(向上向下)、結(jié)束布局十籍、偏移量...)
2蛆封、Recycler 布局支持類、可回收view勾栗、創(chuàng)建view...
3惨篱、AnchorInfo 錨點(diǎn)信息類布局流程:
1、以開(kāi)始方向先更新錨點(diǎn)信息
2围俘、在通過(guò)錨點(diǎn)信息 賦值layoutState 得到可用空間砸讳,偏移量等信息, while(剩余空間) 擺放childrenView 改變偏移量;
3、以底部更新錨點(diǎn)信息
4界牡、重復(fù)第2步
5簿寂、結(jié)束
原理其實(shí)是很簡(jiǎn)單的,只不過(guò)其中變量太多了宿亡,代碼也是很多所以看得云里霧里的常遂,有時(shí)候還會(huì)跑偏找不到南北。
畫(huà)一個(gè)流程圖吧挽荠。
其中填充規(guī)則 fill()方法流程如下
3克胳、 滑動(dòng)時(shí)干了哪些事情
第一次布局看起來(lái)比較簡(jiǎn)單,我們就可以去猜測(cè) 滑動(dòng)時(shí)應(yīng)該加載布局也是一樣的圈匆。 去看看源碼吧漠另。
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
return this.mOrientation == 1?0:this.scrollBy(dx, recycler, state);
}
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
return this.mOrientation == 0?0:this.scrollBy(dy, recycler, state);
}
有兩個(gè), 分析scrollVerticallyBy
臭脓。
int scrollBy(int dy, Recycler recycler, State state) {
if(this.getChildCount() != 0 && dy != 0) {
this.mLayoutState.mRecycle = true;
this.ensureLayoutState();
int layoutDirection = dy > 0?1:-1;
int absDy = Math.abs(dy);
this.updateLayoutState(layoutDirection, absDy, true, state);
// 滑動(dòng)且 返回消耗的距離
int consumed = this.mLayoutState.mScrollingOffset + this.fill(recycler, this.mLayoutState, state, false);
if(consumed < 0) {
return 0;
} else {
// 計(jì)算滑動(dòng)的量
int scrolled = absDy > consumed?layoutDirection * consumed:dy;
this.mOrientationHelper.offsetChildren(-scrolled);
this.mLayoutState.mLastScrollDelta = scrolled;
return scrolled;
}
} else {
return 0;
}
}
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
this.mLayoutState.mInfinite = this.resolveIsInfinite();
this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
this.mLayoutState.mLayoutDirection = layoutDirection;
int scrollingOffset;
View child;
if(layoutDirection == 1) {
this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
child = this.getChildClosestToEnd();
this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
} else {
child = this.getChildClosestToStart();
this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
}
// 可用距離 偏移距離
this.mLayoutState.mAvailable = requiredSpace;
if(canUseExistingSpace) {
this.mLayoutState.mAvailable -= scrollingOffset;
}
this.mLayoutState.mScrollingOffset = scrollingOffset;
}
scrollVerticallyBy
的作用是 酗钞,改變layoutState,調(diào)用fill,布局view砚作,返回實(shí)際消耗的距離窘奏。
fill的流程我們?cè)谏厦嬉惨呀?jīng)做了說(shuō)明了。
補(bǔ)充一點(diǎn)的是葫录,當(dāng)我們滑動(dòng)的距離在一定范圍內(nèi)(沒(méi)有超出頁(yè)面上 第一個(gè)或最后一個(gè)child在屏幕上的范圍)是不會(huì)重新
4着裹、 錨點(diǎn)的位置是如何確定的
從以上流程中我們了解到了layoutManager的一些布局的規(guī)則,我們的布局都是通過(guò)一個(gè)基準(zhǔn)點(diǎn)來(lái)進(jìn)行上下布局米同,那么最重要的肯定就是這個(gè)基準(zhǔn)點(diǎn)即錨點(diǎn)的位置骇扇。
我們知道錨點(diǎn)的信息 保存在 LinearLayoutManager.AnchorInfo
中,進(jìn)而layouState控制布局
1面粮、第一次錨點(diǎn)得位置
void onLayoutChildren(Recycler recycler, State state) {
}
private void updateAnchorInfoForLayout(Recycler recycler, State state, LinearLayoutManager.AnchorInfo anchorInfo) {
if(!this.updateAnchorFromPendingData(state, anchorInfo)) {
if(!this.updateAnchorFromChildren(recycler, state, anchorInfo)) {
//分配坐標(biāo)
anchorInfo.assignCoordinateFromPadding();
//得到數(shù)據(jù)集位置
anchorInfo.mPosition = this.mStackFromEnd?state.getItemCount() - 1:0;
}
}
}
class AnchorInfo {
void assignCoordinateFromPadding() {
this.mCoordinate = this.mLayoutFromEnd?LinearLayoutManager.this.mOrientationHelper.getEndAfterPadding():LinearLayoutManager.this.mOrientationHelper.getStartAfterPadding();
}
}
第一次特別簡(jiǎn)單少孝,就判斷布局方向獲取mCoordinate 開(kāi)始或結(jié)束的padding與itemView的數(shù)據(jù)集位置position。
2熬苍、 滑動(dòng)位置的確定
int scrollBy(int dy, Recycler recycler, State state) {
this.updateLayoutState(layoutDirection, absDy, true, state);
}
private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, State state) {
this.mLayoutState.mInfinite = this.resolveIsInfinite();
this.mLayoutState.mExtra = this.getExtraLayoutSpace(state);
this.mLayoutState.mLayoutDirection = layoutDirection;
int scrollingOffset;
View child;
if(layoutDirection == 1) {
this.mLayoutState.mExtra += this.mOrientationHelper.getEndPadding();
child = this.getChildClosestToEnd();
this.mLayoutState.mItemDirection = this.mShouldReverseLayout?-1:1;
this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedEnd(child);
scrollingOffset = this.mOrientationHelper.getDecoratedEnd(child) - this.mOrientationHelper.getEndAfterPadding();
} else {
child = this.getChildClosestToStart();
this.mLayoutState.mExtra += this.mOrientationHelper.getStartAfterPadding();
this.mLayoutState.mItemDirection = this.mShouldReverseLayout?1:-1;
this.mLayoutState.mCurrentPosition = this.getPosition(child) + this.mLayoutState.mItemDirection;
this.mLayoutState.mOffset = this.mOrientationHelper.getDecoratedStart(child);
scrollingOffset = -this.mOrientationHelper.getDecoratedStart(child) + this.mOrientationHelper.getStartAfterPadding();
}
this.mLayoutState.mAvailable = requiredSpace;
if(canUseExistingSpace) {
this.mLayoutState.mAvailable -= scrollingOffset;
}
this.mLayoutState.mScrollingOffset = scrollingOffset;
}
給張圖吧
文章視頻地址:鏈接:http://pan.baidu.com/s/1o7Ai48E 密碼:98pc
· RecyclerView(1)- Decoration源碼解析
· RecyclerView(2)- 自定義Decoration打造時(shí)光軸效果
· RecyclerView(3)- LayoutMagager源碼解析,LinearLayoutManager
· RecyclerView(4)- 核心稍走、Recycler復(fù)用機(jī)制_1
· RecyclerView(4)- 核心、Recycler復(fù)用機(jī)制_2
· RecyclerView(5)- 自定義LayoutManager(布局柴底、復(fù)用)
· RecyclerView(6)- 自定義ItemAnimator
· RecyclerView(7)- ItemTouchHelper
· RecyclerView(8)- MultiTypeAdapter文章婿脸、MultiTypeAdapter Github地址
文章視頻地址:鏈接:http://pan.baidu.com/s/1hssvXC4 密碼:18v1
希望我的文章不會(huì)誤導(dǎo)在觀看的你,如果有異議的地方歡迎討論和指正柄驻。
如果能給觀看的你帶來(lái)收獲狐树,那就是最好不過(guò)了。