本文還是接續上兩篇文章,繼續聊運行時動態處理按鈕點擊狀態的問題,在評論區的指引之下,本文是這個問題第三種解決方案,應該也是三個中最合理的方案。這三篇依次看下來,可以看到解決一個問題走過的彎路:
也許本文還是彎路(或許再探索一下源碼還可以發現更好的方案),不過走彎路也不是完全沒有意義,走彎路過程中用到的一些方法也許可以在其他的場景下提供一些啟發。
第一篇文章介紹了一種裁剪蒙層圖、生成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() 方法源碼:
看來主要在onStateChange(),它的實現在各個子類里,我們這里只看BitmapDrawable:
updateTintFilter() 的實現又回到父類Drawable 類:
可以看到,這個方法只是更新了tintFilter的color和mode,并沒有觸發Drawable的繪制。所以,從setPressed() 方法跟蹤到此,發現都沒有觸發Drawable重新繪制的代碼,所以按下時的著色 tint color自然沒有顯示出來。
那為什么在 ** API 21 以下反而是有效的 ** 呢?
再看一下DrawableCompat.java 中的setTintList() 的實現,這里直接給到DrawableCompat的API 21的具體實現類 DrawableCompatLollipop:
可以看到,兩個方法在API 21 及以上都是直接調用了Drawable 自己的方法setTintList() 和 setTintMode() ;而在 API 21 以下是調用了DrawableCompatBase里的方法。DrawableCompatBase則調用了DrawableWrapper的方法,DrawableWrapper是一個 Interface,它的方法實現在具體類里,類繼承/實現層級關系如下:
主要的方法實現都在第一層子類DrawableWrapperDonut中,。看一下DrawableWrapperDonut中的setState() 方法的實現:
可以看到調用了updateTint(),就是這個方法觸發Drawable的重新繪制,才能看到pressed state下著色的效果:
現在,弄清楚了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);