其實你不懂:Drawable著色(tint)的兼容方案 源碼解析

前兩天寫一個自定義控件,使用Drawable變色來展示EditText的不同狀態,涉及到了DrawableCompat這個類,今天著重分析一下它。

1:Drawable變色的通用代碼

//1:通過圖片資源文件生成Drawable實例
Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher).mutate();
//2:先調用DrawableCompat的wrap方法
drawable = DrawableCompat.wrap(drawable);
//3:再調用DrawableCompat的setTint方法,為Drawable實例進行著色
DrawableCompat.setTint(drawable, Color.RED);

這里涉及幾個方法:

  • Drawable.mutate()
  • DrawableCompat.wrap(@NonNull Drawable drawable)
  • DrawableCompat.setTint(@NonNull Drawable drawable, @ColorInt int tint)

下面看一下這幾個方法的源碼,Drawable.mutate()稍后分析,先看一下DrawableCompat中的wrap和setTint這兩個方法。

2:DrawableCompat.wrap(@NonNull Drawable drawable)

public static Drawable wrap(@NonNull Drawable drawable) {
  return IMPL.wrap(drawable);
}

從源碼可見,wrap方法內部是return IMPL.wrap(drawable),那這個IMPL是?

    static final DrawableImpl IMPL;
    /**
     * Interface for the full API.
     */
    interface DrawableImpl {
        void jumpToCurrentState(Drawable drawable);
        void setAutoMirrored(Drawable drawable, boolean mirrored);
        boolean isAutoMirrored(Drawable drawable);
        void setHotspot(Drawable drawable, float x, float y);
        void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom);
        void setTint(Drawable drawable, int tint);
        void setTintList(Drawable drawable, ColorStateList tint);
        void setTintMode(Drawable drawable, PorterDuff.Mode tintMode);
        Drawable wrap(Drawable drawable);
        boolean setLayoutDirection(Drawable drawable, int layoutDirection);
        int getLayoutDirection(Drawable drawable);
        int getAlpha(Drawable drawable);
        void applyTheme(Drawable drawable, Resources.Theme t);
        boolean canApplyTheme(Drawable drawable);
        ColorFilter getColorFilter(Drawable drawable);
        void clearColorFilter(Drawable drawable);
        void inflate(Drawable drawable, Resources res, XmlPullParser parser, AttributeSet attrs,
                     Resources.Theme t) throws IOException, XmlPullParserException;
    }
    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new MDrawableImpl();
        } else if (version >= 21) {
            IMPL = new LollipopDrawableImpl();
        } else if (version >= 19) {
            IMPL = new KitKatDrawableImpl();
        } else if (version >= 17) {
            IMPL = new JellybeanMr1DrawableImpl();
        } else if (version >= 11) {
            IMPL = new HoneycombDrawableImpl();
        } else {
            IMPL = new BaseDrawableImpl();
        }
    }

可見,在不同的SDK版本下,IMPL對應DrawableImpl的不同子類實例。下面分別看一下這幾個實現類對wrap方法的實質執行代碼。

  • MDrawableImpl
    public Drawable wrap(Drawable drawable) {
    // No need to wrap on M+
    //未對Drawable實例做任何處理,直接返回
    return drawable;
    }
    可見在SDK版本>= 23(MDrawableImpl)情況下:DrawableCompat.wrap(@NonNull Drawable drawable)直接返回了原始的Drawable實例
  • LollipopDrawableImpl
    public Drawable wrap(Drawable drawable) {
    return DrawableCompatLollipop.wrapForTinting(drawable);
    }
    繼續跟蹤DrawableCompatLollipop.wrapForTinting:
    public static Drawable wrapForTinting(final Drawable drawable) {
    if (!(drawable instanceof TintAwareDrawable)) {
    //當前傳入的Drawable實例并不屬于TintAwareDrawable
    return new DrawableWrapperLollipop(drawable);
    }
    return drawable;
    }
    繼續跟蹤DrawableWrapperLollipop:
    class DrawableWrapperLollipop extends DrawableWrapperKitKat {
    DrawableWrapperLollipop(Drawable drawable) {
    super(drawable);
    }
    ***
    }
    繼續跟蹤DrawableWrapperKitKat:
    class DrawableWrapperKitKat extends DrawableWrapperHoneycomb {
    DrawableWrapperKitKat(Drawable drawable) {
    super(drawable);
    }
    ***
    }
    繼續跟蹤DrawableWrapperHoneycomb:
    class DrawableWrapperHoneycomb extends DrawableWrapperGingerbread {
    DrawableWrapperHoneycomb(Drawable drawable) {
    super(drawable);
    }
    ***
    }
    繼續跟蹤DrawableWrapperGingerbread:
    static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
    private int mCurrentColor;
    private PorterDuff.Mode mCurrentMode;
    private boolean mColorFilterSet;
    DrawableWrapperState mState;  //mState默認是null
    private boolean mMutated;
    Drawable mDrawable;

    DrawableWrapperGingerbread(@Nullable Drawable dr) {
        mState = mutateConstantState();
        // Now set the drawable...
        setWrappedDrawable(dr);
    }
  • mState = mutateConstantState();
