2020年3月13日 星期五

[Android] Fragment transition 動畫在 onAnimationEnd 呼叫時動畫並未真正結束

markdown 最近遇到的一個情況是,我在 fragment 進入動畫結束後要初始化一個 RecyclerView,在 fragment 的 onCreateAnimation 中用將 animation listener 監聽 animation end 事件,並且在該事件 callback onAnimationEnd 中進行初始化 list。
@Override
public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
    Animation animation = super.onCreateAnimation(transit, enter, nextAnim);
    if (!enter)
        return animation;

    if (animation == null)
        animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);

    animation.setAnimationListener(new Animation.AnimationListener() {
        @Override
        public void onAnimationStart(Animation animation) {}

        @Override
        public void onAnimationEnd(Animation animation) {
            initialListView();
        }

        @Override
        public void onAnimationRepeat(Animation animation) {}
    });
    return animation;
}
但是發現了一個問題,就是在進入動畫執行完成前,list 就被更新了,而且因為初始化 list 是一個稍微有點耗時的動作,於是就看到進入 fragment 時動畫在快完成時頓住一下,然後動畫才完成並且 list 顯示出來。 因為網路上找不太到答案,就自己看一下 Android 原始碼,發現原來在 onAnimationEnd 呼叫後,確實還會執行最後一幀動畫。下面是 Android Animation class 中的 getTransformation method。
public boolean getTransformation(long currentTime, Transformation outTransformation) {
    // Ignore unrelated code......

    final boolean expired = normalizedTime >= 1.0f || isCanceled();
    mMore = !expired;
    if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
    if ((normalizedTime >= 0.0f || mFillBefore) && 
        (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            fireAnimationStart();
            mStarted = true;
            if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
        }
        if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
        if (mCycleFlip) {
            normalizedTime = 1.0f - normalizedTime;
        }
        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
        applyTransformation(interpolatedTime, outTransformation);
    }
    if (expired) {
        if (mRepeatCount == mRepeated || isCanceled()) {
            if (!mEnded) {
                mEnded = true;
                guard.close();
                // Invoke onAnimationEnd
                fireAnimationEnd();
            }
        } else {
            if (mRepeatCount > 0) {
                mRepeated++;
            }
            if (mRepeatMode == REVERSE) {
                mCycleFlip = !mCycleFlip;
            }
            mStartTime = -1;
            mMore = true;
            fireAnimationRepeat();
        }
    }
    // If mOneMoreTime is true, return true
    if (!mMore && mOneMoreTime) {
        mOneMoreTime = false;
        return true;
    }
    return mMore;
}
從程式碼中可以看到,在呼叫完 onAnimationEnd 後,會去檢查 mOneMoreTime 欄位,如果該欄位是 true,則 return true 告知 caller 動畫還未結束,然後執行最後一幀動畫。為了確認 mOneMoreTime 在 onAnimationEnd 呼叫時是不是 true,我還用反射去讀取這個欄位印出來驗證,結果證實了 mOneMoreTime 的確是 true。
@Override
public void onAnimationEnd(Animation animation) {
    try {
        Field field = animation.getClass().getSuperclass().getDeclaredField("mOneMoreTime");
        field.setAccessible(true);
        boolean b = field.getBoolean(animation);
        mLogger.logd("mOneMoreTime: %b", b);
    } catch (NoSuchFieldException e) {
        mLogger.loge("NoSuchFieldException Get field fail: %s", e.getMessage());
    } catch (IllegalAccessException e) {
        mLogger.loge("IllegalAccessException Get field fail: %s", e.getMessage());
    }

    initialListView();
}
雖然不知道這是 Android 故意這樣設計的還是 Bug,但是從原始碼中可以確認,這個問題跟 app 設計無關,而是系統造成的。也只能找一些 workaround 去繞過他了。 --- * [Android Animation 执行原理](https://juejin.im/post/5acb95b16fb9a028dc4152ba) * [Android Animations Tutorial 7: The secret of fillBefore, fillAfter and fillEnabled](http://graphics-geek.blogspot.com/2011/08/mysterious-behavior-of-fillbefore.html)

沒有留言:

張貼留言