2016年1月16日土曜日

Android 6.0(M)で oneShot の AnimationDrawable の状態がリセットされる

後方互換性がなくなるのはつらいでござるよ...

oneShotAnimationDrawable をアニメーションさせると最後のフレームのままになります。6.0 未満では画面遷移(別のActivityを開くとかホームボタンを押すとか)して戻ってきても最後のフレームのままでしたが、6.0 から最初のフレームに戻ってしまうようになっています。

M preview のときに Issue↓ が立っていて、Status は Fixedなのですが直っていません(つらい)。

Issue3122 AnimationDrawable (oneShot) end state broken

6.0 で AnimationDrawable のコードが少し変わっていますが、直接的な原因はそこではなく setVisible() メソッドの呼ばれ方が変わったことが影響しています。

5系で実行すると以下のタイミングで setVisible() が呼ばれます。

1. 表示されたとき setVisible(true, false)

一方、6系で実行すると以下のタイミングで setVisible() が呼ばれます。

1. 表示されたとき setVisible(true, false)
2. 他の画面に遷移したとき setVisible(false, false)
3. 戻ってきて表示されたとき setVisible(true, false)

5系だと最初に表示されたときだけ setVisible() が呼ばれるのですが、6系だと別の画面に遷移したときと、戻ってきたときも setVisible() が呼ばれるようになっています。
setVisible() のコードをみると次のようになっています。 @Override public boolean setVisible(boolean visible, boolean restart) { final boolean changed = super.setVisible(visible, restart); if (visible) { if (restart || changed) { boolean startFromZero = restart || !mRunning || mCurFrame >= mAnimationState.getChildCount(); setFrame(startFromZero ? 0 : mCurFrame, true, mAnimating); } } else { unscheduleSelf(this); } return changed; } 別の画面に遷移したとき visible = false で setVisible() が呼ばれるので unscheduleSelf() されてしまい、ここで mRunning が falseになるので、戻ってきたとき(changed = true)に setFrame(0) されてしまうのが原因です。

そこで、oneShotのときは visible = false のときに unscheduleSelf() を呼ばないようにしたサブクラス(AnimationDrawableForM)を用意し、6.0以降の場合はこれを使うようにしてみました。 public class AnimationDrawableCompat { public static AnimationDrawable getAnimationDrawable(Context context, int resId) { AnimationDrawable d = (AnimationDrawable) ContextCompat.getDrawable(context, resId); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return d; } AnimationDrawableForM dm = new AnimationDrawableForM(); dm.setOneShot(d.isOneShot()); final int count = d.getNumberOfFrames(); for (int i = 0; i < count; i++) { dm.addFrame(d.getFrame(i), d.getDuration(i)); } return dm; } public static class AnimationDrawableForM extends AnimationDrawable { private boolean visible = true; @Override public boolean setVisible(boolean visible, boolean restart) { if (this.visible) { this.visible = visible; return super.setVisible(visible, restart); } else { this.visible = visible; return true; } } @Override public void unscheduleSelf(Runnable what) { if (visible || !isOneShot()) { super.unscheduleSelf(what); } } } } しかし、何回も画面遷移するとまだ状態がリセットされてしまうので、フレーム位置を保持するような方法に変えました。 public class AnimationDrawableCompat { public static AnimationDrawable getAnimationDrawable(Context context, int resId) { AnimationDrawable d = (AnimationDrawable) ContextCompat.getDrawable(context, resId); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return d; } AnimationDrawableForM dm = new AnimationDrawableForM(); dm.setOneShot(d.isOneShot()); final int count = d.getNumberOfFrames(); for (int i = 0; i < count; i++) { dm.addFrame(d.getFrame(i), d.getDuration(i)); } return dm; } public static class AnimationDrawableForM extends AnimationDrawable { private int lastFrame = 0; @Override public boolean setVisible(boolean visible, boolean restart) { final int idx = lastFrame; boolean changed = super.setVisible(visible, restart); if (visible && changed && idx > 0) { selectDrawable(getNumberOfFrames() - 1); } return changed; } @Override public boolean selectDrawable(int idx) { lastFrame = idx; return super.selectDrawable(idx); } } } もしくはリフレクションで問題の mRunning と mCurFrame を書き換えるという方法でも対処できます。 public class AnimationDrawableCompat { public static AnimationDrawable getAnimationDrawable(Context context, int resId) { AnimationDrawable d = (AnimationDrawable) ContextCompat.getDrawable(context, resId); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return d; } AnimationDrawableForM dm = new AnimationDrawableForM(); dm.setOneShot(d.isOneShot()); final int count = d.getNumberOfFrames(); for (int i = 0; i < count; i++) { dm.addFrame(d.getFrame(i), d.getDuration(i)); } return dm; } public static class AnimationDrawableForM extends AnimationDrawable { private int lastFrame = 0; @Override public boolean setVisible(boolean visible, boolean restart) { final int idx = lastFrame; if (visible && idx > 0 && isOneShot() && !isRunning()) { try { // set mRunning to true Field mRunning = getClass().getSuperclass().getDeclaredField("mRunning"); mRunning.setAccessible(true); mRunning.set(this, true); // set mCurFrame to lastFrame Field mCurFrame = getClass().getSuperclass().getDeclaredField("mCurFrame"); mCurFrame.setAccessible(true); mCurFrame.set(this, idx); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return super.setVisible(visible, restart); } @Override public boolean selectDrawable(int idx) { lastFrame = idx; return super.selectDrawable(idx); } } } 根本的に直してほしい...



0 件のコメント:

コメントを投稿