2015年12月27日日曜日

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

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

- 解説

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

4.4.0 の DrawableContainer.selectDrawable()
  1. 306     public boolean selectDrawable(int idx) {  
  2.   
  3. 336             if (d != null) {  
  4. 337                 d.mutate();  
  5. 338                 if (mDrawableContainerState.mEnterFadeDuration > 0) {  
  6. 339                     mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;  
  7. 340                 } else {  
  8. 341                     d.setAlpha(mAlpha);  
  9. 342                 }  
  10. 343                 d.setVisible(isVisible(), true);  
  11. 344                 d.setDither(mDrawableContainerState.mDither);  
  12. 345                 d.setColorFilter(mColorFilter);  
  13. 346                 d.setState(getState());  
  14. 347                 d.setLevel(getLevel());  
  15. 348                 d.setBounds(getBounds());  
  16. 349                 d.setLayoutDirection(getLayoutDirection());  
  17. 350                 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);  
  18. 351             }  
5.0.1 の DrawableContainer.selectDrawable()
  1. 412     public boolean selectDrawable(int idx) {  
  2.   
  3. 442             if (d != null) {  
  4. 443                 d.mutate();  
  5. 444                 if (mDrawableContainerState.mEnterFadeDuration > 0) {  
  6. 445                     mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;  
  7. 446                 } else if (mHasAlpha) {  
  8. 447                     d.setAlpha(mAlpha);  
  9. 448                 }  
  10. 449                 if (mDrawableContainerState.mHasColorFilter) {  
  11. 450                     // Color filter always overrides tint.  
  12. 451                     d.setColorFilter(mDrawableContainerState.mColorFilter);  
  13. 452                 } else {  
  14. 453                     if (mDrawableContainerState.mHasTintList) {  
  15. 454                         d.setTintList(mDrawableContainerState.mTintList);  
  16. 455                     }  
  17. 456                     if (mDrawableContainerState.mHasTintMode) {  
  18. 457                         d.setTintMode(mDrawableContainerState.mTintMode);  
  19. 458                     }  
  20. 459                 }  
  21. 460                 d.setVisible(isVisible(), true);  
  22. 461                 d.setDither(mDrawableContainerState.mDither);  
  23. 462                 d.setState(getState());  
  24. 463                 d.setLevel(getLevel());  
  25. 464                 d.setBounds(getBounds());  
  26. 465                 d.setLayoutDirection(getLayoutDirection());  
  27. 466                 d.setAutoMirrored(mDrawableContainerState.mAutoMirrored);  


- 5.0 未満でどうするか

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