相信大家都對`RecyclerView`的用法相當熟悉了,`RecyclerView`的出現給我們開發者提供了一個高擴展的控件,不管是列表、網格、瀑布流,一個控件就可以搞定,而且神奇的是`只需要修改一行代碼,就可以輕松切換`。`RecyclerView`的好處太多太多,就不一一列舉了,網上也有很多關于`RecyclerView`的教程。說到這里,我們就開始進入主題了,雖然網上有那么多的`RecyclerView`教程,但是沒有一篇是詳細介紹`RecyclerView`的動畫的,大部分都是使用默認的`DefaultItemAnimator`或者使用第三方的動畫庫,這篇博客我們就來彌補這個空白,咱們來根據`DefaultItemAnimator`的代碼來實現一個簡單的`RecyclerView`動畫。
> 我們僅僅去實現一下最常用的`add`和`remove`的動畫,其他的動畫,如果大家感興趣,可以自己參考`DefaultItemAnimator`去擴展。
在開始之前,我們先來看看實現的效果吧:

如何定制動畫呢?首先要繼承自`RecyclerView.ItemAnimator`這個類,既然要繼承這個類,那我們有必要去了解一下這個類和這個類中的幾個方法(僅關注我們今天需要的,其他的類同)。
> This class defines the animations that take place on items as changes are made to the adapter. Subclasses of ItemAnimator can be used to implement custom animations for actions on ViewHolder items. The RecyclerView will manage retaining these items while they are being animated, but implementors must call the appropriate “Starting” (dispatchRemoveStarting(ViewHolder), dispatchMoveStarting(ViewHolder), dispatchChangeStarting(ViewHolder, boolean), or dispatchAddStarting(ViewHolder)) and “Finished” (dispatchRemoveFinished(ViewHolder), dispatchMoveFinished(ViewHolder), dispatchChangeFinished(ViewHolder, boolean), or dispatchAddFinished(ViewHolder)) methods when each item animation is being started and ended.
只看重點,當開始動畫時,我們需要調用`dispatchXXXStarting()`,當動畫結束時,我們需要調用`dispatchXXXFinished()`。
接下來,來看看需要我們去動手實現的幾個方法:
1.
isRunning()
> 返回當前是否有動畫需要執行。
1.
runPendingAnimations()
> 當有動畫要執行的時候調用。這里需要說明一點,當我們去add一個item時,動畫可能不是立即去執行的,這種機制可以讓ItemAnimator一個個的添加,然后一塊去執行。
1.
animateAdd()
> add時的動畫,當我們調用`Adapter.notifyItemInsert()`時會觸發該方法,該方法有一個boolean類型的返回值,返回值表示:`runPendingAnimations`是否可以在下一個時機去執行。所以當我們定制動畫時,這個方法要返回true。
1.
與`animateAdd`類似的還有`animateMove`、`animateRemove`、`animateChange`。
1.
dispatchAddStarting()
> 動畫開始時調用。
1.
dispatchAddFinished()
> 動畫結束時調用。
1.
dispatchAnimationsFinished()
> 所有動畫結束時調用。
好了,介紹完了幾個用到的方法,下面就來動手實現我們自己的動畫吧。還是上面說的,我們只實現`add`和`remove`的動畫。模仿著`DefaultItemAnimator`算了算,我們需要4個`ArrayList`.
~~~
private ArrayList<RecyclerView.ViewHolder> mPendingAddHolders =
new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mPendingRemoveHolders =
new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mAddAnimtions = new ArrayList<>();
private ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>();
~~~
明明就兩個動畫,怎么需要4個`ArrayList`呢?而且還是一對一對的!羨慕不?這里要好好說道說道了。上面說了,動畫可能不是立即執行的,而是在`runPendingAnimations`中一塊去執行,所以我們在`animateAdd`中,僅僅是向`mPendingAddHolders`中添加了一個`ViewHolder`,而不是去寫動畫的代碼。這時,考慮一種情況:
> 當我們`animateAdd`了一次,這時`runPendingAnimations`里的動畫還沒執行完畢,所以我們還不能清空`mPendingAddHolders`這個集合,這時又執行了一次`animateAdd`會出現什么情況?前面的又重復執行了一次動畫,在`DefaultItemAnimator`中巧妙的解決了這個問題,和上面4個變量有關,在下面的代碼中,我們也會借鑒這種方式實現。
開始代碼之前,我們先來看看`isRunning`這個方法該怎么寫。
~~~
@Override
public boolean isRunning() {
return !(mPendingAddHolders.isEmpty()
&& mPendingRemoveHolders.isEmpty()
&& mAddAnimtions.isEmpty()
&& mRemoveAnimations.isEmpty());
}
~~~
不多說,只有一句話:`isRunning`不是表示有沒有動畫要執行嘛。
那繼續代碼,來看看`animateAdd`和`animateRemove`方法怎么寫的。
~~~
@Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
holder.itemView.setAlpha(0.f);
mPendingAddHolders.add(holder);
return true;
}
@Override
public boolean animateRemove(RecyclerView.ViewHolder holder) {
mPendingRemoveHolders.add(holder);
return true;
}
~~~
上面說了,這里我們僅僅是向集合中添加一個holder,并且將返回值置為`true`,表示可以去執行`runPendingAnimations`。這里需要注意的就是`animateAdd`方法的第一行代碼,我們將view設置不可見,這樣做的目的是防止item閃動(出現后才去執行動畫)。
那接下來就是重頭戲了:`runPendingAnimations`:
~~~
@Override
public void runPendingAnimations() {
boolean isRemove = !mPendingRemoveHolders.isEmpty();
boolean isAdd = !mPendingAddHolders.isEmpty();
if(!isRemove && !isAdd) return;
// first remove
if(isRemove) {
for(RecyclerView.ViewHolder holder : mPendingRemoveHolders) {
animateRemoveImpl(holder);
}
mPendingRemoveHolders.clear();
}
// last add
if(isAdd) {
ArrayList<RecyclerView.ViewHolder> holders = new ArrayList<>();
holders.addAll(mPendingAddHolders);
mPendingAddHolders.clear();
for(RecyclerView.ViewHolder holder : holders) {
animateAddImpl(holder);
}
holders.clear();
}
}
~~~
解釋一下代碼,首先兩個變量,判斷兩個pending集合是否不為空,這里決定著我們的代碼是否有必要往下執行。然后去判斷`isRemove`,在這里面去執行`remove`的動畫,
可以看到,我們遍歷出保存的每一個ViewHolder,然后去執行`animateRemoveImpl`方法,最后將`mPendingRemoveHolders`清空。
`animateRemoveImpl`我們先不去管它,繼續看看`add`,這里首先將`mPendingAddHolders`中的所有holder添加到了一個局部List中,然后清空,
這樣做的目的是防止動畫的重復執行,接著和remove的時候一樣,去遍歷所有的holder執行`animateAddImpl`方法,最后的最后,將局部的list清空。
那接下來,我們就要去看看`animateRemoveImpl`和`animateAddImpl`方法了,這兩個方法才是真正執行動畫的地方。
~~~
// 執行添加動畫
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
mAddAnimtions.add(holder);
final View item = holder.itemView;
ObjectAnimator animator = ObjectAnimator.ofFloat(item, "alpha", 0.f, 1.f);
animator.setDuration(1000);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchAddStarting(holder);
}
@Override
public void onAnimationCancel(Animator animation) {
item.setAlpha(1.f);
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchAddFinished(holder);
mAddAnimtions.remove(holder);
if (!isRunning()) dispatchAnimationsFinished();
}
});
animator.start();
}
// 執行移出動畫
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
mRemoveAnimations.add(holder);
final View item = holder.itemView;
ObjectAnimator animator = ObjectAnimator.ofFloat(item, "alpha", 1.f, 0.f);
animator.setDuration(1000);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchRemoveStarting(holder);
}
@Override
public void onAnimationEnd(Animator animation) {
mRemoveAnimations.remove(holder);
item.setAlpha(1.f);
dispatchRemoveFinished(holder);
if (!isRunning()) dispatchAnimationsFinished();
}
});
animator.start();
}
~~~
可以看到這兩個方法非常類似,所以我們只說其中的一個,恩,就說離我最近的這個`animateRemoveImpl`吧。首先將這個holder添加到`mRemoveAnimations`中,然后一段大家非常熟悉的屬性動畫,這里我們僅僅在動畫中改變了itemView的`alpha`值,我們按照文檔上說的在動畫開始的時候調用`dispatchRemoveStarting`方法,在動畫結束的石斛調用`dispatchRemoveFinished`方法,最后還去判斷了一下有沒有執行的動畫,如果沒有,調用一下`dispatchAddFinished`。這里所做的一切都是按照文檔的規定來的。
好激動,終于實現了`RecyclerView`的動畫,來使用一下我們的ItemAnimator
~~~
...
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setItemAnimator(new MyItemAnimator());
~~~
看看效果:

