Android 圖片着色Tint後向兼容DrawableCompat庫實現原理分析並簡化封裝

前言:

之前在Android Ui開發中實現ImageView背景圖片點擊變色,往往會要求UI設計師提供兩種不同顏色的圖片分別作爲selector的不同選中狀態下的背景圖,可以想象就是僅僅顏色不一樣,就需要一個相同大小的圖片,這樣不僅僅浪費資源,加大res下圖片資源體積,而且還需要重新加載一個新圖片而導致增加系統負擔。所以如果可以利用一種顏色的圖片就可以實現出來多種顏色,對這個圖片進行着色,實現不同種顏色的背景圖片顯示,那將大大的減少重複類型的圖片。那接下來介紹一個自己封裝系support.v4中DrawableCompat.setTintList的實現而來的TintDrawable類。

Drawable.setTintList介紹

這是在系統Api在21開始提供的方法,同時Android Support v4 的包中提供了 DrawableCompat兼容

    public void setTintList(@Nullable ColorStateList tint) {}

可以看到在Drawable中並沒有實現這個功能,具體均由子類,如BitmapDrawable:

@Override
    public void setTintList(ColorStateList tint) {
        mBitmapState.mTint = tint;
        mTintFilter = updateTintFilter(mTintFilter, tint, mBitmapState.mTintMode);
        invalidateSelf();
    }

所以我們只需要拿到基類Drawable引用之後,直接調用這個setTintList這個方法,就可以實現改變顏色,不論子類是BitmapDrawable、ColorDrawable等。

同時也注意到這個是新Api,要兼容雖然可以直接使用DrawableCompat來實現,但一個如果僅僅使用了這個一個功能就需要導入support.v4包的話,個人接受不了,故必須弄清DrawableCompat兼容低版本的實現原理,希望做到封裝成一個簡單的類。

示例代碼如下
1、改變圖片背景顏色爲白色:

ImageView button = (ImageView) findViewById(R.id.button );
Drawable drawable= button.getDrawable();
Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTintList(wrappedDrawable, ColorStateList.valueOf(Color.WHITE));
button.setImageDrawable(wrappedDrawable );

2、一個圖片實現點擊變色
在color目錄下new 一個 xml取名爲“selector_imageview.xml”作爲我們的控制顏色selector,點擊變爲粉紅色,正常狀態爲白色。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="#FF4081" android:state_pressed="true" />
    <item android:color="#ffffff" />
</selector>
ImageView button = (ImageView) findViewById(R.id.button );
Drawable drawable= button.getDrawable();
Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
//設置selector資源
DrawableCompat.setTintList(wrappedDrawable, getResources().getColorStateList(R.color.selector_imageview));
button.setImageDrawable(wrappedDrawable );

DrawableCompat.setTintList後向兼容實現原理

使用DrawableCompat實現着色的代碼如下:

public static Drawable tintDrawable(Drawable drawable, ColorStateList colors) {
    final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
    DrawableCompat.setTintList(wrappedDrawable, colors);
    return wrappedDrawable;
}

直接查看源碼:

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

public static void setTintList(@NonNull Drawable drawable, @Nullable ColorStateList tint) {
        IMPL.setTintList(drawable, tint);
    }

其中IMPL的實現如下:

static final DrawableImpl IMPL;
    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 if (version >= 5) {
            IMPL = new EclairDrawableImpl();
        } else {
            IMPL = new BaseDrawableImpl();
        }
    }

可以看出對所有的系統版本都有支持,結合實際測試發現,的確所有版本都可以實現了着色功能。既然是高版本纔有的Api,那看下低版本具體是如何實現的,直接從最低版本BaseDrawableImpl開始:

        @Override
        public Drawable wrap(Drawable drawable) {
            return DrawableCompatBase.wrapForTinting(drawable);
        }

        @Override
        public void setTintList(Drawable drawable, ColorStateList tint) {
            DrawableCompatBase.setTintList(drawable, tint);
        }

繼續看DrawableCompatBase的實現:

 public static Drawable wrapForTinting(Drawable drawable) {
        if (!(drawable instanceof DrawableWrapperDonut)) {
            return new DrawableWrapperDonut(drawable);
        }
        return drawable;
    }

 public static void setTintList(Drawable drawable, ColorStateList tint) {
        if (drawable instanceof DrawableWrapper) {
            ((DrawableWrapper) drawable).setCompatTintList(tint);
        }
    }

