寫(xiě)這篇博客是為了記錄一下最近解決的一個(gè)問(wèn)題。其實(shí)這是一個(gè)朋友遇到的問(wèn)題,他想對(duì)RecyclerView的item中的一個(gè)View設(shè)置無(wú)限循環(huán)的動(dòng)畫(huà)(注意,是對(duì)item里的一個(gè)子view設(shè)置動(dòng)畫(huà),不是對(duì)item設(shè)置動(dòng)畫(huà)),但是在RecyclerView滑動(dòng)的時(shí)候,一些item的動(dòng)畫(huà)莫名其妙地停止了,他沒(méi)有找到原因,所以拜托我?guī)兔匆幌隆?/p>
要對(duì)一個(gè)View設(shè)置動(dòng)畫(huà)很簡(jiǎn)單,只要view.setAnimation()傳一個(gè)動(dòng)畫(huà)對(duì)象就可以了。
view.setAnimation(animation);
要對(duì)RecyclerView的item中的一個(gè)View設(shè)置動(dòng)畫(huà),我們很自然的就會(huì)寫(xiě)出下面的代碼。
<?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 + "");
//對(duì)tvPosition執(zhí)行動(dòng)畫(huà)
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); // 無(wú)限循環(huán)
translateAnimation.setDuration(1000);
translateAnimation.setFillAfter(false);
return translateAnimation;
}
運(yùn)行起來(lái),一切正常,但是一滑動(dòng)列表,一些item的動(dòng)畫(huà)就停止了,再i滑動(dòng)一下動(dòng)畫(huà)又執(zhí)行了,而且那個(gè)item會(huì)執(zhí)行動(dòng)畫(huà),那個(gè)item會(huì)停止動(dòng)畫(huà),沒(méi)有一定的規(guī)律。這就是前面說(shuō)的那位朋友所遇到的問(wèn)題。
點(diǎn)進(jìn)View的源碼,追蹤一下設(shè)置進(jìn)去的Animation對(duì)象,發(fā)現(xiàn)View在屏幕中移除的時(shí)候(Detached),會(huì)把Animation對(duì)象置空,導(dǎo)致View動(dòng)畫(huà)停止。
//setAnimation時(shí),View會(huì)把設(shè)置的動(dòng)畫(huà)對(duì)象保存到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();
}
}
//這個(gè)方法在View從屏幕中移除時(shí)執(zhí)行。
@CallSuper
protected void onDetachedFromWindowInternal() {
//去掉了無(wú)關(guān)的代碼
// 把mCurrentAnimation置空
mCurrentAnimation = null;
}
這就是item中的動(dòng)畫(huà)莫名其妙停止的原因。RecyclerView滑動(dòng)時(shí),滑出屏幕的item會(huì)從屏幕中移除(Detached),導(dǎo)致mCurrentAnimation對(duì)象置空,動(dòng)畫(huà)停止。那么當(dāng)item滑動(dòng)進(jìn)屏幕時(shí),不是會(huì)執(zhí)行onBindViewHolder重新設(shè)置動(dòng)畫(huà)嗎?為什么會(huì)有一些item重新設(shè)置了動(dòng)畫(huà),而有一些item沒(méi)有重新設(shè)置動(dòng)畫(huà)呢?
很多人認(rèn)為RecyclerView的item顯示的時(shí)候(Attached)就會(huì)執(zhí)行onBindViewHolder綁定數(shù)據(jù)。其實(shí)不然,RecyclerView的四級(jí)緩存中,其中有一個(gè)mCachedViews列表,緩存的是剛從屏幕移除的ViewHolder(已經(jīng)Detached),復(fù)用這里的ViewHolder不會(huì)重新執(zhí)行onBindViewHolder。也就是說(shuō)item Detached時(shí)動(dòng)畫(huà)置空,而Attached時(shí)可能不會(huì)回調(diào)onBindViewHolder重新設(shè)置動(dòng)畫(huà)。
找到了問(wèn)題所在,要修改這個(gè)bug就很簡(jiǎn)單了,我們應(yīng)該在item Attach到屏幕時(shí)設(shè)置動(dòng)畫(huà),而不是在onBindViewHolder里設(shè)置。
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
if (holder.itemView.getTag() != null){
holder.itemView.removeOnAttachStateChangeListener((View.OnAttachStateChangeListener)holder.itemView.getTag()); //移除舊的監(jiān)聽(tīng)器
}
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); // 保存監(jiān)聽(tīng)器對(duì)象。
holder.tvPosition.setText(position + "");
}
要注意監(jiān)聽(tīng)器的添加和移除。
運(yùn)行一下,完美解決問(wèn)題,不會(huì)再有item因?yàn)榛瑒?dòng)導(dǎo)致動(dòng)畫(huà)停止。
或許有些同學(xué)會(huì)說(shuō),給View設(shè)置動(dòng)畫(huà)不一定要用setAnimation()方法,使用屬性動(dòng)畫(huà)也可以很方便的實(shí)現(xiàn),就像下面這樣。
@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); // 無(wú)限循環(huán)
animator.setRepeatMode(ValueAnimator.RESTART);
animator.start();
holder.tvPosition.setText(position + "");
}
運(yùn)行起來(lái),動(dòng)畫(huà)正常執(zhí)行?;瑒?dòng)列表,動(dòng)畫(huà)也不會(huì)意外停止,似乎完美的實(shí)現(xiàn)了功能。
這樣寫(xiě)真的沒(méi)有問(wèn)題嗎?我們給animator設(shè)置一下監(jiān)聽(tīng)器,在動(dòng)畫(huà)重復(fù)執(zhí)行時(shí)打印一下log。
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationRepeat(Animator animation) {
Log.d("TAG", "onAnimationRepeat");
super.onAnimationRepeat(animation);
}
});
這時(shí)候會(huì)發(fā)現(xiàn),但item滑出屏幕(Detached)時(shí),動(dòng)畫(huà)在執(zhí)行,但頁(yè)面關(guān)閉時(shí),動(dòng)畫(huà)還在執(zhí)行。由于animator持有View,View持有Activity,所以就算頁(yè)面關(guān)閉了,Activity也不會(huì)被回收,這是很嚴(yán)重的內(nèi)存泄露。
所以我們?cè)谑褂脛?dòng)畫(huà)時(shí),無(wú)論是在Activity/Fragment,還是在列表執(zhí)行一個(gè)長(zhǎng)時(shí)間的動(dòng)畫(huà),一定要在適當(dāng)?shù)臅r(shí)候(如:onViewDetachedFromWindow、onDestroy)停止動(dòng)畫(huà),否則會(huì)導(dǎo)致內(nèi)存泄露。這也是Android源碼中,在View Detach時(shí)將動(dòng)畫(huà)置空的原因。