mutateConstantState()一路追蹤到底:
    DrawableWrapperState mutateConstantState() {
        //返回一個DrawableWrapperStateBase實例
        //mState默認是null
        return new DrawableWrapperStateBase(mState, null);
    }
    private static class DrawableWrapperStateBase extends DrawableWrapperState {
        //調用父類 DrawableWrapperState 的構造函數
        //orig就是DrawableWrapperGingerbread中的mState,默認是null
        DrawableWrapperStateBase(
                @Nullable DrawableWrapperState orig, @Nullable Resources res) {
            super(orig, res);
        }
        @Override
        public Drawable newDrawable(@Nullable Resources res) {
            return new DrawableWrapperGingerbread(this, res);
        }
    }
    protected static abstract class DrawableWrapperState extends Drawable.ConstantState {
        int mChangingConfigurations;
        Drawable.ConstantState mDrawableState;
        ColorStateList mTint = null;
        PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
        //orig就是DrawableWrapperGingerbread中的mState,默認是null
        DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
            //因為orig是null,所以mChangingConfigurations,mDrawableState,
            //mTint,mTintMode都是DrawableWrapperState中的默認值
            if (orig != null) {
                mChangingConfigurations = orig.mChangingConfigurations;
                mDrawableState = orig.mDrawableState;
                mTint = orig.mTint;
                mTintMode = orig.mTintMode;
            }
        }
}

代碼一路跟下來可見:mState = mutateConstantState(),mState被賦值為一個新的DrawableWrapperState實例
其中:mState(DrawableWrapperState)中,下面成員變量的值都是默認值:
int mChangingConfigurations;
Drawable.ConstantState mDrawableState;
ColorStateList mTint = null;
PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;

  • setWrappedDrawable(dr);
setWrappedDrawable(Drawable dr)一路追蹤到底:
    public final void setWrappedDrawable(Drawable dr) {
        if (mDrawable != null) {
            mDrawable.setCallback(null);
        }
        mDrawable = dr;
        if (dr != null) {
            dr.setCallback(this);
            // Only call setters for data that's stored in the base Drawable.
            setVisible(dr.isVisible(), true);
            setState(dr.getState());
            setLevel(dr.getLevel());
            setBounds(dr.getBounds());
            //mState不為null:為一個新的DrawableWrapperState實例
            if (mState != null) {
                //為mState的mDrawableState賦值為Drawable原始實例
                //關聯的ConstantState
                mState.mDrawableState = dr.getConstantState();
            }
        }
        invalidateSelf();
    }

這里涉及到DrawableWrapperGingerbread中的幾個方法:
setVisible(boolean visible, boolean restart)
@Override
public boolean setVisible(boolean visible, boolean restart) {
//Drawable中的setVisible,用于控制Drawable實例是否執行動畫,對于AnimationDrawable實例會產生效果,控制是否執行動畫
return super.setVisible(visible, restart) || mDrawable.setVisible(visible, restart);
}
setState(final int[] stateSet)

    @Override
    public boolean setState(final int[] stateSet) {
        boolean handled = mDrawable.setState(stateSet);
        handled = updateTint(stateSet) || handled;
        return handled;
    }
    private boolean updateTint(int[] state) {
        //isCompatTintEnabled()這里直接返回了true
        if (!isCompatTintEnabled()) {
            // If compat tinting is not enabled, fail fast
            return false;
        }
        //mState.mTint是默認值:null
        final ColorStateList tintList = mState.mTint;
        //mState.mTintMode是默認值:DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN
        final PorterDuff.Mode tintMode = mState.mTintMode;
        if (tintList != null && tintMode != null) {
            //tintList為null,所以不會執行下面代碼
            final int color = tintList.getColorForState(state, tintList.getDefaultColor());
            if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {
                setColorFilter(color, tintMode);
                mCurrentColor = color;
                mCurrentMode = tintMode;
                mColorFilterSet = true;
                return true;
            }
        } else {
            //tintList為null
            mColorFilterSet = false;
            //執行的是其父類Drawable的clearColorFilter()
            clearColorFilter();
        }
        return false;
    }
    protected boolean isCompatTintEnabled() {
        // It's enabled by default on Gingerbread
        //這里直接返回了true
        return true;
    }
Drawable的clearColorFilter方法:移除了當前Drawable實例關聯的ColorFilter
    public void clearColorFilter() {
        setColorFilter(null);
    }

