Android 在RecyclerView Item中使用動(dòng)畫(huà)所遇到的問(wèn)題。

寫(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à)置空的原因。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 基本使用 自定義分割線 RecyclerView.Adapter常用方法 getItemViewType(int ...
    孤獨(dú)的根號(hào)十二閱讀 827評(píng)論 0 7
  • 一、前言: 1. RecyclerView是什么 從Android 5.0開(kāi)始,谷歌公司推出了一個(gè)用于大量數(shù)據(jù)展示...
    因?yàn)槲业男?/span>閱讀 1,325評(píng)論 2 2
  • 2015年,我來(lái)到上海,開(kāi)始讀研,剛研一的時(shí)候,一周7天,大概上3天課,也就是大家羨慕的上3休4 ,想想是多么的美...
    Alina空靈閱讀 510評(píng)論 0 0
  • 茶源于中國(guó)。但人們對(duì)茶的認(rèn)識(shí)及感悟卻千差萬(wàn)別。不僅國(guó)外如此,就是國(guó)人也是如此。對(duì)一款茶的好壞優(yōu)劣評(píng)判,大都只停留在...
    羽扇閑人閱讀 1,285評(píng)論 0 7
  • 以前覺(jué)得高中的相處方式不好,現(xiàn)在大學(xué)了,來(lái)自全國(guó)各地的人,不好相處是真的,不像高中的時(shí)候那么有禮貌了,對(duì)不起說(shuō)久了...
    璐癡不是路癡閱讀 261評(píng)論 0 0

友情鏈接更多精彩內(nèi)容