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); } } } 根本的に直してほしい...



2016年1月14日木曜日

一部の文字サイズを変えるだけの場合は RelativeSizeSpan が便利

相対文字サイズを変えるだけなら RelativeSizeSpan が便利です。 SpannableStringBuilder sb = new SpannableStringBuilder(); sb.append("Hello "); int start = sb.length(); sb.append("Android"); sb.setSpan(new RelativeSizeSpan(1.5f), start, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); TextView textView = (TextView) findViewById(R.id.text); textView.setText(sb);

2015年12月27日日曜日

Android 5.0未満では StateListDrawable に追加した Drawable の colorFilter は適用されない

5.0 未満では次のようなコードを実行しても赤色になりませんでしたが、5.0以降で実行すると赤色になります。 Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_launcher); drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{}, drawable); setBackground(stateListDrawable);
- 解説

原因は DrawableContainer の selectDrawable() での処理です。 5.0 から ColorFilter や tint を考慮するようになり、state に対応する Drawable に明示的に ColorFilter や tint をセットするようになりました。

4.4.0 の DrawableContainer.selectDrawable()
306 public boolean selectDrawable(int idx) { ... 336 if (d != null) { 337 d.mutate(); 338 if (mDrawableContainerState.mEnterFadeDuration > 0) { 339 mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; 340 } else { 341 d.setAlpha(mAlpha); 342 } 343 d.setVisible(isVisible(), true); 344 d.setDither(mDrawableContainerState.mDither); 345 d.setColorFilter(mColorFilter); 346 d.setState(getState()); 347 d.setLevel(getLevel()); 348 d.setBounds(getBounds()); 349 d.setLayoutDirection(getLayoutDirection()); 350 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored); 351 } 5.0.1 の DrawableContainer.selectDrawable()
412 public boolean selectDrawable(int idx) { ... 442 if (d != null) { 443 d.mutate(); 444 if (mDrawableContainerState.mEnterFadeDuration > 0) { 445 mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration; 446 } else if (mHasAlpha) { 447 d.setAlpha(mAlpha); 448 } 449 if (mDrawableContainerState.mHasColorFilter) { 450 // Color filter always overrides tint. 451 d.setColorFilter(mDrawableContainerState.mColorFilter); 452 } else { 453 if (mDrawableContainerState.mHasTintList) { 454 d.setTintList(mDrawableContainerState.mTintList); 455 } 456 if (mDrawableContainerState.mHasTintMode) { 457 d.setTintMode(mDrawableContainerState.mTintMode); 458 } 459 } 460 d.setVisible(isVisible(), true); 461 d.setDither(mDrawableContainerState.mDither); 462 d.setState(getState()); 463 d.setLevel(getLevel()); 464 d.setBounds(getBounds()); 465 d.setLayoutDirection(getLayoutDirection()); 466 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);

- 5.0 未満でどうするか

以下のコードでは、デフォルト用の Drawable からタップしたとき用の ColorFilter がかかった Drawable を用意し、StateListDrawable を構成して背景にセットしています。 このコードを 5.0 以降で実行すると思ったように動きますが、5.0 未満だとタップしたときに色が変わりません。 Drawable drawable = ContextCompat.getDrawable(context, R.drawable.ic_launcher); Drawable pressedDrawable = drawable.getConstantState().newDrawable().mutate(); pressedDrawable.setColorFilter(pressedColor, PorterDuff.Mode.SRC_ATOP); StateListDrawable stateListDrawable = new StateListDrawable(); stateListDrawable.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable); stateListDrawable.addState(new int[]{}, drawable); setBackground(stateListDrawable); しょうがないので、http://stackoverflow.com/questions/7979440/android-cloning-a-drawable-in-order-to-make-a-statelistdrawable-with-filters にあるように次のような StateListDrawable の subclass を用意します。 private static class PressedStateListDrawable extends StateListDrawable { private final int pressedColor; public PressedStateListDrawable(Drawable drawable, int pressedColor) { super(); this.pressedColor = pressedColor; addState(new int[]{android.R.attr.state_pressed}, drawable); addState(new int[]{}, drawable); } @Override protected boolean onStateChange(int[] states) { boolean isPressed = false; for (int state : states) { if (state == android.R.attr.state_pressed) { isPressed = true; break; } } if (isPressed) { setColorFilter(pressedColor, PorterDuff.Mode.SRC_ATOP); } else { clearColorFilter(); } return super.onStateChange(states); } @Override public boolean isStateful() { return true; } } setBackground(new PressedStateListDrawable(drawable, pressedColor));