不喜勿噴,有錯請留言(以下源碼均來自recyclerView-27.1.1版本)
對於RecyclerView這個ViewGroup而言,我們已經初步瞭解了ViewGroup的各個基本方法,下面我們進入layoutManager這個必需的控件中,藉助LinearLayoutManager這個實例重點分析LayoutManger,爲避免篇幅過長,而且layout與scroll相對獨立,故將拆成兩篇,一篇layout篇和一篇scroll篇
LayoutManager組成
LayoutManager位於RecyclerView中,是一個靜態抽象類方法,在其中有兩個靜態內部類,分別是:
和
由Properties我們一眼可瞭解對於LayoutManger而言,其提供的自定義樣式有對應的四種,而LayoutPrefetchRegistry而言,其作用是我們layoutManager的預取策略制定,對於它們倆我們不着重分析,我麼進入LinearLayoutManager中進行分析
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
ItemTouchHelper.ViewDropHandler,RecyclerView.SmoothScroller.ScrollVectorProvider
有經驗的童鞋發現的在繼承RecyclerView.LayoutManager自定義LayoutManager時候,必須要實現一個叫generateDefaultLayoutParams的方法
public class MyLinearLayoutManager extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
由於LayoutManager抽象了generateDefaultLayoutParams方法,子類必須實現該方法
在LinearLayoutManager中其提供還是RecyclerView的LayoutParams,我們照樣返回,
可能有人對generateDefaultLayoutParams有些疑問感覺沒啥卵用,這裏只做簡單介紹,有興趣的童鞋可自行深入瞭解,對於generateDefaultLayoutParams的意義是如果childView的佈局參數爲null就是調用這個方法生成一個默認的佈局參數
從上一文的ViewGroup篇,我們知道RecyclerView通過LayoutManager的onLayoutChildren,對子view進行擺放,在我們分析onLayoutChildren之前,LinearLayoutManager中有幾個狀態記錄類
LayoutState: 一個佈局狀態幫助類
SavedState:保存一些滾動或者掛起的信息
AnchorInfo:重新佈局時保留定位信息
那接着我們通過分析onLayoutChildren這個方法來完善我們自己的LayoutManager
在我們分析前我們想象下,如果是我們自己實現LayoutManager的時候該怎麼實現
- onMeasure測量子view,測量自身
- 獲取padding等獲取有效擺放範圍
- 滿足一行時或擺不下,自動換行
注意對於LayoutManager而言不僅僅這點功能,其還有reverse和橫豎向變化,那我們考慮橫豎向變化,我們的LayoutManager又該如何實現呢,我們先簡單介紹下android的LinearLayoutManager是幾個概念:
LayoutManager應對layout佈局變化時,引入了一個anchor這個佈局錨點信息的概念(也就是我們之前說的AnchorInfo)和一個layoutfromEnd佈局方向的概念,用於根據佈局的錨點信息,按照layoutfromEnd的佈局方向去填充itemview
接下來我們看看LinearLayoutManager是onLayoutChildren實現的
onLayoutChildren:官方的對layout algorithm描述如下:
- 找到定位點座標和定位點項目位置
首先在ensureLayoutState中創建一個LayoutState用於後續記錄使用
然後resolveShouldLayoutReverse中設置佈局方向
若錨點信息已過期或者滾動位置不是初始位置,或者預存儲狀態不爲null,則重置錨點
注意此處 mShouldReverseLayout ^ mStackFromEnd
mStackFromEnd若未有人爲操作一直爲false,此處位運算,mLayoutFromEnd根據mShouldReverseLayout值走向,即VERTICAL方向爲false,HORIZONAL方向爲true
最後計算填充的錨點位置和偏移量,保存繪製錨點信息即(updateAnchorInfoForLayout)
對於updateAnchorInfoForLayout內
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
//存在掛起的滾動位置或保存的狀態,則從中更新定位信息並返回true 否則返回false
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}
//定位從存在的view中找出一個作爲錨點的view,這個view要麼是最靠近開始的位置要麼在最後,如果一個子view有焦點,則優先給它
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
//根據RecyclerView的填充指定定位的座標
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
注意最後一句,anchorInfo它的錨點位置 如果mStackFromEnd爲true時,其爲條目總個數,若爲false,則爲-1。
-
錨點維護(updateAnchorFromPendingData和updateAnchorFromChildren是對滾動view的選取即是對錨點的處理)
可能有朋友還是對錨點不太能理解,我就從使用方來解釋錨點,首先大家都在使用LinearlayoutManager的時候使用過setStackFromEnd這個方法來改變stackFromEn這個值,在updateAnchorInfoForLayout的具體呈現就是
這兩種情況的填充是相反的,即一個是自上而下,一個是自下而上,子控件在y軸上起始繪製偏移量,就會不同。
對於updateLayoutStateToFillEnd和updateLayoutStateToFillStart它們功能很相似分別如下
updateStateToFillEnd:
private void updateLayoutStateToFillEnd(int itemPosition, int offset) { mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; mLayoutState.mItemDirection = mShouldReverseLayout ? MyLayoutState.ITEM_DIRECTION_HEAD : MyLayoutState.ITEM_DIRECTION_TAIL; mLayoutState.mCurrentPosition = itemPosition; mLayoutState.mLayoutDirection = MyLayoutState.LAYOUT_END; mLayoutState.mOffset = offset; mLayoutState.mScrollingOffset = MyLayoutState.SCROLLING_OFFSET_NaN; } 注意這裏第一步是通過OrientationHelper的getEndAfterPadding獲取到佈局的結束爲止減去偏移量 layout的方向時LAOYOUT_END也就是說自上而下的layout
而updateStateToFillStart
private void updateLayoutStateToFillStart(int itemPosition, int offset) { mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); mLayoutState.mCurrentPosition = itemPosition; mLayoutState.mItemDirection = mShouldReverseLayout ? MyLayoutState.ITEM_DIRECTION_TAIL : MyLayoutState.ITEM_DIRECTION_HEAD; mLayoutState.mLayoutDirection = MyLayoutState.LAYOUT_START; mLayoutState.mOffset = offset; mLayoutState.mScrollingOffset = MyLayoutState.SCROLLING_OFFSET_NaN; } 而這裏的第一步是偏移量減去通過OrientationHelper的getStartAfterPadding獲取佈局的起始位置 layout的方向時LAYOUT_START也就是說自下而上的layout
從這我們已經能理解錨點的存在了吧
-
接下來是根據不同的layout方向進行layout操作了,當mStackFromEnd爲true時
-
當mStackFromEnd爲false時
首先我們先從錨點信息出對我們layout的佈局範圍以及方向做確定,之前已經介紹過的updateLayoutStateTo的方法,可能有人對此處有疑問,爲什麼此處即有toStart也有toEnd兩種操作,因爲在我們真正使用過程中,我們的layout的方向大致是這樣的(這裏借用網上的例子來說明下)
其實我們後面分析其scroll的時候就會解釋到,當我們RecyclerView處理時邊滾動邊添加,之前添加過的也不是恆久不變的會出現回收等,需要重新創建layout,那麼就會出現兩個方向,所以不僅會有toStart也會有toEnd兩種操作
我們進入layout最重要的fill方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//此處是用於標記Recyclerview進行 緩存回收view 操作
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
...省略LayoutChunk的循環填充操作 後面單獨拎出來分析
//填充完修改起始位置
return start - layoutState.mAvailable;
}
下面是fill方法的核心方法了也就是上面省略的代碼部分
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//先重置layoutChunkResult狀態
layoutChunkResult.resetInternal();
//填充空間
layoutChunk(recycler, state, layoutState, layoutChunkResult);
//...省略部分剩餘空間等代碼
}
我們關注填充空間的layoutChunk方法
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//先嚐試獲取下一個view(mScrapList和mRecyclerPool兩個緩存中優先獲取),獲取不到就創建一個
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
//獲取layoutParams 根據情況addView
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);
}
}
//重新測量包含margin值
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//計算子view的left,top,right,bottom座標
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
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;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// 確定位置後就進行layout
layoutDecoratedWithMargins(view, left, top, right, bottom);
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
注意,layoutDecoratedWithMargins這裏其實是在RecyclerView中實現的
它將計算出來的左上右下去具體的layout操作
總的來說layoutChunk就是對我們的提供的ItemView進行創建,填充,測量和佈局。
- 先通過LayoutState的next方法從RecylerView的一二級緩存中取itemView沒有得話就創建一個(也就是我們的onCreateView)
- 然後將這個view add進我們的viewGroup中
- 然後對view進行測量跟擺放
至此我們的layout篇就到這結束了,可能有人問不是還有layoutForPredictiveAnimations的方法沒有分析麼,其實layoutForPredictiveAnimations雖然屬於layout部分,但是其和滾動的時機密切相關,所以我把這塊放在scroll篇去分析