Android 图片着色 Tint 详解
問題描述
在app中可能存在一張圖片只是因為顏色的不同而引入了多張圖片資源的情況。比如 一張右箭頭的圖片,有白色、灰色和黑色三種圖片資源存在。所以我們可不可以只保留一張基礎(chǔ)圖片,在此圖片基礎(chǔ)上只是顏色改變的情況是否可以通過代碼設(shè)置來動態(tài)修改呢?
知識點概覽:
1. setTint、setTintList :對drawable 進行著色。
2. DrawableCompat.wrap: 對drawable 進行包裝,使其可以在不同版本中設(shè)置著色生效。
3. drawable.mutate(): 使drawable 可變,打破其共享資源模式。
4. ConstantState :① 享元模式。② 保存資源信息。③可通過自己創(chuàng)建新的drawable 對象。
初識tint
為了兼容android 的不同版本,google 在DrawableCompat API中提供了著色的相關(guān)方法。
setTint、setTintList
先構(gòu)造好我們的測試demo。提供一個工具類用于對Drawable 進行著色。
(注:為了測試對低版本的兼容,這里使用的測試機型為三星 galaxy s4 android版本為4.4.2)
public class SkxDrawableHelper {
/**
* 對目標Drawable 進行著色
*
* @param drawable 目標Drawable
* @param color 著色的顏色值
* @return 著色處理后的Drawable
*/
public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(wrappedDrawable, color);
return wrappedDrawable;
}
/**
* 對目標Drawable 進行著色
*
* @param drawable 目標Drawable
* @param colors 著色值
* @return 著色處理后的Drawable
*/
public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
// 進行著色
DrawableCompat.setTintList(wrappedDrawable, colors);
return wrappedDrawable;
}
}
測試代碼:
調(diào)用此方法對Deawable進行著色。我們分別對設(shè)置背景的Drawable著色 #30c3a6, 圖片著色為#ff4081
Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
R.drawable.icon_beijing);
mImageView1.setBackground(
SkxDrawableHelper.tintDrawable(originBitmapDrawable,
Color.parseColor("#30c3a6")));
mImageView2.setImageDrawable(
SkxDrawableHelper.tintDrawable(originBitmapDrawable,
Color.parseColor("#ff4081")));
沒有進行著色處理的原效果:
進行著色后的效果如下:
一臉懵逼,這都什么跟什么????。。?我只修改了下面的兩個ImageView,并沒有對上面的兩個ImageView進行修改啊。而且 圖4是怎么出來那么個畸形的。
好吧,一步步來!
DrawableCompat wrap
這里簡單介紹下wrap 這個方法。這個方法的作用是對目標Drawable進行包裝,它可以用于跨越不同的API級別,通過在這個類中的著色方法,簡單來說就是為了兼容不同的版本。如果想對Drawable 進行著色就必須調(diào)用此方法。
* Drawable bg = DrawableCompat.wrap(view.getBackground());
* // Need to set the background with the wrapped drawable
* view.setBackground(bg);
*
* // You can now tint the drawable
* DrawableCompat.setTint(bg, ...);
與wrap 方法對應的有 unwrap(@NonNull Drawable drawable) 方法,用于解除對目標Drawable的包裝。
ConstantState 享元模式
為什么會出現(xiàn)上面出現(xiàn)的這種情況呢?
這里簡單解釋下。不同的Drawble如果加載的是同一個資源,那么將擁有共同的狀態(tài),這是google對Drawable
做的內(nèi)存優(yōu)化。在Drawable 中的表現(xiàn)為 ConstantState,ConstantState是抽象靜態(tài)內(nèi)部類,Drawable
的子類如ColorDrawble,BitmapDrawable 也分別都進行了不同的實現(xiàn)。而在ConstantState
內(nèi)部類中保存的就是Drawable 需要展示的信息,在ColorDrawable 中ConstantState
的實現(xiàn)類是ColorState,其中包含了一些顏色信息;在BitmapDrawable
中ConstantState的實現(xiàn)類是BitmapState,其中包含了Paint,Bitmap,ColorStateList等一些屬性,不同的Drawable子類依靠其對應的ConstantState實現(xiàn)類來刷新渲染視圖。默認情況下,從同一資源加載的所有drawables實例都共享一個公共狀態(tài),如果修改一個實例的狀態(tài),所有其他實例將接收相同的修改。
我們從ContextCompat類獲取Drawable 方法一步步往下看android 是如何實現(xiàn)Drawable共享的。
ContextCompat.java
public static final Drawable getDrawable(Context context, int id) {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
return ContextCompatApi21.getDrawable(context, id);
} else {
return context.getResources().getDrawable(id);
}
}
Resources.java
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme) throws NotFoundException {
TypedValue value;
......
// 從這里繼續(xù)跟進去,這是加載Drawable的方法
final Drawable res = loadDrawable(value, id, theme);
synchronized (mAccessLock) {
if (mTmpValue == null) {
mTmpValue = value;
}
}
return res;
}
@Nullable
Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
......
final boolean isColorDrawable;
// Drawable 的資源緩存類
final DrawableCache caches;
// 緩存的key
final long key;
......
// 這里先判斷是否加載過,如果已經(jīng)加載過就去緩存里面去取,如果成功從緩存中取到就返回。
if (!mPreloading) {
final Drawable cachedDrawable = caches.getInstance(key, theme);
if (cachedDrawable != null) {
return cachedDrawable;
}
}
// 緩存中沒有,則根據(jù)ConstantState 來創(chuàng)建新的Drawable
final ConstantState cs;
if (isColorDrawable) {
cs = sPreloadedColorDrawables.get(key);
} else {
cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
}
Drawable dr;
if (cs != null) {
dr = cs.newDrawable(this);
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
dr = loadDrawableForCookie(value, id, null);
}
......
// 緩存Drawable
if (dr != null) {
dr.setChangingConfigurations(value.changingConfigurations);
cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr);
}
return dr;
}
可以看下cacheDrawable 這個方法,雖然從名字上理解是緩存Drawable,但其實是緩存的Drawable對應的ConstantState 。
private void cacheDrawable(TypedValue value, boolean isColorDrawable, DrawableCache caches,
Theme theme, boolean usesTheme, long key, Drawable dr) {
final ConstantState cs = dr.getConstantState();
......
// 緩存ConstantState
caches.put(key, theme, cs, usesTheme);
......
}
}
DrawableCache.java
public Drawable getInstance(long key, Resources.Theme theme) {
// 注意這里,從緩存中取出來的是 ConstantState
final Drawable.ConstantState entry = get(key, theme);
if (entry != null) {
return entry.newDrawable(mResources, theme);
}
return null;
}
跟到這里心里大概也有譜了,原來android 不是共享的Drawable ,而是共享的內(nèi)部類 ConstantState,ConstantState 中才是保存相關(guān)信息的。所以也就會出現(xiàn)如果修改了資源的某一個項信息,引用相同資源的其他Drawable 也就一同變化。這會兒我們看下面的這張圖也就不難理解了!
而如果要實現(xiàn)對同一個Drawable進行不同著色就必須要打破這種共享狀態(tài)。使之成為下圖所展示的狀態(tài)。
那么如何才能打破這種狀態(tài)呢?
mutate() 使Drawable可變
上面說到如果要實現(xiàn)對同一個Drawable進行不同著色就必須要打破這種共享狀態(tài)。默認情況下,從同一資源加載的所有drawables實例都共享一個公共狀態(tài);
如果修改一個實例的狀態(tài),所有其他實例將接收相同的修改。而mutate() 方法就是使drawable 可變,
一個可變的drawable不與任何其他drawable共享它的狀態(tài),這樣如果只修改可變drawable的屬性就不會影響到其他與它加載同一個資源的drawable。
那么為mutate方法是如何打破共享狀態(tài)呢?
Drawable 是抽象類,同時mutate()返回的是this,我們以BitmapDrawable 為例,看下mutate() 這個方法。
/**
* A mutable BitmapDrawable still shares its Bitmap with any other Drawable
* that comes from the same resource.
*
* @return This drawable.
*/
@Override
public Drawable mutate() {
/*
mMutated 是個標簽,用來保證mutate只會設(shè)置一次,也就解釋了在Drawable中對mutate()
方法的一個解釋,Calling this method on a mutable Drawable will have no
effect(在已經(jīng)可變的drawable上調(diào)用此方法無效),因為返回的還是自身
*/
if (!mMutated && super.mutate() == this) {
// 重新引用了一個新的狀態(tài)對象
mBitmapState = new BitmapState(mBitmapState);
mMutated = true;
}
return this;
}
而BitmapState(BitmapState bitmapState) 這個構(gòu)造方法是對自己的屬性重新進行了賦值。這樣就相當于不再引用共享的公共狀態(tài)了,重新指向了一個新的狀態(tài)。
ok,修改我們的工具類重新看下效果。
/**
* 對目標Drawable 進行著色
*
* @param drawable 目標Drawable
* @param color 著色的顏色值
* @return 著色處理后的Drawable
*/
public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
Drawable wrappedDrawable = DrawableCompat.wrap(drawable).mutate();
DrawableCompat.setTint(wrappedDrawable, color);
return wrappedDrawable;
}
效果:
又是一臉懵逼中,怎么還是不對。雖然上面的兩個ImageView 顯示ok 了,但為下面的兩個ImageVIew顯示還是不對???奇了怪了!
猜想1:文檔中有這么一句介紹 “Calling this method on a mutable Drawable will have
no effect.”在可變的Drawable 上調(diào)用此方法無效。所以我猜想會不會因為目標Drawable 已經(jīng)可變的了,但是因為
warp()方法是對同一個Drawable 對象做的包裝,如果已經(jīng)調(diào)用過mutate()方法了,那么再次調(diào)用mutate
方法無效,對Drawable的最后一次修改覆蓋了之前的修改。猜想來源于倆個現(xiàn)象,1.上面的兩個ImageView
沒有受影響,顯示的是正確的;2.后修改的紅色生效,而原本應該顯示綠色ImageView 卻顯示成了紅色。
猜想2:以BitmapDrawable 為例,在BitmapDrawable 的mutate 方法中有這么一句描述:“A mutable
BitmapDrawable still shares its Bitmap with any other Drawable that
comes from the same resource.”
那么經(jīng)過wrap 處理過的drawable 是否還是原來的drawable呢?
打印 DrawableCompat.wrap(drawable).toString()
發(fā)現(xiàn)兩次得到的結(jié)果是不一樣的,也就是說傳入的和包裝后的不是同一個對象。但是我用小米5 android版本是7.0
得到的結(jié)果又是一樣的,即傳入的和包裝后的是同一個對象。
測試機型為小米5 系統(tǒng)版本為7.0。出現(xiàn)的效果和三星Galaxy s4 是一樣。
Log.e("drawable", drawable.toString());
Log.e("wrap", DrawableCompat.wrap(drawable).toString());
02-07 21:41:36.557 24675-24675/com.skx.tomike E/drawable:
android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.557 24675-24675/com.skx.tomike E/wrap:
android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.558 24675-24675/com.skx.tomike E/drawable:
android.graphics.drawable.BitmapDrawable@12bc2f1
02-07 21:41:36.558 24675-24675/com.skx.tomike E/wrap:
android.graphics.drawable.BitmapDrawable@12bc2f1
通過查看代碼中也得到了相應的答案。
DrawableCompat.java
version >= 23
static class MDrawableImpl extends LollipopDrawableImpl {
@Override
public void setLayoutDirection(Drawable drawable, int layoutDirection) {
DrawableCompatApi23.setLayoutDirection(drawable, layoutDirection);
}
@Override
public int getLayoutDirection(Drawable drawable) {
return DrawableCompatApi23.getLayoutDirection(drawable);
}
@Override
public Drawable wrap(Drawable drawable) {
// No need to wrap on M+ M以上版本不需要包裝,直接返回drawable
return drawable;
}
}
version >= 19
static class KitKatDrawableImpl extends JellybeanMr1DrawableImpl {
@Override
public void setAutoMirrored(Drawable drawable, boolean mirrored) {
DrawableCompatKitKat.setAutoMirrored(drawable, mirrored);
}
@Override
public boolean isAutoMirrored(Drawable drawable) {
return DrawableCompatKitKat.isAutoMirrored(drawable);
}
@Override
public Drawable wrap(Drawable drawable) {
// 這里是new 出來的新對象。
return DrawableCompatKitKat.wrapForTinting(drawable);
}
@Override
public int getAlpha(Drawable drawable) {
return DrawableCompatKitKat.getAlpha(drawable);
}
}
這里我摘出來兩個來進行對比。當api版本>=23 時,wrap 方法返回是傳入的drawable。當api版本>=19 && <21 時,warp方法返回的是DrawableCompatKitKat.wrapForTinting(drawable)。這也就解釋了為什么api版本不同,返回的結(jié)果不同了。
在高版本上(api>23)也就驗證了猜想1是正確的,因為前后兩次著色都是針對同一個drawable對象,而mutate 方法又只會生效一次,所以第二次的設(shè)置就理所應當?shù)母采w了第一次的設(shè)置,那么表現(xiàn)出來的結(jié)果就應該都是后面設(shè)置的顏色。
但是對于低版本就不太清楚為什么了,對drawable 進行包裝后得到的兩個不同的對象,既然是不同的對象,而且還都進行了mutate()設(shè)置為什么還是會表現(xiàn)出一樣呢?這里做個記錄!
針對猜想1我們做個簡單試驗。如果只是因為引用的是同一個Drawable對象的話,那我們只需要引用不同的Drawable 對象就OK了。
這樣做下簡單修改:
Drawable originBitmapDrawable = ContextCompat.getDrawable(this,
R.drawable.icon_beijing);
mImageView1.setBackground(
SkxDrawableHelper.tintDrawable(originBitmapDrawable,
Color.parseColor("#30c3a6")));
Drawable originBitmapDrawable2 = ContextCompat.getDrawable(this,
R.drawable.icon_beijing);
mImageView2.setImageDrawable(
SkxDrawableHelper.tintDrawable(originBitmapDrawable2,
Color.parseColor("#ff4081")));
效果:
對了?還是很懵,還有好多想不通的地方!還是要多翻源碼啊。
Drawable getConstantState()
返回一個持有此Drawable的共享狀態(tài)的ConstantState實例。而ConstantState類中也提供了方法來創(chuàng)建Drawable,在上面的部分我們也見到過。
newDrawable:從當前共享狀態(tài)來創(chuàng)建一個drawable 實例。
這樣的話我們就可以通過 getConstantState() 方法來獲取drawable 所持有的共享狀態(tài)的ConstantState,然后通過 newDrawable 方法來獲取相應的drawable實例。
Android Tint工具類
public class SkxDrawableHelper {
/**
* 對目標Drawable 進行著色
*
* @param drawable 目標Drawable
* @param color 著色的顏色值
* @return 著色處理后的Drawable
*/
public static Drawable tintDrawable(@NonNull Drawable drawable, int color) {
// 獲取此drawable的共享狀態(tài)實例
Drawable wrappedDrawable = getCanTintDrawable(drawable);
// 進行著色
DrawableCompat.setTint(wrappedDrawable, color);
return wrappedDrawable;
}
/**
* 對目標Drawable 進行著色。
* 通過ColorStateList 指定單一顏色
*
* @param drawable 目標Drawable
* @param color 著色值
* @return 著色處理后的Drawable
*/
public static Drawable tintListDrawable(@NonNull Drawable drawable, int color) {
return tintListDrawable(drawable, ColorStateList.valueOf(color));
}
/**
* 對目標Drawable 進行著色
*
* @param drawable 目標Drawable
* @param colors 著色值
* @return 著色處理后的Drawable
*/
public static Drawable tintListDrawable(@NonNull Drawable drawable, ColorStateList colors) {
Drawable wrappedDrawable = getCanTintDrawable(drawable);
// 進行著色
DrawableCompat.setTintList(wrappedDrawable, colors);
return wrappedDrawable;
}
/**
* 獲取可以進行tint 的Drawable
* <p>
* 對原drawable進行重新實例化 newDrawable()
* 包裝 warp()
* 可變操作 mutate()
*
* @param drawable 原始drawable
* @return 可著色的drawable
*/
@NonNull
private static Drawable getCanTintDrawable(@NonNull Drawable drawable) {
// 獲取此drawable的共享狀態(tài)實例
Drawable.ConstantState state = drawable.getConstantState();
// 對drawable 進行重新實例化、包裝、可變操作
return DrawableCompat.wrap(state == null ? drawable : state.newDrawable()).mutate();
}
}
總結(jié)
以上是生活随笔為你收集整理的Android 图片着色 Tint 详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【ROS-rviz】发布一个图像结果 t
- 下一篇: txt文件覆盖恢复