Android 图片着色Tint后向兼容DrawableCompat库实现原理分析并简化封装

来源:互联网 发布:翩跹紫晶淘宝 编辑:程序博客网 时间:2024/06/05 01:12

前言:

之前在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 );
原创粉丝点击