寫這篇博客是為了記錄一下最近解決的一個問題。其實這是一個朋友遇到的問題,他想對RecyclerView的item中的一個View設置無限循環的動畫(注意,是對item里的一個子view設置動畫,不是對item設置動畫),但是在RecyclerView滑動的時候,一些item的動畫莫名其妙地停止了,他沒有找到原因,所以拜托我幫忙看一下。
要對一個View設置動畫很簡單,只要view.setAnimation()傳一個動畫對象就可以了。
view.setAnimation(animation);
要對RecyclerView的item中的一個View設置動畫,我們很自然的就會寫出下面的代碼。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:layout_marginTop="5dp"
android:paddingBottom="5dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvPosition"
android:layout_width="40dp"
android:layout_height="40dp"
android:textColor="@android:color/white"
android:textSize="14sp"
android:gravity="center"
android:background="@android:color/holo_red_dark" />
</LinearLayout>
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.tvPosition.setText(position + "");
//對tvPosition執行動畫
holder.tvPosition.setAnimation(initAnimation(-120, 1200));
}
private TranslateAnimation initAnimation(float start, float end) {
TranslateAnimation translateAnimation = new TranslateAnimation(start, end, 0, 0);
translateAnimation.setRepeatMode(ValueAnimator.RESTART);
translateAnimation.setRepeatCount(ValueAnimator.INFINITE); // 無限循環
translateAnimation.setDuration(1000);
translateAnimation.setFillAfter(false);
return translateAnimation;
}
運行起來,一切正常,但是一滑動列表,一些item的動畫就停止了,再i滑動一下動畫又執行了,而且那個item會執行動畫,那個item會停止動畫,沒有一定的規律。這就是前面說的那位朋友所遇到的問題。
點進View的源碼,追蹤一下設置進去的Animation對象,發現View在屏幕中移除的時候(Detached),會把Animation對象置空,導致View動畫停止。
//setAnimation時,View會把設置的動畫對象保存到mCurrentAnimation。
public void setAnimation(Animation animation) {
mCurrentAnimation = animation;
if (animation != null) {
if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
&& animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
}
animation.reset();
}
}
//這個方法在View從屏幕中移除時執行。
@CallSuper
protected void onDetachedFromWindowInternal() {
//去掉了無關的代碼
// 把mCurrentAnimation置空
mCurrentAnimation = null;
}
這就是item中的動畫莫名其妙停止的原因。RecyclerView滑動時,滑出屏幕的item會從屏幕中移除(Detached),導致mCurrentAnimation對象置空,動畫停止。那么當item滑動進屏幕時,不是會執行onBindViewHolder重新設置動畫嗎?為什么會有一些item重新設置了動畫,而有一些item沒有重新設置動畫呢?
很多人認為RecyclerView的item顯示的時候(Attached)就會執行onBindViewHolder綁定數據。其實不然,RecyclerView的四級緩存中,其中有一個mCachedViews列表,緩存的是剛從屏幕移除的ViewHolder(已經Detached),復用這里的ViewHolder不會重新執行onBindViewHolder。也就是說item Detached時動畫置空,而Attached時可能不會回調onBindViewHolder重新設置動畫。
找到了問題所在,要修改這個bug就很簡單了,我們應該在item Attach到屏幕時設置動畫,而不是在onBindViewHolder里設置。
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
if (holder.itemView.getTag() != null){
holder.itemView.removeOnAttachStateChangeListener((View.OnAttachStateChangeListener)holder.itemView.getTag()); //移除舊的監聽器
}
View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
holder.tvPosition.setAnimation(initAnimation(-120, 1200));
}
@Override
public void onViewDetachedFromWindow(View v) {
}
};
holder.itemView.addOnAttachStateChangeListener(listener);
holder.itemView.setTag(listener); // 保存監聽器對象。
holder.tvPosition.setText(position + "");
}
要注意監聽器的添加和移除。
運行一下,完美解決問題,不會再有item因為滑動導致動畫停止。
或許有些同學會說,給View設置動畫不一定要用setAnimation()方法,使用屬性動畫也可以很方便的實現,就像下面這樣。
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
ObjectAnimator animator = ObjectAnimator.ofFloat(holder.tvPosition, "translationX",-120, 1200);
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE); // 無限循環
animator.setRepeatMode(ValueAnimator.RESTART);
animator.start();
holder.tvPosition.setText(position + "");
}
運行起來,動畫正常執行?;瑒恿斜恚瑒赢嬕膊粫馔馔V?,似乎完美的實現了功能。
這樣寫真的沒有問題嗎?我們給animator設置一下監聽器,在動畫重復執行時打印一下log。
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animation) {
Log.d("TAG", "onAnimationRepeat");
super.onAnimationRepeat(animation);
}
});
這時候會發現,但item滑出屏幕(Detached)時,動畫在執行,但頁面關閉時,動畫還在執行。由于animator持有View,View持有Activity,所以就算頁面關閉了,Activity也不會被回收,這是很嚴重的內存泄露。
所以我們在使用動畫時,無論是在Activity/Fragment,還是在列表執行一個長時間的動畫,一定要在適當的時候(如:onViewDetachedFromWindow、onDestroy)停止動畫,否則會導致內存泄露。這也是Android源碼中,在View Detach時將動畫置空的原因。