Android RecyclerView工作原理分析(下)

前言
  前文已經(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()幸缕,我也就不再列出來了群发。以上就是詳細的布局錨點確認過程了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末发乔,一起剝皮案震驚了整個濱河市熟妓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栏尚,老刑警劉巖起愈,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異译仗,居然都是意外死亡抬虽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門纵菌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來阐污,“玉大人,你說我怎么就攤上這事咱圆〉驯伲” “怎么了功氨?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長手幢。 經(jīng)常有香客問我捷凄,道長,這世上最難降的妖魔是什么围来? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任跺涤,我火速辦了婚禮,結果婚禮上监透,老公的妹妹穿的比我還像新娘桶错。我一直安慰自己,他們只是感情好才漆,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布牛曹。 她就那樣靜靜地躺著佛点,像睡著了一般醇滥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上超营,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天鸳玩,我揣著相機與錄音,去河邊找鬼演闭。 笑死不跟,一個胖子當著我的面吹牛,可吹牛的內容都是我干的米碰。 我是一名探鬼主播窝革,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吕座!你這毒婦竟也來了虐译?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤吴趴,失蹤者是張志新(化名)和其女友劉穎漆诽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锣枝,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡厢拭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了撇叁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片供鸠。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖陨闹,靈堂內的尸體忽然破棺而出楞捂,到底是詐尸還是另有隱情家制,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布泡一,位于F島的核電站颤殴,受9級特大地震影響,放射性物質發(fā)生泄漏鼻忠。R本人自食惡果不足惜涵但,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望帖蔓。 院中可真熱鬧矮瘟,春花似錦、人聲如沸塑娇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽埋酬。三九已至哨啃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間写妥,已是汗流浹背拳球。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留珍特,地道東北人祝峻。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像扎筒,于是被迫代替她去往敵國和親莱找。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內容

  • 基本使用RecyclerView的基本使用并不復雜嗜桌,只需要提供一個RecyclerView.Apdater的實現(xiàn)用...
    龐哈哈哈12138閱讀 6,017評論 2 46
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,325評論 25 707
  • RecyclerView 源碼分析 本文原創(chuàng)奥溺,轉載請注明出處。歡迎關注我的 簡書 症脂,關注我的專題 Android ...
    MeloDev閱讀 10,117評論 6 49
  • 故事十一(不可能有有答案的"答案") 德國著名數(shù)字高斯在上中學時谚赎,有一次在數(shù)學課上打瞌睡,下課鈴聲響起時诱篷,他醒了過...
    芊然閱讀 958評論 0 0
  • 與你相識有十五余年了棕所,最深刻的應該是你那如花般的笑容---從容自信闸盔、活潑俏皮。你渾身總是充滿正能量琳省。咱們在一...
    yy914閱讀 189評論 0 1