可見:setState(dr.getState())這一步直接移除了Drawable實例關聯的ColorFilter.
**setLevel直接使用的是其父類Drawable中的方法setLevel(@IntRange(from=0,to=10000) int level) **

Drawable的setLevel方法:
    public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
        //在這里,因為mLevel就是之前DrawableWrapperGingerbread構造函數中的Drawable dr的level值,
        //而level=dr.getLevel()返回的也是Drawable dr的level值,mLevel == level,
        //所以下面的代碼并不會執行
        if (mLevel != level) {
            mLevel = level;
            return onLevelChange(level);
        }
        return false;
    }

可見:setLevel(dr.getLevel())這一步并未產生實質影響,未執行處理邏輯。
setBounds直接使用的是其父類Drawable中的方法setBounds(@NonNull Rect bounds)

    /**
     * Specify a bounding rectangle for the Drawable. This is where the drawable
     * will draw when its draw() method is called.
     */
    public void setBounds(@NonNull Rect bounds) {
        setBounds(bounds.left, bounds.top, bounds.right, bounds.bottom);
    }

可見:setBounds(dr.getBounds())這一步為新產生的DrawableWrapperGingerbread實例設置其繪制范圍與原始Drawable實例一致。

可見在SDK版本>= 21(LollipopDrawableImpl)情況下:DrawableCompat.wrap(@NonNull Drawable drawable)返回了Drawable的子類DrawableWrapperGingerbread的一個新實例。
且在updateTint方法中移除了該新實例關聯過的ColorFilter,設置了該新實例的繪制范圍和原始Drawable實例相同

  • KitKatDrawableImpl
    跟蹤終點同LollipopDrawableImpl
在KitKatDrawableImpl情況下,wrap(Drawable drawable)一路跟蹤到底:
@Override
public Drawable wrap(Drawable drawable) {
    return DrawableCompatKitKat.wrapForTinting(drawable);
}
**
繼承關系跟蹤到最后還是DrawableWrapperGingerbread,和LollipopDrawableImpl相同:
DrawableWrapperGingerbread(@Nullable Drawable dr) {
    mState = mutateConstantState();
    // Now set the drawable...
    setWrappedDrawable(dr);
}
  • JellybeanMr1DrawableImpl
    跟蹤終點同LollipopDrawableImpl
  • HoneycombDrawableImpl
    跟蹤終點同LollipopDrawableImpl
  • BaseDrawableImpl
    跟蹤終點同LollipopDrawableImpl

綜上可見:
1:在SDK版本>= 23(MDrawableImpl)情況下:DrawableCompat.wrap(@NonNull Drawable drawable)直接返回了原始的Drawable實例;
2:其余情況下,DrawableCompat.wrap(@NonNull Drawable drawable)返回了Drawable的子類DrawableWrapperGingerbread的一個新實例,且在updateTint方法中移除了該新實例關聯過的ColorFilter,設置了該新實例的繪制范圍和原始Drawable實例相同;

3:DrawableCompat.setTint(@NonNull Drawable drawable, @ColorInt int tint)

    public static void setTint(@NonNull Drawable drawable, @ColorInt int tint) {
        IMPL.setTint(drawable, tint);
    }

之前分析wrap方法時候已經看到IMPL在不同SDK版本下有不同的實現,還是逐一查看:

  • MDrawableImpl
setTint一路跟蹤:
@Override
public void setTint(Drawable drawable, int tint) {
    DrawableCompatLollipop.setTint(drawable, tint);
}
public static void setTint(Drawable drawable, int tint) {
    //執行的是Drawable原生的setTint方法
    drawable.setTint(tint);
}
  • LollipopDrawableImpl
setTint一路跟蹤:
@Override
public void setTint(Drawable drawable, int tint) {
    //同MDrawableImpl
    DrawableCompatLollipop.setTint(drawable, tint);
}
  • KitKatDrawableImpl
setTint一路跟蹤:
@Override
public void setTint(Drawable drawable, int tint) {
    DrawableCompatBase.setTint(drawable, tint);
}
public static void setTint(Drawable drawable, int tint) {
    if (drawable instanceof TintAwareDrawable) {
        ((TintAwareDrawable) drawable).setTint(tint);
    }
}
public interface TintAwareDrawable {
    void setTint(@ColorInt int tint);
    void setTintList(ColorStateList tint);
    void setTintMode(PorterDuff.Mode tintMode);
}

