android RecyclerView (三):ItemAnimator 詳解

本文繼上篇 ItemDecoration 以後,是深刻理解 RecyclerView 系列的第二篇,關注於 ItemAnimator,主要是分析 RecyclerView Animators 這個庫的原理,而後總結如何本身編寫自定義的 ItemAnimator。本文涉及到的完整代碼能夠在 Github 獲取java

先看看類結構

  • DefaultItemAnimator extends SimpleItemAnimator extends RecyclerView.ItemAnimator
  • FadeInAnimator extends BaseItemAnimator extends SimpleItemAnimator extends RecyclerView.ItemAnimator
  • RecyclerView.ItemAnimator 定義了一系列 API 用於開發 item view 的動效
    • animateDisappearance, animateAppearance, animatePersistence, animateChange 這4個 API 用來對 item view 進行動畫顯示
    • recordPreLayoutInformation, recordPostLayoutInformation 這2個 API 用來記錄 item view 在 layout 先後的狀態信息,這些信息封裝在 ItemHolderInfo 或者其子類中,並將傳遞給上述4個動畫API中,以便進行動畫展現
    • runPendingAnimations 能夠用來延遲動畫到下一幀,此時就須要在上述4個 API 的實現中返回 true,而且自行記錄延遲的動畫信息,以便在下一幀時執行
    • dispatchAnimationStarteddispatchAnimationFinished 是用來進行狀態同步和事件通知的,子類必須在動畫開始時調用 dispatchAnimationStarted,結束時調用 dispatchAnimationFinished,固然若是不展現動畫,那就只須要直接調用 dispatchAnimationFinished
  • SimpleItemAnimator 則對 RecyclerView.ItemAnimator 的 API 進行了一次封裝
    • 把父類定義的4個動畫 API 轉換爲了 animateRemove, animateAdd, animateMove, animateChange 這4個,爲何這樣?這一次封裝就把對 preLayoutInfo 和 postLayoutInfo 的處理的公共代碼封裝了起來,把 ItemHolderInfo 轉換爲了 left, top, x, y 這樣的位置信息,這樣,大部分動畫只須要根據位置變化信息的實現,專一實現本身的動畫邏輯便可,一方面複用了代碼,另外一方面也更好的踐行了單一職責原則
    • 可是若是位置信息對於動畫的展現不夠,那就須要本身重寫 RecyclerView.ItemAnimator 的相應動畫 API 了
    • 同時也定義了一系列 dispatch***on*** API,用於進行事件回調
  • DefaultItemAnimator 是 RecyclerView 包中的一個默認實現,而 BaseItemAnimator 則是 RecyclerView Animators 庫中 animator 的基類,它們都繼承自 SimpleItemAnimator,二者具備很大類似性,只分析後者

BaseItemAnimator

BaseItemAnimator 實現了父類的 animateRemove, animateAdd, animateMove, animateChange 這4個 API,而實現方式都是把參數包裝一下,放入相應的 animation 列表中,並返回 true,而後在 runPendingAnimations 函數中集中顯示動畫。爲何要這樣呢?由於 recycler view 的變化是隨時均可能發生的,而這樣的處理就能夠把動畫的顯示按幀對其,即兩幀之間的變化,都在下一幀開始時一塊兒處理。可是這樣作有什麼優點呢?暫時不得而知,DefaultItemAnimator 就是這樣處理的。git

例如 animateRemove 的實現以下:github

@Override public boolean animateRemove(final ViewHolder holder) { endAnimation(holder); preAnimateRemove(holder); mPendingRemovals.add(holder); return true; } 

那麼下面重點看看 runPendingAnimations。app