從效果中可以看到,我們的`add`動畫是沒有問題的,但尼瑪`remove`動畫絕壁不是我們想要的效果!!這也太…了吧。
恩,在仔細觀察了1min效果后,我終于發現問題所在了,偷偷告訴你:
> 在我們remove的時候,下面的item是不是往上移動了? 移動了, 那是不是要執行`animateMove`方法?
所以,我們還需要一套`move`的處理過程。
首先加一對集合。
~~~
private ArrayList<MoveInfo> mPendingMoveHolders =
new ArrayList<>();
private ArrayList<MoveInfo> mMoveAnimtions = new ArrayList<>();
~~~
又是一對好基友,哦,不對,是好情侶。哎?這個`MoveInfo`是啥? 仔細想想,一個移動的過程,是不是需要知道`來自哪里,將要去何方`。我們模仿`DefaultItemAnimator`來定義一個內部類`MoveInfo`。
~~~
class MoveInfo {
private RecyclerView.ViewHolder holder;
private int fromX;
private int fromY;
private int toX;
private int toY;
public MoveInfo(RecyclerView.ViewHolder holder,
int fromX, int fromY, int toX, int toY) {
this.holder = holder;
this.fromX = fromX;
this.fromY = fromY;
this.toX = toX;
this.toY = toY;
}
}
~~~
恩,沒啥好說的, 那就繼續看`animateMove`方法吧,肯定是向`mPendingMoveHolders`中添加一個,不過這里添加的就是一個`MoveInfo`了。
~~~
@Override
public boolean animateMove(RecyclerView.ViewHolder holder,
int fromX, int fromY, int toX, int toY) {
View view = holder.itemView;
fromY += view.getTranslationY();
int delta = toY - fromY;
view.setTranslationY(-delta);
MoveInfo info = new MoveInfo(holder, fromX, fromY, toX, toY);
mPendingMoveHolders.add(info);
return true;
}
~~~
值得一提的是`int delta = toY - fromY`我們去計算了改view需要移動的距離,然后取反塞給`view.translationY`,這樣做的目的是讓上面一個在移出的過程中,下面的item不會立馬移動上去。而是偏移一定的距離。這里為了好理解,我們打印一組值來幫我我們理解
> fromY: 20, toY: 0
delta: -20
translationY: 20
所以在這個場景下,首先將view向下移動了20個像素,效果就是它在原來的位置不動。往下走,構造了一個`MoveInfo`并添加到`mPendingMoveHolders`里。
繼續修改`runPendingAnimations`方法,將我們的move操作加上,這部分代碼和`add`的代碼非常相似,完全可以copy過來修改修改。
~~~
@Override
public void runPendingAnimations() {
boolean isRemove = !mPendingRemoveHolders.isEmpty();
boolean isMove = !mPendingMoveHolders.isEmpty();
boolean isAdd = !mPendingAddHolders.isEmpty();
if(!isRemove && !isMove && !isAdd) return;
...
// then move
if(isMove) {
ArrayList<MoveInfo> infos = new ArrayList<>();
infos.addAll(mPendingMoveHolders);
mPendingMoveHolders.clear();
for(MoveInfo info : infos) {
animateMoveImpl(info);
}
infos.clear();
}
...
// last add
}
~~~
可以看到我們move的處理完全就是add的翻版,如果不理解,可以網上翻翻博客,看看add部分的說明。繼續來到`animateMoveImpl`方法。
~~~
// 執行移動動畫
private void animateMoveImpl(final MoveInfo info) {
mMoveAnimtions.remove(info);
final View view = info.holder.itemView;
ObjectAnimator animator = ObjectAnimator.ofFloat(view,
"translationY", view.getTranslationY(), 0);
animator.setDuration(1000);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
dispatchMoveStarting(info.holder);
}
@Override
public void onAnimationEnd(Animator animation) {
dispatchMoveFinished(info.holder);
mMoveAnimtions.remove(info.holder);
if(!isRunning()) dispatchAnimationsFinished();
}
});
animator.start();
}
~~~
這里面我們構造了一個`translationY`的動畫,效果是從該view當前的偏移量到0的一個不斷偏移效果。說白了就是不斷往上的效果。在動畫開始和結束中的處理和add是一樣的邏輯。
現在代碼終于完成了,效果就是博客剛開始的那個效果。
說起來,到現在我們唯一一個沒有實現的效果就是`change`的效果了,其實`change`的處理和`move`的處理也很相似,感興趣的朋友可以去參考一下`DefaultItemAnimator`的源碼。看起來,自己去實現一個`RecyclerView`的item動畫也不是那么的復雜,但是代碼量也不少,那是不是我們每次需要一種效果都要寫這么長的代碼? 當然不是!仔細觀察代碼,其實動畫的實現都是在`animateXXXImpl`中實現的,我們完全可以把`aninateXXXImpl`抽象出來,需要什么動畫,我們就繼承這個類,僅僅去實現`aninateXXXImpl`中的代碼就可以,當然,別人也給我們提供好了很多動畫庫,我們也完全可以不用自己去寫,直接使用三方的。下面就推薦一個做的非常棒的`RecyclerView`動畫庫,直接copy到項目里就可以使用。
[github上的RecyclerView item動畫庫](https://github.com/wasabeef/recyclerview-animators)
最后是demo的下載地址:
[demo下載,戳這里](http://download.csdn.net/detail/qibin0506/8954799)
*注意:demo中的屬性動畫,如果需要向下兼容,可以換用`nineoldandroids`或者`ViewCompat`實現。*
- 前言
- 高逼格UI-ASD(Android Support Design)
- AndroidSupportDesign之TabLayout使用詳解
- RecyclerView的高級用法——定制動畫
- Android官方數據綁定框架DataBinding(一)
- Android官方數據綁定框架DataBinding(二)
- 你所不知道的Activity轉場動畫——ActivityOptions
- RecyclerView+ImageLoader打造多選圖庫
- Android Material Design動畫
- RecyclerView添加Header的正確方式
- CoordinatorLayout高級用法-自定義Behavior