2020年3月13日 星期五

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

最近遇到的一個情況是,我在 fragment 進入動畫結束後要初始化一個 RecyclerView,在 fragment 的 onCreateAnimation 中用將 animation listener 監聽 animation end 事件,並且在該事件 callback onAnimationEnd 中進行初始化 list。

  1. @Override
  2. public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) {
  3. Animation animation = super.onCreateAnimation(transit, enter, nextAnim);
  4. if (!enter)
  5. return animation;
  6.  
  7. if (animation == null)
  8. animation = AnimationUtils.loadAnimation(getActivity(), nextAnim);
  9.  
  10. animation.setAnimationListener(new Animation.AnimationListener() {
  11. @Override
  12. public void onAnimationStart(Animation animation) {}
  13.  
  14. @Override
  15. public void onAnimationEnd(Animation animation) {
  16. initialListView();
  17. }
  18.  
  19. @Override
  20. public void onAnimationRepeat(Animation animation) {}
  21. });
  22. return animation;
  23. }

但是發現了一個問題,就是在進入動畫執行完成前,list 就被更新了,而且因為初始化 list 是一個稍微有點耗時的動作,於是就看到進入 fragment 時動畫在快完成時頓住一下,然後動畫才完成並且 list 顯示出來。

因為網路上找不太到答案,就自己看一下 Android 原始碼,發現原來在 onAnimationEnd 呼叫後,確實還會執行最後一幀動畫。下面是 Android Animation class 中的 getTransformation method。

  1. public boolean getTransformation(long currentTime, Transformation outTransformation) {
  2. // Ignore unrelated code......
  3.  
  4. final boolean expired = normalizedTime >= 1.0f || isCanceled();
  5. mMore = !expired;
  6. if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
  7. if ((normalizedTime >= 0.0f || mFillBefore) &&
  8. (normalizedTime <= 1.0f || mFillAfter)) {
  9. if (!mStarted) {
  10. fireAnimationStart();
  11. mStarted = true;
  12. if (NoImagePreloadHolder.USE_CLOSEGUARD) {
  13. guard.open("cancel or detach or getTransformation");
  14. }
  15. }
  16. if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
  17. if (mCycleFlip) {
  18. normalizedTime = 1.0f - normalizedTime;
  19. }
  20. final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
  21. applyTransformation(interpolatedTime, outTransformation);
  22. }
  23. if (expired) {
  24. if (mRepeatCount == mRepeated || isCanceled()) {
  25. if (!mEnded) {
  26. mEnded = true;
  27. guard.close();
  28. // Invoke onAnimationEnd
  29. fireAnimationEnd();
  30. }
  31. } else {
  32. if (mRepeatCount > 0) {
  33. mRepeated++;
  34. }
  35. if (mRepeatMode == REVERSE) {
  36. mCycleFlip = !mCycleFlip;
  37. }
  38. mStartTime = -1;
  39. mMore = true;
  40. fireAnimationRepeat();
  41. }
  42. }
  43. // If mOneMoreTime is true, return true
  44. if (!mMore && mOneMoreTime) {
  45. mOneMoreTime = false;
  46. return true;
  47. }
  48. return mMore;
  49. }

從程式碼中可以看到,在呼叫完 onAnimationEnd 後,會去檢查 mOneMoreTime 欄位,如果該欄位是 true,則 return true 告知 caller 動畫還未結束,然後執行最後一幀動畫。為了確認 mOneMoreTime 在 onAnimationEnd 呼叫時是不是 true,我還用反射去讀取這個欄位印出來驗證,結果證實了 mOneMoreTime 的確是 true。

  1. @Override
  2. public void onAnimationEnd(Animation animation) {
  3. try {
  4. Field field = animation.getClass().getSuperclass().getDeclaredField("mOneMoreTime");
  5. field.setAccessible(true);
  6. boolean b = field.getBoolean(animation);
  7. mLogger.logd("mOneMoreTime: %b", b);
  8. } catch (NoSuchFieldException e) {
  9. mLogger.loge("NoSuchFieldException Get field fail: %s", e.getMessage());
  10. } catch (IllegalAccessException e) {
  11. mLogger.loge("IllegalAccessException Get field fail: %s", e.getMessage());
  12. }
  13.  
  14. initialListView();
  15. }

雖然不知道這是 Android 故意這樣設計的還是 Bug,但是從原始碼中可以確認,這個問題跟 app 設計無關,而是系統造成的。也只能找一些 workaround 去繞過他了。


沒有留言:

張貼留言