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));