RecyclerView處發佈以來,就受到開發者的青睞,它良好的功能解耦,使咱們在定製它的功能方面變得遊刃有餘。自從在項目中使用這個控件以來,我對它是不勝歡喜,以致於我想用一系列的文章來剖析它。本文就從最基本的顯示入手來分析,爲後面的分析打下堅實的基礎。java
本文先分析RecyclerView
從建立到顯示的過程,這將爲後面的系列文章打下堅實的基礎,咱們先看下它的基本使用函數
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());
複製代碼
LayoutManager
和Adapter
是RecyclerView
必不可少的部分,本文就來分析這段代碼。源碼分析
爲了方便,在後面的分析中,我將使用RV表示
RecyclerView
,用LM表示LayoutManager
,用LLM表示LinearLayoutManager
。佈局
View的構造函數一般就是用來解析屬性和初始化變量,RV的構造函數也不例外,而與本文相關代碼以下this
public RecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
// ...
mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
// ...
});
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
// ...
});
// ...
}
複製代碼
從全名就大體能夠猜想出這兩個類的做用。AdapterHelper
是Adapter
的輔助類,用來處理Adapter
的更新操做。ChildHelper
是RV的輔助類,用來管理它的子View。spa
public void setLayoutManager(@Nullable LayoutManager layout) {
// ...
// 保存LM
mLayout = layout;
if (layout != null) {
// LM保存RV引用
mLayout.setRecyclerView(this);
// 若是RV添加到Window中,那麼就通知LM
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
// ...
// 請求從新佈局
requestLayout();
}
複製代碼
setLayoutManager()
方法最主要的操做就是RV和LM互相保存引用,因爲RV的LM改變了,所以須要從新請求佈局。code
setAdapter()
方法是由setAdapterInternal()
實現server
private void setAdapterInternal(@Nullable Adapter adapter, boolean compatibleWithPrevious, boolean removeAndRecycleViews) {
// ...
// RV保存Adapter引用
mAdapter = adapter;
if (adapter != null) {
// 給新Adapter註冊監聽者
adapter.registerAdapterDataObserver(mObserver);
// 通知新Adapter已經添加到RV中
adapter.onAttachedToRecyclerView(this);
}
// 若是LM存在,就通知LM,Adapter改變了
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
// 通知RV,Adapter改變了
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
// 表示Adapter改變了
mState.mStructureChanged = true;
}
複製代碼
RV保存Adapter
引用並給新Adapter註冊監聽者,而後通知每個關心Adapter
的監聽者,例如RV, LM。繼承
當一切準備就緒後,如今來分析測量的部分開發
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
// 若是LM使用自動測量機制
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 爲了兼容處理,實際調用了RV的defaultOnMeasure()方法測量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// 若是寬和高的測量模式都是EXACTLY,那麼就使用默認測量值,並直接返回
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// ... 省略剩餘的測量代碼
} else {
// ...
}
}
複製代碼
首先根據LM是否支持RV的自動測量機制來決定測量邏輯,LLM是支持自動測量機制的,所以只分析這種狀況的測量。
究竟什麼是自動測量機制,你們能夠仔細研讀源碼的註釋以及測量邏輯,我這裏只作簡單分析。
使用自動測量機制,首先會調用LM的onMeasure()
進行測量。這裏你可能會有疑問,既然叫作自動測量機制,爲什麼還會用LM來測量呢。其實這是爲了兼容處理,它實際是調用了RV的defaultOnMeasure()
方法
void defaultOnMeasure(int widthSpec, int heightSpec) {
final int width = LayoutManager.chooseSize(widthSpec,
getPaddingLeft() + getPaddingRight(),
ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,
getPaddingTop() + getPaddingBottom(),
ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
複製代碼
咱們能夠發現,RV做爲一個ViewGroup,這裏竟然沒有考慮子View就保存了測量的結果。很顯然,這是一個粗糙的測量。
可是這個粗糙的測量實際上是爲了知足一種特殊狀況,那就是父View給出的寬高限制模式都是MeasureSpec.EXACTLY
。從代碼中能夠發現,在經歷這一步粗糙測量後,就處理了這種特殊狀況。
爲了簡化分析,目前只考慮這種特殊狀況。 被省略的代碼其實就是考慮子View測量的代碼,而這段代碼在onLayout()
中也有,由於放到後面講解。
onLayout
是由dispatchLayout()
實現的
void dispatchLayout() {
// ...
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
// RV已經測量完畢,所以LM保存RV的測量結果
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// ...
} else {
// ...
}
dispatchLayoutStep3();
}
複製代碼
佈局的過程,不管如何,都是通過dispatchLayoutStep1()
,dispatchLayoutStep2()
,dispatchLayoutStep3()
完成。而與本文相關的只有dispatchLayoutStep2()
,它是完成子View的實際佈局操做,它是由LM的onLayoutChildren()
實現。
從ww前面的分析可知,RV對子View的佈局是交給LM來處理的。例子中使用的是LLM,所以這裏分析它的onLayoutChildren()
方法。因爲這個方法代碼量比較大,所以將分步解析。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 確保mLayoutState被建立
ensureLayoutState();
mLayoutState.mRecycle = false;
// 解析是否使用反向佈局
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
複製代碼
首先確保了LayoutState mLayoutState
的建立,LayoutState
用來保存佈局的狀態。
而後解析是否使用反向佈局,例子中的LLM使用的是垂直佈局,而且佈局使用默認的不支持RTL
,所以mShouldReverseLayout
值爲false
,表示不是反向佈局。
你們須要知道LLM的返回佈局的狀況。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新錨點信息
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
// 錨點信息保存是不是反向佈局
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 計算錨點的位置和座標
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
// 表示錨點信息有效
mAnchorInfo.mValid = true;
}
// ...
final int firstLayoutDirection;
if (mAnchorInfo.mLayoutFromEnd) {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
: LayoutState.ITEM_DIRECTION_HEAD;
} else {
firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
: LayoutState.ITEM_DIRECTION_TAIL;
}
// 通知錨點信息準備就緒
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
}
複製代碼
AnchorInfo mAnchorInfo
是用來保存錨點信息,錨點位置和座標來表示佈局從哪裏開始,這個將會在後面看到。
錨點信息保存了是否反向佈局的信息,這裏又冒出來一個mStackFromEnd
,這個是爲了兼容支持AbsListView#setStackFromBottom(boolean)
特性,說白了就是爲了提供給開發者一致的操做方法。我的以爲這真是一個垃圾操做。
以後用updateAnchorInfoForLayout()
方法計算出了錨點的位置和座標
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
// ...
// 根據padding值決定錨點座標
anchorInfo.assignCoordinateFromPadding();
// 若是不是反向佈局,錨點位置爲0
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
// AnchorInfo#assignCoordinateFromPadding()
void assignCoordinateFromPadding() {
// 若是不是反向佈局,座標就是RV的paddingTop值
mCoordinate = mLayoutFromEnd
? mOrientationHelper.getEndAfterPadding()
: mOrientationHelper.getStartAfterPadding();
}
複製代碼
根據例子中的狀況,錨點座標是RV的paddingTop
,位置是0。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新錨點信息
// 3. 計算佈局的額外空間
// 保存佈局方向,無滾動的狀況下,值爲LayoutState.LAYOUT_END
mLayoutState.mLayoutDirection = mLayoutState.mLastScrollDelta >= 0
? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 計算佈局須要的額外空間,結果保存到mReusableIntPair
calculateExtraLayoutSpace(state, mReusableIntPair);
// 額外究竟還要考慮padding
int extraForStart = Math.max(0, mReusableIntPair[0])
+ mOrientationHelper.getStartAfterPadding();
int extraForEnd = Math.max(0, mReusableIntPair[1])
+ mOrientationHelper.getEndPadding();
if (state.isPreLayout() && mPendingScrollPosition != RecyclerView.NO_POSITION
&& mPendingScrollPositionOffset != INVALID_OFFSET) {
// ...
}
}
複製代碼
在RV滑動的時候,calculateExtraLayoutSpace()
會分配一個頁面的額外空間,其它的狀況下是不會分配額外空間的。
對於例子中的狀況,calculateExtraLayoutSpace()
分配的額外空間就是0。可是對於佈局,額外空間還須要考慮RV的padding值。
若是自定義一個繼承自LLM的LM,能夠複寫
calculateExtraLayoutSpace()
定義額外空間的分配策略。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// 1. 初始化信息
// 2. 更新錨點信息
// 3. 計算佈局的額外空間
// 4. 爲子View佈局
// 首先分離而且回收子View
detachAndScrapAttachedViews(recycler);
// RV的高度爲0,而且模式爲UNSPECIFIED
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
mLayoutState.mNoRecycleSpace = 0;
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// 從錨點位置向後填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// 從錨點位置向前填充
// ...
// 若是還有額外空間,就向後填充更多的子View
if (mLayoutState.mAvailable > 0) {
// ...
}
}
複製代碼
爲子View佈局以前,首先從RV分離子View,並回收。而後,經過fill()
分別從錨點位置,向後以及向前填充子View,最後若是還有剩餘空間,就嘗試嘗試繼續向後填充子View(若是還有子View的話)。
根據例子計算出來的錨點位置是0,座標是paddongTop
,所以這裏只分析從錨點位置向後填充的過程。
首先調用updateLayoutStateToFillEnd()
方法,根據錨點信息來更新mLayoutState
private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
// 參數傳入的是錨點的位置和座標
updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}
private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
// 可用空間就是去掉padding後的可用大小
mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
// 表示Adapter的數據遍歷的方向
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;
}
複製代碼
更新完mLayoutState
信息後,就調用fill()
填充子View
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
// ...
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
// 有能夠空間,而且還有子View沒有填充
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 這裏表明全部子View已經layout完畢
if (layoutChunkResult.mFinished) {
break;
}
// 更新佈局偏移量
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
// 從新計算可用空間
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
// ...
}
// 返回這次佈局使用了多少空間
return start - layoutState.mAvailable;
}
複製代碼
根據例子來分析,只有還存在可用空間,而且還有子View沒有填充,那麼就會一直調用layoutChunk()
方法進行填充子View,直到可用空間消耗完,或者沒有了子View。
layoutChunk()
是LLM爲子View佈局的核心方法,咱們須要重點關注這個方法的實現。因爲這個方法也比較長,所以我也打算分段講解
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. 獲取一個子View,並更新mLayoutState.mCurrentPosition
View view = layoutState.next(recycler);
}
複製代碼
根據例子的狀況,這裏是從RecyclerView.Recycler
中建立一個子View,下面的代碼爲建立新View的代碼
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
// ...
if (holder == null) {
if (holder == null) {
// 1. 回調Adapter.onCreateViewHoler()建立ViewHolder,並設置ViewHolder類型
holder = mAdapter.createViewHolder(RecyclerView.this, type);
// ...
}
}
// ...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// ...
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 2. 回調Adapter.bindViewHolder()綁定ViewHolder,並更新ViewHolder的一些信息
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
// 3. 確保建立View的佈局參數的正確性並更新信息
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
// 佈局參數保存ViewHolder
rvLayoutParams.mViewHolder = holder;
// 若是View不是新建立,而且已經綁定,那麼mPendingInvalidate爲true,表示須要刷新
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}
複製代碼
從Adapter
中建立ViewHolder
,就是利用Adapter
的幾個回調,而後更新ViewHolder
的相關信息,最後更新建立View的佈局參數。
你們須要知道基本的
Adapter
是如何寫。
獲取子View後,就須要把這個子View添加到RV中
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. 獲取子View
// 2. 把子View添加到RV中
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// 把獲取的子View添加到RV末尾
addView(view);
} else {
}
}
}
複製代碼
addView()
方法是利用基類LM的addViewInt()
實現,而最終是經過mChildHelper.addView()
實現。
// ChildHelper#addView()
void addView(View child, int index, boolean hidden) {
final int offset;
if (index < 0) {
// index爲-1,就獲取RV已經有了多少個子View
offset = mCallback.getChildCount();
} else {
offset = getOffset(index);
}
// ...
// 根據offset,把子View添加到RV中
mCallback.addView(child, offset);
}
複製代碼
因爲index
值爲-1,所以是把這個View添加到RV的子View末尾。
把子View添加到RV後,就須要測量這個子View
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. 獲取子View
// 2. 把子View添加到RV中
// 3. 測量子View
measureChildWithMargins(view, 0, 0);
// 保存LLM相應方向上消耗的大小
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
}
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 獲取ItemDecoration的Rect
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
// 已經使用的寬和高要把ItemDecoration的Rect計算在內
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
// 這是一個考慮了padding, margin, ItemDecoration Rect的測量
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
複製代碼
對子View的測量,考慮了padding
, margin
, 以及ItemDecoration
的Rect
。 具體如何測量就不在本文的分析範圍內。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) {
// 1. 獲取子View
// 2. 把子View添加到RV中
// 3. 測量子View
// 4. 子View佈局
// 計算佈局須要的座標值
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
// ...
}
// 進行佈局
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
複製代碼
這個佈局過程很簡單,這裏是一筆代過了。
你們必定要了解自定義View如何測量和佈局,這是你分析本文的基礎。
測量和佈局過程都已經分析完畢,剩下的就是繪製過程
public void draw(Canvas c) {
// 調用onDraw()方法
super.draw(c);
// 用ItemDecoration.onDrawOver()進行繪製
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// 繪製邊界效果
// ...
}
public void onDraw(Canvas c) {
super.onDraw(c);
// 用ItemDecoration.onDraw()進行繪製
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
複製代碼
對於RV來講,繪製過程主要就是繪製ItemDecoration
,首先是用ItemDecoration.onDraw()
方法進行繪製,而後再用ItemDecoration.onDrawOver()
進行繪製。
本系列的文章中,我會用單獨一篇文章講解
ItemDecoration
的原理,以及如何使用。
RV的源碼分析真不能一蹴而就,須要極大的耐心和毅力。本文以極簡的方式(文章仍然很長)分析了RV從建立到顯示界面的過程,初始窺探了RV的原理,可是這還遠遠不能知足個人好奇心,我將以這篇文章爲基石繼續前行。