本文還是接續(xù)上兩篇文章,繼續(xù)聊運(yùn)行時動態(tài)處理按鈕點(diǎn)擊狀態(tài)的問題,在評論區(qū)的指引之下,本文是這個問題第三種解決方案,應(yīng)該也是三個中最合理的方案。這三篇依次看下來,可以看到解決一個問題走過的彎路:
也許本文還是彎路(或許再探索一下源碼還可以發(fā)現(xiàn)更好的方案),不過走彎路也不是完全沒有意義,走彎路過程中用到的一些方法也許可以在其他的場景下提供一些啟發(fā)。
第一篇文章介紹了一種裁剪蒙層圖、生成StateListDrawable的方式;第二篇講了著色的方式,但是需要自己設(shè)置OnTouchListener來確定著色的時機(jī);本文介紹的方式利用系統(tǒng)View源碼中自己的狀態(tài)轉(zhuǎn)換機(jī)制,使用DrawableCompat.setTintList() 來完成各個狀態(tài)(這里的需求主要是pressed 狀態(tài))下的著色。本文主要說一下這種方式在處理pressed狀態(tài)時一個需要注意的問題。
Drawable 類的 setTintList() 和 setTintMode() 方法都是在API level 21 才引入的方法。所以要兼容低版本需要使用DrawableCompat.setTintList()這個靜態(tài)方法。我們這里要處理的是pressed狀態(tài),代碼大致如下:
Drawable drawable = new BitmapDrawable(context.getResources(), bitmap); // bitmap從服務(wù)端加載而來
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 及以上的設(shè)備上時,在按下按鈕后并沒有著色效果。
我們來看一下源碼。
這種方式設(shè)置了一個ColorStateList ,因此需要View在被按下時在pressed state下使用tint color。查看View類源碼,setPressed() 方法調(diào)用了refreshDrawableState()方法,refreshDrawableState()方法里調(diào)用了drawableStateChanged(), drawableStateChanged() 里 則調(diào)用了 Drawable 的 setState() 方法。我們來看一些setState() 方法源碼:

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

updateTintFilter() 的實(shí)現(xiàn)又回到父類Drawable 類:

可以看到,這個方法只是更新了tintFilter的color和mode,并沒有觸發(fā)Drawable的繪制。所以,從setPressed() 方法跟蹤到此,發(fā)現(xiàn)都沒有觸發(fā)Drawable重新繪制的代碼,所以按下時的著色 tint color自然沒有顯示出來。
那為什么在 ** API 21 以下反而是有效的 ** 呢?
再看一下DrawableCompat.java 中的setTintList() 的實(shí)現(xiàn),這里直接給到DrawableCompat的API 21的具體實(shí)現(xiàn)類 DrawableCompatLollipop:

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

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

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

現(xiàn)在,弄清楚了API 21以下有效的原因,就可以解決API 21及以上無效的問題了。我們也在setState()后觸發(fā)一下Drawable重繪。
在上面的分析中,調(diào)用棧: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) {
// 觸發(fā)Drawable重新繪制,以使利用setTintList()設(shè)置的各個狀態(tài)下的tint效果得到顯示
invalidateSelf();
}
}
return ret;
}
然后最上邊 Drawable drawable = new BitmapDrawable(context.getResources(), bitmap); 改為 new 這個自定義的子類的實(shí)例即可:
Drawable drawable = new SomeExtendedBitmapDrawable(context.getResources(), bitmap);