Android RecyclerView工作原理分析(上)
基本使用
RecyclerView的基本使用並不複雜,只需要提供一個RecyclerView.Apdater的實現用於處理資料集與ItemView的繫結關係,和一個RecyclerView.LayoutManager的實現用於 測量並佈局 ItemView。
繪製流程
眾所周知,android控制元件的繪製可以分為3個步驟:measure、layout、draw。RecyclerView的繪製自然也經這3個步驟。但是,RecyclerView將它的measure與layout過程委託給了RecyclerView.LayoutManager來處理,並且,它對子控制元件的measure及layout過程是逐個處理的,也就是說,執行完成一個子控制元件的measure及layout過程再去執行下一個。下面看下這段程式碼:
protected void onMeasure(int widthSpec, int heightSpec) {
...
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
...
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
...
} else {
...
}
}
這是RecyclerView的測量方法,再看下dispatchLayoutStep2()方法:
private void dispatchLayoutStep2() {
...
mLayout.onLayoutChildren(mRecycler, mState);
...
}
上面的mLayout就是一個RecyclerView.LayoutManager例項。通過以上程式碼(和方法名稱),不難推斷出,RecyclerView的measure及layout過程委託給了RecyclerView.LayoutManager。接著看onLayoutChildren方法,在相容包中提供了3個RecyclerView.LayoutManager的實現,這裡我就只以LinearLayoutManager來舉例說明:
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.
...
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
...
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
...
}
...
}
原始碼中的註釋部分我並沒有略去,它已經解釋了此處的邏輯了。這裡我以垂直佈局來說明,mAnchorInfo為佈局錨點資訊,包含了子控制元件在Y軸上起始繪製偏移量(coordinate),ItemView在Adapter中的索引位置(position)和佈局方向(mLayoutFromEnd)——這裡是指start、end方向。這部分程式碼的功能就是:確定佈局錨點,以此為起點向開始和結束方向填充ItemView,如圖所示:
在上一段程式碼中,fill()方法的作用就是填充ItemView,而圖(3)說明了,在上段程式碼中fill()方法呼叫2次的原因。雖然圖(3)是更為普遍的情況,而且在實現填充ItemView演算法時,也是按圖(3)所示來實現的,但是mAnchorInfo在賦值過程(updateAnchorInfoForLayout)中,只會出現圖(1)、圖(2)所示情況。現在來看下fill()方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
while (...&&layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
if (...) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
remainingSpace -= layoutChunkResult.mConsumed;
}
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
...
}
下面是layoutChunk()方法:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
...
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
...
measureChildWithMargins(view, 0, 0);
...
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
...
}
這裡的addView()方法,其實就是ViewGroup的addView()方法;measureChildWithMargins()方法看名字就知道是用於測量子控制元件大小的,這裡我先跳過這個方法的解釋,放在後面來做,目前就簡單地理解為測量子控制元件大小就好了。下面是layoutDecoreated()方法:
public void layoutDecorated(...) {
...
child.layout(...);
}
總結上面程式碼,在RecyclerView的measure及layout階段,填充ItemView的演算法為:向父容器增加子控制元件,測量子控制元件大小,佈局子控制元件,佈局錨點向當前佈局方向平移子控制元件大小,重複上訴步驟至RecyclerView可繪製空間消耗完畢或子控制元件已全部填充。
這樣所有的子控制元件的measure及layout過程就完成了。回到RecyclerView的onMeasure方法,執行mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)這行程式碼的作用就是根據子控制元件的大小,設定RecyclerView的大小。至此,RecyclerView的measure和layout實際上已經完成了。
但是,你有可能已經發現上面過程中的問題了:如何確定RecyclerView的可繪製空間?不過,如果你熟悉android控制元件的繪製機制的話,這就不是問題。其實,這裡的可繪製空間,可以簡單地理解為父容器的大小;更準確的描述是,父容器對RecyclerView的佈局大小的要求,可以通過MeasureSpec.getSize()方法獲得——這裡不包括滑動情況,滑動情況會在後文描述。需要特別說明的是在23.2.0版本之前,RecyclerView是不支援WRAP_CONTENT的。先看下RecyclerView的onLayout()方法:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
...
dispatchLayout();
...
}
這是dispatchLayout()方法:
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
...
dispatchLayoutStep2();
}
dispatchLayoutStep3();
...
}
可以看出,這裡也會執行子控制元件的measure及layout過程。結合onMeasure方法對skipMeasure的判斷可以看出,如果要支援WRAP_CONTENT,那麼子控制元件的measure及layout就會提前在RecyclerView的測量方法中執行完成,也就是說,先確定了子控制元件的大小及位置後,再由此設定RecyclerView的大小;如果是其它情況(測量模式為EXACTLY),子控制元件的measure及layout過程就會延遲至RecyclerView的layout過程(RecyclerView.onLayout())中執行。再看onMeasure方法中的mLayout.mAutoMeasure,它表示,RecyclerView的measure及layout過程是否要委託給RecyclerView.LayoutManager,在相容包中提供的3種RecyclerView.LayoutManager的這個屬性預設都是為true的。好了,以上就是RecyclerView的measure及layout過程,下面來看下它的draw過程。
RecyclerView的draw過程可以分為2部分來看:RecyclerView負責繪製所有decoration;ItemView的繪製由ViewGroup處理,這裡的繪製是android常規繪製邏輯,本文就不再闡述了。下面來看看RecyclerView的draw()和onDraw()方法:
@Override
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
...
}
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
可以看出對於decoration的繪製程式碼上十分簡單。但是這裡,我必須要抱怨一下RecyclerView.ItemDecoration的設計,它實在是太過於靈活了,雖然理論上我們可以使用它在RecyclerView內的任何地方繪製你想要的任何東西——到這一步,RecyclerView的大小位置已經確定的哦。但是過於靈活,太難使用,以至往往使我們無從下手。
好了,題外話就不多說了,來看看decoration的繪製吧。還記得上面提到過的measureChildWithMargins()方法嗎?先來看看它:
public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
final int widthSpec = ...
final int heightSpec = ...
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
這裡是getItemDecorInsetsForChild()方法:
Rect getItemDecorInsetsForChild(View child) {
...
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
方法getItemOffsets()就是我們在實現一個RecyclerView.ItemDecoration時可以重寫的方法,通過mTempRect的大小,可以為每個ItemView設定位置偏移量,這個偏移量最終會參與計算ItemView的大小,也就是說ItemView的大小是包含這個位置偏移量的。我們在重寫getItemOffsets()時,可以指定任意數值的偏移量:
4個方向的位置偏移量對應mTempRect的4個屬性(left,top,right,bottom),我以top offset的值在垂直線性佈局中的應用來舉例說明下。如果top offset等於0,那麼ItemView之間就沒有空隙;如果top offset大於0,那麼ItemView之前就會有一個間隙;如果top offset小於0,那麼ItemView之間就會有重疊的區域。
當然,我們在實現RecyclerView.ItemDecoration時,並不一定要重寫getItemOffsets(),同樣的對於RecyclerView.ItemDecoration.onDraw()或RecyclerView.ItemDecoration.onDrawOver()方法也不是一定要重寫,而且,這個繪製方法和我們所設定的位置偏移量沒有任何聯絡。下面我來實現一個RecyclerView.ItemDecoration來加深下這裡的理解:我將在垂直線性佈局下,在ItemView間繪製一條5個畫素寬、只有ItemView一半長、與ItemView居中對齊的紅色分割線,這條分割線在ItemView內部top位置。
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
Paint paint = new Paint();
paint.setColor(Color.RED);
for (int i = 0; i < parent.getLayoutManager().getChildCount(); i++) {
final View child = parent.getChildAt(i);
float left = child.getLeft() + (child.getRight() - child.getLeft()) / 4;
float top = child.getTop();
float right = left + (child.getRight() - child.getLeft()) / 2;
float bottom = top + 5;
c.drawRect(left,top,right,bottom,paint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
outRect.set(0, 0, 0, 0);
}
程式碼不是很嚴謹,大家姑且一看吧,當然這裡getItemOffsets()方法可以省略的。
以上就是RecyclerView的整個繪製流程了,值得注意的地方也就是在23.2.0中RecyclerView支援WRAP_CONTENT屬性了;還有就是ItemView的填充演算法fill()算是一個亮點吧。接下來,我將分析ReyclerView的滑動流程。
滑動
RecyclerView的滑動過程可以分為2個階段:手指在螢幕上移動,使RecyclerView滑動的過程,可以稱為scroll;手指離開螢幕,RecyclerView繼續滑動一段距離的過程,可以稱為fling。現在先看看RecyclerView的觸屏事件處理onTouchEvent()方法:
public boolean onTouchEvent(MotionEvent e) {
...
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
...
switch (action) {
...
case MotionEvent.ACTION_MOVE: {
...
final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
...
if (mScrollState != SCROLL_STATE_DRAGGING) {
...
if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
if (dy > 0) {
dy -= mTouchSlop;
} else {
dy += mTouchSlop;
}
startScroll = true;
}
if (startScroll) {
setScrollState(SCROLL_STATE_DRAGGING);
}
}
if (mScrollState == SCROLL_STATE_DRAGGING) {
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
} break;
...
case MotionEvent.ACTION_UP: {
...
final float yvel = canScrollVertically ?
-VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
setScrollState(SCROLL_STATE_IDLE);
}
resetTouch();
} break;
...
}
...
}
這裡我以垂直方向的滑動來說明。當RecyclerView接收到ACTION_MOVE事件後,會先計算出手指移動距離(dy),並與滑動閥值(mTouchSlop)比較,當大於此閥值時將滑動狀態設定為SCROLL_STATE_DRAGGING,而後呼叫scrollByInternal()方法,使RecyclerView滑動,這樣RecyclerView的滑動的第一階段scroll就完成了;當接收到ACTION_UP事件時,會根據之前的滑動距離與時間計算出一個初速度yvel,這步計算是由VelocityTracker實現的,然後再以此初速度,呼叫方法fling(),完成RecyclerView滑動的第二階段fling。顯然滑動過程中關鍵的方法就2個:scrollByInternal()與fling()。接下來同樣以垂直線性佈局來說明。先來說明scrollByInternal(),跟蹤進入後,會發現它最終會呼叫到LinearLayoutManager.scrollBy()方法,這個過程很簡單,我就不列出原始碼了,但是分析到這裡先暫停下,去看看fling()方法:
public boolean fling(int velocityX, int velocityY) {
...
mViewFlinger.fling(velocityX, velocityY);
...
}
有用的就這一行,其它亂七八糟的不看也罷。mViewFlinger是一個Runnable的實現ViewFlinger的物件,就是它來控制元件著ReyclerView的fling過程的演算法的。下面來看下類ViewFlinger的一段程式碼:
void postOnAnimation() {
if (mEatRunOnAnimationRequest) {
mReSchedulePostAnimationCallback = true;
} else {
removeCallbacks(this);
ViewCompat.postOnAnimation(RecyclerView.this, this);
}
}
public void fling(int velocityX, int velocityY) {
setScrollState(SCROLL_STATE_SETTLING);
mLastFlingX = mLastFlingY = 0;
mScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
postOnAnimation();
}
可以看到,其實RecyclerView的fling是藉助Scroller實現的;然後postOnAnimation()方法的作用就是在將來的某個時刻會執行我們給定的一個Runnable物件,在這裡就是這個mViewFlinger物件,這部分原理我就不再深入分析了,它已經不屬於本文的範圍了。並且,關於Scroller的作用及原理,本文也不會作過多解釋。對於這兩點各位可以自行查閱,有很多文章對於作過詳細闡述的。接下來看看ViewFlinger.run()方法:
public void run() {
...
if (scroller.computeScrollOffset()) {
final int x = scroller.getCurrX();
final int y = scroller.getCurrY();
final int dx = x - mLastFlingX;
final int dy = y - mLastFlingY;
...
if (mAdapter != null) {
...
if (dy != 0) {
vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
...
}
...
if (!awakenScrollBars()) {
invalidate();//重新整理介面
}
...
if (scroller.isFinished() || !fullyConsumedAny) {
setScrollState(SCROLL_STATE_IDLE);
} else {
postOnAnimation();
}
}
...
}
本段程式碼中有個方法mLayout.scrollVerticallyBy(),跟蹤進入你會發現它最終也會走到LinearLayoutManager.scrollBy(),這樣雖說RecyclerView的滑動可以分為兩階段,但是它們的實現最終其實是一樣的。這裡我先解釋下上段程式碼。第一,dy表示滑動偏移量,它是由Scroller根據時間偏移量(Scroller.fling()開始時間到當前時刻)計算出的,當然如果是RecyclerView的scroll階段,這個偏移量也就是手指滑動距離。第二,上段程式碼會多次執行,至到Scroller判斷滑動結束或已經滑動到邊界。再多說一下,postOnAnimation()保證了RecyclerView的滑動是流暢,這裡涉及到著名的“android 16ms”機制,簡單來說理想狀態下,上段程式碼會以16毫秒一次的速度執行,這樣其實,Scroller每次計算的滑動偏移量是很小的一部分,而RecyclerView就會根據這個偏移量,確定是平移ItemView,還是除了平移還需要再建立新ItemView。
現在就來看看LinearLayoutManager.scrollBy()方法:
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
mOrientationHelper.offsetChildren(-scrolled);
...
}
如上文所講到的fill()方法,作用就是向可繪製區間填充ItemView,那麼在這裡,可繪製區間就是滑動偏移量!再看方法mOrientationHelper.offsetChildren()作用就是平移ItemView。好了整個滑動過程就分析完成了,當然RecyclerView的滑動還有個特性叫平滑滑動(smooth scroll),其實它的實現就是一個fling滑動,所以就不再贅述了。
Recycler
Recycler的作用就是重用ItemView。在填充ItemView的時候,ItemView是從它獲取的;滑出螢幕的ItemView是由它回收的。對於不同狀態的ItemView儲存在了不同的集合中,比如有scrapped、cached、exCached、recycled,當然這些集合並不是都定義在同一個類裡。
回到之前的layoutChunk方法中,有行程式碼layoutState.next(recycler),它的作用自然就是獲取ItemView,我們進入這個方法檢視,最終它會呼叫到RecyclerView.Recycler.getViewForPosition()方法:
View getViewForPosition(int position, boolean dryRun) {
...
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrap = holder != null;
}
// 1) Find from scrap by position
if (holder == null) {
holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
...
}
if (holder == null) {
...
// 2) Find from scrap via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
...
}
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
...
}
}
if (holder == null) {
...
holder = getRecycledViewPool().getRecycledView(type);
...
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
...
mAdapter.bindViewHolder(holder, offsetPosition);
...
}
...
}
這個方法比較長,我先解釋下它的邏輯吧。根據列表位置獲取ItemView,先後從scrapped、cached、exCached、recycled集合中查詢相應的ItemView,如果沒有找到,就建立(Adapter.createViewHolder()),最後與資料集繫結。其中scrapped、cached和exCached集合定義在RecyclerView.Recycler中,分別表示將要在RecyclerView中刪除的ItemView、一級快取ItemView和二級快取ItemView,cached集合的大小預設為2,exCached是需要我們通過RecyclerView.ViewCacheExtension自己實現的,預設沒有;recycled集合其實是一個Map,定義在RecyclerView.RecycledViewPool中,將ItemView以ItemType分類儲存了下來,這裡算是RecyclerView設計上的亮點,通過RecyclerView.RecycledViewPool可以實現在不同的RecyclerView之間共享ItemView,只要為這些不同RecyclerView設定同一個RecyclerView.RecycledViewPool就可以了。
上面解釋了ItemView從不同集合中獲取的方式,那麼RecyclerView又是在什麼時候向這些集合中新增ItemView的呢?下面我逐個介紹下。
scrapped集合中儲存的其實是正在執行REMOVE操作的ItemView,這部分會在後文進一步描述。
在fill()方法的迴圈體中有行程式碼recycleByLayoutState(recycler, layoutState);,最終這個方法會執行到RecyclerView.Recycler.recycleViewHolderInternal()方法:
void recycleViewHolderInternal(ViewHolder holder) {
...
if (forceRecycle || holder.isRecyclable()) {
if (!holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE)) {
// Retire oldest cached view
final int cachedViewSize = mCachedViews.size();
if (cachedViewSize == mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
}
if (cachedViewSize < mViewCacheMax) {
mCachedViews.add(holder);
cached = true;
}
}
if (!cached) {
addViewHolderToRecycledViewPool(holder);
recycled = true;
}
}
...
}
這個方法的邏輯是這樣的:首先判斷集合cached是否満了,如果已満就從cached集合中移出一個到recycled集合中去,再把新的ItemView新增到cached集合;如果不満就將ItemView直接新增到cached集合。
最後exCached集合是我們自己建立的,所以新增刪除元素也要我們自己實現。
資料集、動畫
RecyclerView定義了4種針對資料集的操作,分別是ADD、REMOVE、UPDATE、MOVE,封裝在了AdapterHelper.UpdateOp類中,並且所有操作由一個大小為30的物件池管理著。當我們要對資料集作任何操作時,都會從這個物件池中取出一個UpdateOp物件,放入一個等待佇列中,最後呼叫RecyclerView.RecyclerViewDataObserver.triggerUpdateProcessor()方法,根據這個等待佇列中的資訊,對所有子控制元件重新測量、佈局並繪製且執行動畫。以上就是我們呼叫Adapter.notifyItemXXX()系列方法後發生的事。
顯然當我們對某個ItemView做操作時,它很有可以會影響到其它ItemView。下面我以REMOVE為例來梳理下這個流程。
首先呼叫Adapter.notifyItemRemove(),追溯到方法RecyclerView.RecyclerViewDataObserver.onItemRangeRemoved():
public void onItemRangeRemoved(int positionStart, int itemCount) {
assertNotInLayoutOrScroll(null);
if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
triggerUpdateProcessor();
}
}
這裡的mAdapterHelper.onItemRangeRemoved()就是向之前提及的等待佇列新增一個型別為REMOVE的UpdateOp物件, triggerUpdateProcessor()方法就是呼叫View.requestLayout()方法,這會導致介面重新佈局,也就是說方法RecyclerView.onLayout()會隨後呼叫,這之後的流程就和在繪製流程一節中所描述的一致了。但是動畫在哪是執行的呢?檢視之前所列出的onLayout()方法發現dispatchLayoutStepX方法共有3個,前文只解釋了dispatchLayoutStep2()的作用,這裡就其它2個方法作進一步說明。不過dispatchLayoutStep1()沒有過多要說明的東西,它的作用只是初始化資料,需要詳細說明的是dispatchLayoutStep3()方法:
private void dispatchLayoutStep3() {
...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
...
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
程式碼註釋已經說明得很清楚了,這裡我沒有列出step 3相關的程式碼是因為這部分只是初始化或賦值一些執行動畫需要的中間資料,process()方法最終會執行到RecyclerView.animateDisappearance()方法:
private void animateDisappearance(...) {
addAnimatingView(holder);
holder.setIsRecyclable(false);
if (mItemAnimator.animateDisappearance(holder, preLayoutInfo, postLayoutInfo)) {
postAnimationRunner();
}
}
這裡的animateDisappearance()會把一個動畫與ItemView繫結,並新增到待執行佇列中, postAnimationRunner()呼叫後就會執行這個佇列中的動畫,注意方法addAnimatingView():
private void addAnimatingView(ViewHolder viewHolder) {
final View view = viewHolder.itemView;
...
mChildHelper.addView(view, true);
...
}
這裡最終會向ChildHelper中的一個名為mHiddenViews的集合新增給定的ItemView,那麼這個mHiddenViews又是什麼東西?上節中的getViewForPosition()方法中有個getScrapViewForPosition(),作用是從scrapped集合中獲取ItemView:
ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
...
View view = mChildHelper.findHiddenNonRemovedView(position, type);
...
}
接下來是findHiddenNonRemovedView()方法:
View findHiddenNonRemovedView(int position, int type) {
final int count = mHiddenViews.size();
for (int i = 0; i < count; i++) {
final View view = mHiddenViews.get(i);
RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
if (holder.getLayoutPosition() == position && !holder.isInvalid() && !holder.isRemoved()
&& (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
return view;
}
}
return null;
}
Oops!看到這裡就我之前所講的scrapped集合聯絡起來了,雖然繞了個圈。所以這裡就論證我之前對於scrapped集合的理解。
文章到這裡也快結束了,最後關於動畫,本節提到的對資料集的4種操作,在DefalutItemAnimator中給出了對應的預設實現,就是改變透明度,實現淡入淡出效果。如果要自定義ItemView的動畫可以參考這裡的實現來做。好了,以上就是我對於RecyclerView的全部剖析了,也許還有我沒有提及的方面,或是我講錯的地方,歡迎指正。