前言
前文已經(jīng)在整體上對RecyclerView的實現(xiàn)作出了剖析,但是有些細節(jié)上脾拆,并沒有做太過深入的解釋财异,本文將針對RecyclerView的動畫作更深入剖析劳跃」劾埃
pre&post layout
在RecyclerView中存在一個叫“預布局”的階段邑闲,當然這個是我自己作的翻譯,本來叫pre layout梧油,與之對應的還有個叫post layout的階段苫耸,它們分別發(fā)生在真正的子控件測量&布局的前后。其中pre layout階段的作用是記錄數(shù)據(jù)集改變前的子控件信息儡陨,post layout階段的作用是記錄數(shù)據(jù)集改變后的子控件信息及觸發(fā)動畫褪子。
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
...
dispatchLayoutStep2();
}
dispatchLayoutStep3();
...
}
方法dispatchLayout()會在RecyclerView.onLayout()中被調用,其中dispatchLayoutStep1就是pre layout骗村,dispatchLayoutStep3就是post layout嫌褪,而dispatchLayoutStep2自然就是處理真正測量&布局的了。 首先來看看pre layout時都記錄了什么內容:
private void dispatchLayoutStep1() {
...
if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = ...
...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(...);
mViewInfoStore.addToPreLayout(holder, animationInfo);
...
}
}
...
}
類ItemHolderInfo中封閉了對應ItemView的邊界信息胚股,即ItemView的left笼痛、top、right琅拌、bottom值缨伊。對象mViewInfoStore的作用正如源碼注釋:
/**
* Keeps data about views to be used for animations
*/
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
再來看看addToPreLayout()方法:
void addToPreLayout(ViewHolder holder, ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.preInfo = info;
record.flags |= FLAG_PRE;
}
由上可已看出RecyclerView將pre layout階段的ItemView信息存放在了ViewInfoStore中的mLayoutHolderMap集合中。 接下來我們看看post layout階段:
private void dispatchLayoutStep3() {
...
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
...
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
...
if (...) {
...
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
...
}
這是addToPostLayout()方法:
void addToPostLayout(ViewHolder holder, ItemHolderInfo info) {
InfoRecord record = mLayoutHolderMap.get(holder);
if (record == null) {
record = InfoRecord.obtain();
mLayoutHolderMap.put(holder, record);
}
record.postInfo = info;
record.flags |= FLAG_POST;
}
與pre layout階段相同RecyclerView也是將post layout階段的ItemView信息存放在mViewInfoStore的mLayoutHolderMap集合中进宝,并且不難看出刻坊,同一個ItemView(或者叫ViewHolder)的pre layout信息與post layout信息封裝在了同一個InfoRecord中,分別叫InfoRecord.preInfo與InforRecord.postInfo党晋,這樣InfoRecord就保存著同一個ItemView在數(shù)據(jù)集變化前后的信息谭胚,我們可以根據(jù)此信息定義動畫的開始和結束狀態(tài)。
如上圖所示未玻,當我們插入A時漏益,在完成了上文所訴過程后,以ItemView2為例深胳,通過比較它的preInfo與postInfo——都為非空绰疤,源碼中是以標志位的形式實現(xiàn)的,就可以知道它將執(zhí)行MOVE操作舞终;而A自然就是ADD操作轻庆。下面是ViewInfoStore.ProcessCallback實現(xiàn)中的其中一個方法,它會在mViewInfoStore.process()方法中被調用:
public void processPersistent(...) {
...
if (mDataSetHasChangedAfterLayout) {
...
} else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
postAnimationRunner();
}
}
我們知道敛劝,RecyclerView中ItemAnimator的默認實現(xiàn)是DefaultItemAnimator余爆,這里我就只以默認實現(xiàn)來說明,這是animatePersistence()方法:
public boolean animatePersistence(...) {
if (preInfo.left != postInfo.left || preInfo.top != postInfo.top) {
...
return animateMove(viewHolder,
preInfo.left, preInfo.top, postInfo.left, postInfo.top);
}
dispatchMoveFinished(viewHolder);
return false;
}
當然這個方法在DefaultItemAnimator的父類SimpleItemAnimator中夸盟,通過比較preInfo與postInfo的left和top屬性分別確定ItemView在水平或垂直方向是否要執(zhí)行MOVE操作蛾方,而上面的方法postAnimationRunner()就是用來觸發(fā)動畫執(zhí)行的。
通過前文我們知道,RecyclerView中定義了4種針對數(shù)據(jù)集的操作(也可以稱為針對ItemView的操作)桩砰,分別是ADD拓春、REMOVE、UPDATE亚隅、MOVE硼莽,RecyclerView就是通過比較preInfo與postInfo來確定ItemView要執(zhí)行哪種操作的,上文我描述了MOVE情況煮纵,這個比較過程是在方法ViewInfoStore.process()中實現(xiàn)的懂鸵,其它情況我就不再贅述了,各位不妨自己去看看行疏。
在DefaultItemAnimator中實現(xiàn)了上面4種操作下的動畫匆光。當postAnimationRunner()執(zhí)行后,會觸發(fā)DefaultItemAnimator.runPendingAnimations()方法的調用酿联,這個方法過長终息,我這里只作下解釋便可。4種操作對應的動畫是有先后順序的货葬,remove–>move&change–>add采幌,之所以有這樣的順序,不難看出是為了不讓ItemView之間有重疊的區(qū)域震桶,這個順序是由ViewCompat.postOnAnimationDelayed()方法通過控制延時來實現(xiàn)的休傍。在DefaultItemAnimator中,REMOVE和ADD對應的是淡入淡出動畫(改變透明度)蹲姐,MOVE對應的是平移動畫磨取;UPDATE相對來說要復雜一些,是因為它不再是記錄同一個ItemView的變化情況柴墩,而是記錄2個ItemView的信息來作比較忙厌,pre layout階段的信息來自“oldChangeViewHolder”,post layout階段的信息來自“holder”江咳,這兩個對象在dispatchLayoutStep3方法中可以找到逢净,而且,這2個ItemView的動畫是同時執(zhí)行的歼指,所以它對應的動畫是:“oldHolder”淡出且向“newHolder”平移爹土,同時“newHolder”淡入。特別說明踩身,前文有提過一個叫scrapped的集合胀茵,其實它除了保存REMOVE操作的ItemView,還保存著UPDATE操作中的“oldHolder”挟阻!
以上就是RecyclerView默認動畫的具體實現(xiàn)邏輯了琼娘,總結下來就是:當數(shù)據(jù)集發(fā)生變化時峭弟,會導致RecyclerView重新測量&布局子控件,我們記錄下這個變化前后的RecyclerView的快照(preInfo與postInfo)脱拼,通過比較這2個快照瞒瘸,從而確定子控件要執(zhí)行什么操作,最后再實現(xiàn)不同操作下對應的動畫就好了挪拟。通常我們會調用notifyItemXXX()系列方法來通知RecyclerView數(shù)據(jù)集變化挨务,這些方法之所以比notifyDataSetChanged()高效的原因就是它們不會讓整個RecyclerView重新繪制击你,而是只重繪具體的子控件玉组,并且通過動畫連接子控件的前后狀態(tài),這樣也就實現(xiàn)了在Material design中所講的“Visual continuity”效果丁侄。
子控件的測量與布局
這一節(jié)將對preInfo與postInfo是如果確定(賦值)的惯雳,作進一步描述。 從前文我們知道鸿摇,子控件的測量與布局其實在RecyclerView的測量階段(onMeasure)就執(zhí)行完了石景,這樣做是為了支持WRAP_CONTENT,具體的方法呢就是dispatchLayoutStep1()與dispatchLayoutStep2()拙吉,同樣這兩個方法也會出現(xiàn)在RecyclerView的布局階段(onLayout)潮孽,但并不是說它們就會被調用,這里的調用邏輯是由RecyclerView.State類控制的筷黔,它定義了RecyclerView的整個測量布局過程往史,分為3步STEP_START、STEP_LAYOUT佛舱、STEP_ANIMATIONS椎例,具體流程是:初始狀態(tài)是STEP_START;如果RecyclerView當前在STEP_START階段dispatchLayoutStep1()會執(zhí)行请祖,記錄下preInfo订歪,將狀態(tài)改為STEP_LAYOUT;如果RecyclerView在STEP_LAYOUT階段dispatchLayoutStep2()會執(zhí)行肆捕,測量布局子控件刷晋,將狀態(tài)改為STEP_ANIMATIONS;如果RecyclerView在STEP_ANIMATIONS階段dispatchLayoutStep3()會執(zhí)行慎陵,記錄下postInfo眼虱,觸發(fā)動畫,將狀態(tài)改為STEP_START荆姆。每次數(shù)據(jù)集更改都會執(zhí)行上述3步蒙幻。 在測量布局子控件的過程中,最重要的莫過于確定布局錨點了胆筒,以LinearLayoutManager垂直布局為例邮破,在onLayoutChildren()方法中诈豌,會調用updateAnchorInfoForLayout()方法來確定布局錨點:
private void updateAnchorInfoForLayout(...) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
...
return;
}
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
...
return;
}
...
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
這里布局錨點的確定方法有3種依據(jù)。首先抒和,如果是第一次布局(沒有ItemView)矫渔,這種情況已經(jīng)在前文有過描述了,這里就不再說明摧莽;剩余的2種分別是“滑動位置”與“子控件”庙洼,這2種情況都是發(fā)生在已經(jīng)有ItemView時的,而且這里的“滑動位置”是指由方法scrollToPosition()確認的镊辕,并賦給了mPendingScrollPosition變量∮凸唬現(xiàn)在先來看看“滑動位置”updateAnchorFromPendingData()方法:
private boolean updateAnchorFromPendingData(...) {
...
// if child is visible, try to make it a reference child and ensure it is fully visible.
// if child is not visible, align it depending on its virtual position.
anchorInfo.mPosition = mPendingScrollPosition;
...
if (mPendingScrollPositionOffset == INVALID_OFFSET) {
View child = findViewByPosition(mPendingScrollPosition);
if (child != null) {
...
} else { // item is not visible.
...
}
return true;
}
...
return true;
}
布局錨點中的mCoordinate與mPosition,在前文描述為起始繪制偏移量與索引位置征懈,再直白點就是屏幕位置與數(shù)據(jù)集位置石咬,就是告訴RecyclerView從屏幕的mCoordinate位置開始填充子控件,與子控件綁定的數(shù)據(jù)從數(shù)據(jù)集的mPosition位置開始取得卖哎。上面這個方法中確定“屏幕位置”分為2種情況鬼悠,就是對應于mPendingScrollPosition是否存在子控件,mCoordinate值的確定我就不再講述了亏娜,無非是一邊界判斷的語句焕窝。 下面來看看“子控件”依據(jù)的情況,這是updateAnchorFromChildren():
private boolean updateAnchorFromChildren(...) {
...
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild);
...
return true;
}
return false;
}
這種情況也并不復雜维贺,就是找到最外邊的一個子控件它掂,以它的位置信息來確定布局錨點,就是方法assignFromView()幸缕,我也就不再列出來了群发。以上就是詳細的布局錨點確認過程了。