在上面分析DrawableCompat.wrap方法時候,已知其返回結果為DrawableWrapperGingerbread新實例,看一下DrawableWrapperGingerbread類的聲明:class DrawableWrapperGingerbread extends Drawable implements Drawable.Callback, DrawableWrapper, TintAwareDrawable
由此可見,setTint實質執行的還是DrawableWrapperGingerbread的setTint方法,繼續跟蹤:

    setTint一路追蹤:
    @Override
    public void setTint(int tint) {
        setTintList(ColorStateList.valueOf(tint));
    }
    @Override
    public void setTintList(ColorStateList tint) {
        //1:在上面分析DrawableCompat.wrap時候,mState的值如下:
        //mState(DrawableWrapperState)中,下面成員變量的值都是默認值:
        //int mChangingConfigurations;
        //Drawable.ConstantState mDrawableState;
        //ColorStateList mTint = null;
        //PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;

        //2:在DrawableCompat.setTint時候,mState.mTint不再為空值
        mState.mTint = tint;
        updateTint(getState());
    }

    //之前DrawableCompat.wrap已經執行過一次updateTint,
    //現在DrawableCompat.setTint第二次執行!!
    private boolean updateTint(int[] state) {
        //isCompatTintEnabled()返回true
        if (!isCompatTintEnabled()) {
            // If compat tinting is not enabled, fail fast
            return false;
        }
        //此時mState.mTint已經在setTintList中賦值不為null
        final ColorStateList tintList = mState.mTint;
        //mState.mTintMode依然為默認值不為null
        final PorterDuff.Mode tintMode = mState.mTintMode;
        if (tintList != null && tintMode != null) {
            //兩者都不為空,因而執行if條件下代碼

            //獲取當前狀態下對應的顏色
            final int color = tintList.getColorForState(state, tintList.getDefaultColor());
            //mColorFilterSet默認是false
            //color即為setTint時候傳入的顏色
            //mCurrentColor默認值是0
            //tintMode是mState中的mTintMode=DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN
            //mCurrentMode默認值是null
            if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {
                //對Drawable實例產生著色的,本質上還是執行了Drawable中的setColorFilter方法。
                setColorFilter(color, tintMode);
                mCurrentColor = color;
                mCurrentMode = tintMode;
                mColorFilterSet = true;
                return true;
            }
        } else {
            mColorFilterSet = false;
            clearColorFilter();
        }
        return false;
    }
  • JellybeanMr1DrawableImpl
    跟蹤終點同KitKatDrawableImpl
  • HoneycombDrawableImpl
    跟蹤終點同KitKatDrawableImpl
  • BaseDrawableImpl
    跟蹤終點同KitKatDrawableImpl

綜上可見:
1:在SDK版本>= 21(MDrawableImpl和LollipopDrawableImpl)情況下:DrawableCompat.setTint(@NonNull Drawable drawable, @ColorInt int tint)執行的是Drawable原生的setTint方法;
2:其余情況下,DrawableCompat.setTint(@NonNull Drawable drawable, @ColorInt int tint)本質上還是執行了Drawable中的setColorFilter方法;

4:原生Drawable.setTint(@ColorInt int tintColor)

public void setTint(@ColorInt int tintColor) {
    setTintList(ColorStateList.valueOf(tintColor));
}
public void setTintList(@Nullable ColorStateList tint) {
    //你沒有看錯,竟然是個空方法!!!!
}

剛看到這兒時候也有些納悶,后來一想肯定是我們在獲取Drawable原始實例的時,獲取的其實是Drawable的子類實例,在Drawable子類里對setTintList做了重寫,有圖有真相:


setTintList重寫.png

5:Drawable.mutate()的作用

    /**
     * Make this drawable mutable. This operation cannot be reversed. A mutable
     * drawable is guaranteed to not share its state with any other drawable.
     * This is especially useful when you need to modify properties of drawables
     * loaded from resources. By default, all drawables instances loaded from
     * the same resource share a common state; if you modify the state of one
     * instance, all the other instances will receive the same modification.
     *
     * Calling this method on a mutable Drawable will have no effect.
     *
     * @return This drawable.
     * @see ConstantState
     * @see #getConstantState()
     */
    public @NonNull Drawable mutate() {
        return this;
    }

單純看源碼解釋可能比較抽象,說的通俗一點,我們通過Resource獲取mipmap文件夾下的一張資源圖片,在獲取Drawable初始實例時候如果不使用mutate(),那么我們對這個Drawable進行著色,不僅改變了當前Drawable實例的顏色,以后任何通過這個圖片獲取到的Drawable實例,都會具有之前設置的顏色。所以如果我們對一張資源圖片的著色不是APP全局生效的,就需要使用mutate()。

具體原因:
Android為了優化系統性能,同一張資源圖片生成的Drawable實例在內存中只存在一份,在不使用mutate的情況下,修改任意Drawable都會全局發生變化。
使用mutate,Android系統也沒有把Drawable實例又單獨拷貝一份,僅僅是單獨存放了狀態值,很小的一部分數據,Drawable實例在內存中仍然保持1份,因而并不會影響系統的性能。
具體變化可以通過2張圖片說明:
1:不使用mutate:

共享狀態.png

2:使用mutate:
不共享狀態.png

以上就是個人分析的一點結果,若有錯誤,請各位同學留言告知!

That's all !

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容