@Override public void runPendingAnimations() { boolean removalsPending = !mPendingRemovals.isEmpty(); boolean movesPending = !mPendingMoves.isEmpty(); boolean changesPending = !mPendingChanges.isEmpty(); boolean additionsPending = !mPendingAdditions.isEmpty(); if (!removalsPending && !movesPending && !additionsPending && !changesPending) { // nothing to animate return; } // First, remove stuff for (ViewHolder holder : mPendingRemovals) { doAnimateRemove(holder); } mPendingRemovals.clear(); // Next, move stuff if (movesPending) { final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>(); moves.addAll(mPendingMoves); mMovesList.add(moves); mPendingMoves.clear(); Runnable mover = new Runnable() { @Override public void run() { for (MoveInfo moveInfo : moves) { animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, moveInfo.toX, moveInfo.toY); } moves.clear(); mMovesList.remove(moves); } }; if (removalsPending) { View view = moves.get(0).holder.itemView; ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); } else { mover.run(); } } // Next, change stuff, to run in parallel with move animations if (changesPending) { final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>(); changes.addAll(mPendingChanges); mChangesList.add(changes); mPendingChanges.clear(); Runnable changer = new Runnable() { @Override public void run() { for (ChangeInfo change : changes) { animateChangeImpl(change); } changes.clear(); mChangesList.remove(changes); } }; if (removalsPending) { ViewHolder holder = changes.get(0).oldHolder; ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); } else { changer.run(); } } // Next, add stuff if (additionsPending) { final ArrayList<ViewHolder> additions = new ArrayList<ViewHolder>(); additions.addAll(mPendingAdditions); mAdditionsList.add(additions); mPendingAdditions.clear(); Runnable adder = new Runnable() { public void run() { for (ViewHolder holder : additions) { doAnimateAdd(holder); } additions.clear(); mAdditionsList.remove(additions); } }; if (removalsPending || movesPending || changesPending) { long removeDuration = removalsPending ? getRemoveDuration() : 0; long moveDuration = movesPending ? getMoveDuration() : 0; long changeDuration = changesPending ? getChangeDuration() : 0; long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); View view = additions.get(0).itemView; ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); } else { adder.run(); } } } 

在這裏,remove 最早執行,remove 執行完畢後,再同時開始 move 和 change,而它倆都結束後,最後再執行 add。BaseItemAnimator 對 add 和 remove 這兩個動畫的播放進行了再一次的封裝,定義了 animateAddImplanimateRemoveImpl 這兩個 API,以及 preAnimateAddImplpreAnimateRemoveImpl 供動畫開始前進行須要的操做,而這個庫內置的多種 animator,都只是在這兩個 API 中實現了不一樣的出現和消失的邏輯。add 和 remove 這兩個動畫的進一步封裝,再次簡化了編寫 animator 的代碼,具體的 animator 只須要專一於本身的動畫顯示邏輯便可。而 move 和 change 這兩類動畫,則是直接使用了 DefaultItemAnimator 的代碼,move 就是經過 TranslationX 和 TranslationY 把 item view 從老位置移動到新位置,change 就是經過 TranslationX, setTranslationY 和 alpha 來完成內容的改變效果。ide

自定義的 move 和 change 實現

這部分有一篇不錯的文章:InstaMaterial - RecyclerView animations done right 函數

基本原理仍是 RecyclerView.ItemAnimator 提供的 API,canReuseUpdatedViewHolder 控制動效時是否建立新的 ViewHolder,recordPreLayoutInformation/recordPostLayoutInformation 用來在 layout 以前/後記錄須要的信息,animateChange/animateMove 來實現具體的動畫邏輯,而這時可能會須要 layout 先後記錄的信息。post

在這篇文章中,做者就是在 recordPreLayoutInformation 中把須要的信息記錄在了自定義的 ItemHolderInfo 中,而且在 animateChange 使用記錄的信息進行動畫的顯示。這個過程並無什麼難點,主要仍是動畫的設計,以及實現的效率和穩定性,例如避免反覆建立沒必要要的對象,避免出現閃退等。具體的例子能夠看這篇文章。動畫

Talk is cheap, show me the code

好了,說了這麼多,仍是須要一個完整的 demo 才接地氣。結合自家產品的需求,這個 demo 中將要實現這樣的效果:列表可滑動,新數據加入時,若是正在滑動,則不自動滾到最新(最底部),若是超過5秒不滑動,則自動滾動到最新,若是原本就在最新,則動效塞入新數據(fade in),列表中的數據15秒自動消失,fade out 消失動效,每一個 item 點擊以後有一個「桃心放大」的效果,模仿的是上一節中那篇文章的效果。關於滑動檢測、自動滑動的內容,將在下一篇中展開,本篇聚焦於 ItemAnimator,因此這一版本包含的是增長、移除、點擊的動效。spa

總體效果

增長和移除的動效,直接繼承自 RecyclerView Animators 庫的 FadeInAnimator 就能夠實現了,而點擊動效,則直接借用了 InstaMaterial 的部分代碼。在這個過程當中,還發現了 RecyclerView Animators 的一個 bug,它的全部內置 animator 的實現,change 動效都不起做用,並且還會影響其餘類型的動效展現,緣由比較簡單,重載了 animateChange,可是既沒有調用 super 的實現,也沒有調用 dispatchAnimationFinished,具體能夠查看這個 issue 下的評論設計

從最後的效果圖中咱們能夠看到,若是 item view 快要消失時,咱們點擊了,播放點擊動效以後,item 的消失會有閃爍的問題,這個問題本篇先暫且放下,後續的文章中會進行分析和解決。

相關文章
相關標籤/搜索