LoginSignup
1
1

More than 5 years have passed since last update.

LayerDrawable#setTintListが機能しない件

Last updated at Posted at 2017-02-07

Lollipop以降に追加されたLayerDrawable#setTintList()ですが呼び出しても機能しません。

サンプル

原因

AOSPのソースを追ってみた所、LayerDrawable#isStateful()が古い結果をキャッシュし続けていてViewがDrawable#setState()を呼ばないのが原因のようです。
LayerDrawable#setTintList()の実装にキャッシュを無効化するコードが入ってないのでStatefulなはずなのにisStateful()がfalseを返してしまいます。

LayerDrawable.java
    public void setTintList(ColorStateList tint) {
        final ChildDrawable[] array = mLayerState.mChildren;
        final int N = mLayerState.mNum;
        for (int i = 0; i < N; i++) {
            final Drawable dr = array[i].mDrawable;
            if (dr != null) {
                dr.setTintList(tint);
            }
        }
        // ← Statefulキャッシュを無効化してない
    }

    public boolean isStateful() {
        return mLayerState.isStateful();
    }

    static class LayerState extends ConstantState {
        public final boolean isStateful() {
            if (mHaveIsStateful) { // ← こいつがキャッシュを保持し続ける
                return mIsStateful;
            }

            final ChildDrawable[] array = mChildren;
            final int N = mNum;
            boolean isStateful = false;
            for (int i = 0; i < N; i++) {
                final Drawable dr = array[i].mDrawable;
                if (dr != null && dr.isStateful()) {
                    isStateful = true;
                    break;
                }
            }

            mIsStateful = isStateful;
            mHaveIsStateful = true;
            return isStateful;
        }
    }

View.java
                if (mBackground.isStateful()) {
                    mBackground.setState(getDrawableState()); // ← isStateful()がtrueじゃない限りDrawable#setState()を呼んでくれない
                }

回避方法

statefulキャッシュを無効化するにはLayerDrawable.LayerState#invalidateCache()を呼べばいいのですがそれを呼んでいるはLayerDrawable#setDrawable()とLayerDrawable#setDrawableByLayerId()です。

なのでsetTintList()を呼ぶ直前に上記のAPIを呼べばisStateful()は正しい結果を返して問題は解決します。

いちいち書くのは面倒なので簡単なUtilityクラスを用意しました。ソースにも書いてますがlayerの最初のitemにandroid:idの設定が必要になります。

LayerDrawableUtil.java
public class LayerDrawableUtil {
    public static void invalidateCache(Drawable d) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
        if (!(d instanceof LayerDrawable)) return;

        LayerDrawable ld = (LayerDrawable) d;
        int count = ld.getNumberOfLayers();
        if (count == 0) return;

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            // TODO first layer item needs to be set android:id
            int id = ld.getId(0);
            Drawable d0 = ld.findDrawableByLayerId(id);
            ld.setDrawableByLayerId(id, d0);
        } else {
            Drawable d0 = ld.getDrawable(0);
            ld.setDrawable(0, d0);
        }
    }
}

これを下のように使います。

MainActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Not Worked
        Button button1 = (Button) findViewById(R.id.button1);
        ViewCompat.setBackgroundTintList(button1, ContextCompat.getColorStateList(this, R.color.tint));

        // Worked
        Button button2 = (Button) findViewById(R.id.button2);
        LayerDrawableUtil.invalidateCache(button2.getBackground());
        ViewCompat.setBackgroundTintList(button2, ContextCompat.getColorStateList(this, R.color.tint));
    }

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1