如何閱讀本篇文章
本文主要講解RecyclerView Layout變化觸發動畫執行的原理。前半部分偏重原理和代碼的講解,後半部分經過圖文結合場景講解各個階段的執行過程。java
建議先粗略閱讀前半部分的原理和代碼篇,作到心中有概念,帶着理論知識去閱讀後半部分的場景篇。最後結合全文學到的知識,帶着問題去閱讀源碼,效果會更好。web
原理篇
1. Adapter的notify方法
用過RecyclerView的同窗大概都應該知道Adapter有幾個notify相關的方法,它們分別是:面試
-
notifyDataSetChanged() -
notifyItemChanged(int) -
notifyItemInserted(int) -
notifyItemRemoved(int) -
notifyItemRangeChanged(int, int) -
notifyItemRangeInserted(int, int) -
notifyItemRangeRemoved(int, int) -
notifyItemMoved(int, int)
稍微有點開發經驗的同窗都知道,notifyDataSetChanged()方法比其它的幾個方法更重量級一點,它會致使整個列表刷新,其它幾個方法則不會。有更多開發經驗的同窗可能還知道notifyDataSetChanged()方法不會觸發RecyclerView的動畫機制,其它幾個方法則會觸發各類不一樣類型的動畫。算法
2. RecyclerView的佈局邏輯
2.1 RecyclerView的dispatchLayout
dispatchLayout顧名思義,固然是把子View佈局(添加並放置到合適的位置)到RecyclerView上面了。打開它的源碼咱們能夠看到這樣一段註釋。緩存
Wrapper around layoutChildren() that handles animating changes caused by layout. Animations work on the assumption that there are five different kinds of items in play:微信
PERSISTENT: items are visible before and after layoutapp
REMOVED: items were visible before layout and were removed by the app框架
ADDED: items did not exist before layout and were added by the app編輯器
DISAPPEARING: items exist in the data set before/after, but changed from visible to non-visible in the process of layout (they were moved off screen as a side-effect of other changes)ide
APPEARING: items exist in the data set before/after, but changed from non-visible to visible in the process of layout (they were moved on screen as a side-effect of other changes)
從註釋咱們能夠知道。dispatchLayout方法不只有給子View佈局的功能,並且能夠處理動畫。動畫主要分爲五種:
-
PERSISTENT:針對佈局前和佈局後都在手機界面上的View所作的動畫 -
REMOVED:在佈局前對用戶可見,可是數據已經從數據源中刪除掉了 -
ADDED:新增數據到數據源中,而且在佈局後對用戶可見 -
DISAPPEARING:數據一直都存在於數據源中,可是佈局後從可見變成不可見狀態 -
APPEARING:數據一直都存在於數據源中,可是佈局後從不可見變成可見狀態
到目前爲止,咱們還不能徹底理解這五種類型的動畫有什麼具體的區別,分別在什麼樣的場景下會觸發這些類型的動畫。可是給咱們提供了很好的研究思路。目前咱們只須要簡單瞭解有這五種動畫,接着往下,咱們這裏看下dispatchLayout的源碼,爲了響應文章標題,這裏貼出精簡過的源碼:
void dispatchLayout(){
...
dispatchLayoutStep1();
dispatchLayoutStep2();
dispatchLayoutStep3();
...
}
關於dispatchLayoutStepX方法,相信不少人都據說或者瞭解過,文章後面我會作詳細的介紹,簡單介紹以下:
從dispatchLayout的註釋中,咱們注意到before和after兩個單詞,分別表示佈局前和佈局後。這麼說來那就簡單了。dispatchLayoutStep1對應的是before(佈局前),dispatchLayoutStep2的意思是佈局中,dispatchLayoutStep3對應的是after(佈局後)。它們的做用描述以下:
-
dispatchLayoutStep1 -
判斷是否須要開啓動畫功能
-
若是開啓動畫,將當前屏幕上的Item相關信息保存起來供後續動畫使用
-
若是開啓動畫,調用mLayout.onLayoutChildren方法預佈局
-
預佈局後,與第二步保存的信息對比,將新出現的Item信息保存到Appeared中
精簡後的代碼以下:
private void dispatchLayoutStep1() {
...
//第一步 判斷是否須要開啓動畫功能
processAdapterUpdatesAndSetAnimationFlags();
...
if (mState.mRunSimpleAnimations) {
...
//第二步 將當前屏幕上的Item相關信息保存起來供後續動畫使用
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());
mViewInfoStore.addToPreLayout(holder, animationInfo);
}
...
if (mState.mRunPredictiveAnimations) {
saveOldPositions();
//第三步 調用onLayoutChildren方法預佈局
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
final View child = mChildHelper.getChildAt(i);
final ViewHolder viewHolder = getChildViewHolderInt(child);
if (viewHolder.shouldIgnore()) {
continue;
}
//第四步 預佈局後,對比預佈局先後,哪些item須要放入到Appeared中
if (!mViewInfoStore.isInPreLayout(viewHolder)) {
if (wasHidden) {
recordAnimationInfoIfBouncedHiddenView(viewHolder, animationInfo);
} else {
mViewInfoStore.addToAppearedInPreLayoutHolders(viewHolder, animationInfo);
}
}
}
clearOldPositions();
} else {
clearOldPositions();
}
}
}
-
dispatchLayoutStep2 根據數據源中的數據進行佈局,真正展現給用戶看的最終界面
private void dispatchLayoutStep2() {
...
// Step 2: Run layout
mState.mInPreLayout = false;//此處關閉預佈局模式
mLayout.onLayoutChildren(mRecycler, mState);
...
}
-
dispatchLayoutStep3 觸發動畫
private void dispatchLayoutStep3() {
...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
// traverse list in reverse because we may call animateChange in the loop which may
// remove the target view holder.
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
// run a change animation
...
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
//觸發動畫
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
從代碼咱們能夠看出dispatchLayoutStep1和dispatchLayoutStep2方法中調用了onLayoutChildren方法,而dispatchLayoutStep3沒有調用。
2.2 LinearLayoutManager的onLayoutChildren方法
以垂直方向的RecyclerView爲例子,咱們填充RecyclerView的方向有兩種,從上往下填充和從下往上填充。開始填充的位置不是固定的,能夠從RecyclerView的任意位置處開始填充。該方法的功能我精簡爲如下幾個步驟:
-
尋找填充的錨點(最終調用findReferenceChild方法) -
移除屏幕上的Views(最終調用detachAndScrapAttachedViews方法) -
從錨點處從上往下填充(調用fill和layoutChunk方法) -
從錨點處從下往上填充(調用fill和layoutChunk方法) -
若是還有多餘的空間,繼續填充(調用fill和layoutChunk方法) -
非預佈局,將scrapList中多餘的ViewHolder填充(調用layoutForPredictiveAnimations)
本文只講解onLayoutChildren的主流程,具體的填充邏輯請參考RecyclerView填充邏輯一文
LinearLayoutManager#onLayoutChildren
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//1. 尋找填充的錨點
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
...
//2. 移除屏幕上的Views
detachAndScrapAttachedViews(recycler);
...
//3. 從錨點處從上往下填充
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
...
//4. 從錨點處從下往上填充
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtraFillSpace = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
...
//5. 若是還有多餘的空間,繼續填充
if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
// start could not consume all it should. add more items towards end
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtraFillSpace = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}
...
//6. 非預佈局,將scrapList中多餘的ViewHolder填充
layoutForPredictiveAnimations(recycler, state, startOffset, endOffset);
...
LinearLayoutManager#layoutForPredictiveAnimations
private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler,
RecyclerView.State state, int startOffset,
int endOffset) {
//判斷是否知足條件,若是是預佈局直接返回
if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout()
|| !supportsPredictiveItemAnimations()) {
return;
}
// 遍歷scrapList,步驟2中屏幕中被移除的View
int scrapExtraStart = 0, scrapExtraEnd = 0;
final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
final int scrapSize = scrapList.size();
final int firstChildPos = getPosition(getChildAt(0));
for (int i = 0; i < scrapSize; i++) {
RecyclerView.ViewHolder scrap = scrapList.get(i);
//若是被remove掉了,跳過
if (scrap.isRemoved()) {
continue;
}
//計算額外的控件
scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
}
mLayoutState.mScrapList = scrapList;
...
// 步驟6 繼續填充
if (scrapExtraEnd > 0) {
View anchor = getChildClosestToEnd();
updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
mLayoutState.mExtraFillSpace = scrapExtraEnd;
mLayoutState.mAvailable = 0;
mLayoutState.assignPositionFromScrapList();
fill(recycler, mLayoutState, state, false);
}
mLayoutState.mScrapList = null;
}
至此,佈局的邏輯已經講解完畢。關於具體的動畫執行邏輯,因爲篇幅有限。不在本文中講解
場景篇
1. notifyItemRemoved
咱們來測試從屏幕中刪除View,調用notifyItemRemoved相關的方法,dispatchLayout是如何從新佈局的。假設初始狀態以下圖,假設Adapter數據有100條,屏幕上有Item1~Item6 6個View,刪除Item1和Item2。
-
將Item1 Item2對應的ViewHolder設置爲REMOVE狀態 -
將全部的Item對應的ViewHolder的mPreLayoutPosition字段賦值爲當前的position
咱們回顧一下onLayoutChildren的幾個步驟
-
尋找填充的錨點(最終調用findReferenceChild方法) -
移除屏幕上的Views(最終調用detachAndScrapAttachedViews方法) -
從錨點處從上往下填充(調用fill和layoutChunk方法) -
從錨點處從下往上填充(調用fill和layoutChunk方法) -
若是還有多餘的空間,繼續填充(調用fill和layoutChunk方法) -
非預佈局,將scrapList中多餘的ViewHolder填充(調用layoutForPredictiveAnimations)
1.1 dispatchLayoutStep1階段
-
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3
-
移除屏幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap緩存中,這個緩存的好處是若是position對應上了,無需從新綁定,直接拿來用。
-
從錨點Item3處往下填充,mAttachedScrap只剩下ViewHolder2和ViewHolder1
-
從錨點Item3處往上填充Item2 Item1,由於Item2,Imte1已經被remove掉了,它消耗的空間不會被記錄,那麼到步驟5的時候還能夠填充
-
還有多餘的空間,繼續填充,把Item七、Item8填充到屏幕中
-
由於當前是預佈局,直接返回
至此step1的layout結束
1.2 dispatchLayoutStep2階段
-
尋找填充的錨點,尋找錨點的邏輯是,從上往下,找到第一個非remove狀態的Item。在本Case中,找到Item3
-
移除屏幕上的Views,將它們的ViewHolder放入到Recycler的mAttachedScrap緩存中
-
從錨點Item3處往下填充,填充到Item6爲止,就沒有足夠的距離了,mAttachedScrap只剩下ViewHolder8,ViewHolder7,ViewHolder2,ViewHolder1
-
往上填充,雖然此時還有兩個View的高度,可是此時,上邊沒有數據了,此處不填充
-
此時還有兩個View的高度,繼續往下填充
注意此時已經佈局完成可是屏幕上部與第一個有GAP,會修復
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
// we re-calculate it.
// find which side we should check for gaps.
if (mShouldReverseLayout ^ mStackFromEnd) {
int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
} else {
int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
startOffset += fixOffset;
endOffset += fixOffset;
fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
startOffset += fixOffset;
endOffset += fixOffset;
}
}
修復後效果以下
-
當前不是預佈局,可是由於ViewHolder1和ViewHolder2都是被Remove掉的,因此跳過
2. notifyItemInserted
假設在Item1下面插入兩條數據AddItem1,AddItem2
2.1 dispatchLayoutStep1階段
-
尋找錨點,找到Item1 -
移除屏幕上的Views,放入到mAttachedScrap中 -
錨點處從上往下填充 -
錨點處從下往上填充,由上圖可知,上面沒有空間了,不填充 -
判斷是否還有剩餘的空間,若是有在末尾填充,下面沒空間了,不填充 -
由於當前是預佈局階段,不填充
2.2 dispatchLayoutStep2階段
-
尋找錨點,找到Item1 -
移除屏幕上的Views,放入到mAttachedScrap中 -
錨點處從上往下填充,此時將變化後的數據填充到屏幕上,addItem1和addItem2被填充到item1下面 -
錨點處從下往上填充,由圖可知,沒有空間不填充 -
判斷是否還有剩餘的空間,由圖可知,沒有空間不填充 -
當前是layoutStep2階段,會將mAttachScrap的內容,填充到屏幕末尾,ViewHolder5和ViewHolder6對應的ItemView被填充
2.3 dispatchLayoutStep3階段
開始動畫,動畫結束後,item5和item6會被回收掉,此時會被回收到mCachedViews緩存池中
本篇不涉及到動畫具體如何執行,且聽下回分解吧。
往期文章
常見的鏈表翻轉,字節跳動加了個條件,面試者高呼「我太難了」| 圖解算法
Gson 和 Kotlin data class 的避坑指南
若是你以爲對你有所幫助的話,能夠關注個人公衆號 徐公碼字(stormjun94),第一時間會在上面更新
本文分享自微信公衆號 - 徐公碼字(stormjun94)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。