Android Drawable / DrawableCompat # setTintList( ) 使用時一個值得注意的問題

本文還是接續上兩篇文章,繼續聊運行時動態處理按鈕點擊狀態的問題,在評論區的指引之下,本文是這個問題第三種解決方案,應該也是三個中最合理的方案。這三篇依次看下來,可以看到解決一個問題走過的彎路:

  1. Android 運行時給動態加載的圖標按鈕添加點擊效果
  2. Android 按鈕 pressed 狀態的顯示時機 (附少許源碼分析)
  3. 本文

也許本文還是彎路(或許再探索一下源碼還可以發現更好的方案),不過走彎路也不是完全沒有意義,走彎路過程中用到的一些方法也許可以在其他的場景下提供一些啟發。

第一篇文章介紹了一種裁剪蒙層圖、生成StateListDrawable的方式;第二篇講了著色的方式,但是需要自己設置OnTouchListener來確定著色的時機;本文介紹的方式利用系統View源碼中自己的狀態轉換機制,使用DrawableCompat.setTintList() 來完成各個狀態(這里的需求主要是pressed 狀態)下的著色。本文主要說一下這種方式在處理pressed狀態時一個需要注意的問題。

Drawable 類的 setTintList() 和 setTintMode() 方法都是在API level 21 才引入的方法。所以要兼容低版本需要使用DrawableCompat.setTintList()這個靜態方法。我們這里要處理的是pressed狀態,代碼大致如下:

Drawable drawable = new BitmapDrawable(context.getResources(), bitmap); // bitmap從服務端加載而來

int[][] colorStates = new int[][] {
        new int[] {android.R.attr.state_pressed},
        new int[] { }
};

int[] colors = new int[]{context.getResources().getColor(pressTintColorId), Color.TRANSPARENT};
ColorStateList colorStateList = new ColorStateList(colorStates, colors);

finalDrawable = DrawableCompat.wrap(drawable);
finalDrawable = finalDrawable.mutate();
DrawableCompat.setTintList(finalDrawable, colorStateList);
DrawableCompat.setTintMode(finalDrawable, PorterDuff.Mode.SRC_ATOP);

問題所在

這段代碼跑在API 21 及以上的設備上時,在按下按鈕后并沒有著色效果。

我們來看一下源碼。

這種方式設置了一個ColorStateList ,因此需要View在被按下時在pressed state下使用tint color。查看View類源碼,setPressed() 方法調用了refreshDrawableState()方法,refreshDrawableState()方法里調用了drawableStateChanged(), drawableStateChanged() 里 則調用了 Drawable 的 setState() 方法。我們來看一些setState() 方法源碼:

Drawable.java 源碼片段

看來主要在onStateChange(),它的實現在各個子類里,我們這里只看BitmapDrawable:

BitmapDrawable.java 源碼片段

updateTintFilter() 的實現又回到父類Drawable 類:

Drawable.java 源碼片段

可以看到,這個方法只是更新了tintFilter的color和mode,并沒有觸發Drawable的繪制。所以,從setPressed() 方法跟蹤到此,發現都沒有觸發Drawable重新繪制的代碼,所以按下時的著色 tint color自然沒有顯示出來。

那為什么在 ** API 21 以下反而是有效的 ** 呢?

再看一下DrawableCompat.java 中的setTintList() 的實現,這里直接給到DrawableCompat的API 21的具體實現類 DrawableCompatLollipop:

DrawableCompatLollipop.java 源碼片段

可以看到,兩個方法在API 21 及以上都是直接調用了Drawable 自己的方法setTintList() 和 setTintMode() ;而在 API 21 以下是調用了DrawableCompatBase里的方法。DrawableCompatBase則調用了DrawableWrapper的方法,DrawableWrapper是一個 Interface,它的方法實現在具體類里,類繼承/實現層級關系如下:

DrawableWrapper 的實現類層級

主要的方法實現都在第一層子類DrawableWrapperDonut中,。看一下DrawableWrapperDonut中的setState() 方法的實現:

DrawableWrapperDonut.java 源碼片段

可以看到調用了updateTint(),就是這個方法觸發Drawable的重新繪制,才能看到pressed state下著色的效果:

DrawableWrapperDonut.java 源碼片段

現在,弄清楚了API 21以下有效的原因,就可以解決API 21及以上無效的問題了。我們也在setState()后觸發一下Drawable重繪。

在上面的分析中,調用棧:View#setPressed() -> View#refreshDrawableState() -> View#drawableStateChanged() -> Drawable#setState() -> BitmapDrawable#onStateChange() 。

那么我們就新定義一個繼承自BitmapDrawable的子類,覆蓋onStateChange() :

@Override
protected boolean onStateChange(int[] stateSet) {
    boolean ret = super.onStateChange(stateSet);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        if (ret) {
            // 觸發Drawable重新繪制,以使利用setTintList()設置的各個狀態下的tint效果得到顯示
            invalidateSelf();
        }
    }

    return ret;
}

然后最上邊 Drawable drawable = new BitmapDrawable(context.getResources(), bitmap); 改為 new 這個自定義的子類的實例即可:

Drawable drawable = new SomeExtendedBitmapDrawable(context.getResources(), bitmap);
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容