最近遇到的一個情況是,我在 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 去繞過他了。