可以看到返回盡然DrawableWrapperDonut這個類,繼承了Drawable並其實現了DrawableWrapper的接口:

class DrawableWrapperDonut extends Drawable implements Drawable.Callback, DrawableWrapper{
......
}
public interface DrawableWrapper {

    void setCompatTint(int tint);

    void setCompatTintList(ColorStateList tint);

    void setCompatTintMode(PorterDuff.Mode tintMode);

    Drawable getWrappedDrawable();

    void setWrappedDrawable(Drawable drawable);

}

所以都是通過DrawableWrapperDonut 來實現的:

    @Override
    public void setCompatTintList(ColorStateList tint) {
        mState.mTint = tint;
        updateTint(getState());
    } 

   private boolean updateTint(int[] state) {
        if (!isCompatTintEnabled()) {
            // If compat tinting is not enabled, fail fast
            return false;
        }

        final ColorStateList tintList = mState.mTint;
        final PorterDuff.Mode tintMode = mState.mTintMode;

        if (tintList != null && tintMode != 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 {
            mColorFilterSet = false;
            clearColorFilter();
        }
        return false;
    }

可以看到setTintList最後都是,先獲取color值以及tintMode ,並通過 setColorFilter(color, tintMode)來設置。setColorFilter在Drawable中的實現

 public void setColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode) {
        setColorFilter(new PorterDuffColorFilter(color, mode));
    }

 public abstract void setColorFilter(@Nullable ColorFilter colorFilter);

發現是個抽象函數,子類必須實現,那看下子類DrawableWrapperDonut 是如何實現的:

 @Override
  public void setColorFilter(ColorFilter cf) {
        mDrawable.setColorFilter(cf);
    }

發現轉而交給上文中new DrawableWrapperDonut(drawable)時候傳遞而來的drawbale實現

  DrawableWrapperDonut(@Nullable Drawable dr) {
        mState = mutateConstantState();
        mDrawable = dr;
    }

就上文圖片變成白色的例子而言,此時的mDrawable =button.getDrawable(),這個getDrawable()獲取的是ImageView的圖片背景BitmapDrawable,具體實現可以參考我的另一片文章:圖片加載原理,所以 mDrawable.setColorFilter(cf)是由BitmapDrawable,可以看下其實現:

 @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mBitmapState.mPaint.setColorFilter(colorFilter);
        invalidateSelf();
    }

可以得知就是對mBitmapState畫筆mPaint設置ColorFilter屬性,然後待在View繪製背景時候,會調用背景drawable.draw(Canvas canvas),在BitmapDrawable.draw(Canvas canvas)中canvas會利用paint來背景,其屬性ColorFilter從而改變背景顏色。

簡化封裝

既然後向兼容都是DrawableWrapperDonut這個類實現的,那爲何不直接抽象出來直接使用,而不再使用support.v4包,個人封裝了一個類TintDrawable,模擬DrawableWrapperDonut實現,甚至可以直接使用。

細節注意

上文中有改變ImageView背景的例子,現在我們新建另一個ImageView,不改變背景顏色直接加載原圖顯示,會發現圖片還是改過顏色的背景,而不是原圖,這裏主要是系統加載圖片原理決定的(具體參考我的另一篇文章:Res目錄下資源如圖片文件和xml文件資源如何被加載顯示出來),在系統加載一次圖片之後,會對這次加載出來的Drawable做緩存,資源id作爲Key,所以當加載相同資源id時候,會先查緩存,沒有就加載。而上述第一次加載圖片之後並且着色,改變了第一加載圖片drawable對象,並且是直接改變該緩存對象,所以後面加載相同資源都是被改變過的Drawable。
解決辦法:通過mutate複製一個相同類型對象出來,而不改變緩存的對象

ImageView button = (ImageView) findViewById(R.id.button );
//通過mutate()複製加載出來的對象
Drawable drawable= button.getDrawable().mutate();
Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTintList(wrappedDrawable, ColorStateList.valueOf(Color.WHITE));
button.setImageDrawable(wrappedDrawable );
發佈了39 篇原創文章 · 獲贊 58 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章