从使用到源码,细说 Android 中的 tint 着色器

LynLegere 8年前
   <p>自 API 21 (Android M)开始,Android SDK 引入 tint 着色器,可以随意改变安卓项目中图标或者 View 背景的颜色,一定程度上可以减少同一个样式不同颜色图标的数量,从而起到 Apk 瘦身的作用。不过使用 tint 存在一定的兼容性问题,且听本文慢慢说来。</p>    <h2>xml 中的 tint 和 tintMode 属性</h2>    <ul>     <li> <p>android:tint:给图标着色的属性,值为所要着色的颜色值,没有版本限制;通常用于给透明通道的 png 图标或者点九图着色。</p> </li>     <li> <p>android:tintMode:图标着色模式,值为枚举类型,共有 六种可选值(add、multiply、screen、src_over、src_in、src_atop),仅可用于 API 21 及更高版本。</p> </li>    </ul>    <p>对应于给图片着色的这两个属性,给 View 背景着色也有两个属性: <strong>backgroundTint</strong> 和 <strong>backgroundTintMode</strong> ,用法相同,只是作用于 android:background 属性。需要注意的是,这两个属性也只是作用于 API 21 及更高版本。</p>    <p>这里我们在使用默认 tintMode 的情况下,演示一下图标着色和背景着色的前后对比情况:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/ad28c7b5e2139d92678567261207a397.png"></p>    <p>原图:不做任何处理的 ImageButton,代码如下:</p>    <pre>  <code class="language-java"><ImageButton      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:src="@mipmap/ic_home"      android:background="@android:color/transparent"/>  </code></pre>    <p>图标着色:使用 android:tint 属性对 src 属性指向的图标着色处理,代码如下:</p>    <pre>  <code class="language-java"><ImageButton      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:tint="@android:color/black"      android:src="@mipmap/ic_home"      android:background="@android:color/transparent"/>  </code></pre>    <p>背景着色:使用 backgroundTint 属性对 background 属性赋予的背景色着色处理,代码如下(这里只是为了演示,实际上直接改变 background 背景色即可):</p>    <pre>  <code class="language-java"><ImageButton      android:layout_width="wrap_content"      android:layout_height="wrap_content"      android:backgroundTint="@android:color/black"      android:src="@mipmap/ic_home"      android:background="@android:color/white"/>  </code></pre>    <p>这里 android:background 属性值使用的是颜色值,如果是图片的话,一样可以着色处理。并且,背景使用图片时着色的需求更现实一些。</p>    <p>注意:tint 或 backgroundTint 属性,与 src 或 background 属性一定是对应成对出现的。这个不难理解,要有处理源嘛。</p>    <h2>java 代码中的 DrawableCompat</h2>    <p>通过 xml 中的属性或者对应的 Java 代码中的 API 方法可以改变 View 所用到的图片颜色,但是存在一定的兼容性问题。好在有相应的兼容性 API 可以适配 6.0 之前的系统,也就是 DrawableCompat 类。直接看代码吧:</p>    <pre>  <code class="language-java">Drawable originalDrawable = ContextCompat.getDrawable(this, R.mipmap.ic_home);  Drawable tintDrawable = DrawableCompat.wrap(originalDrawable).mutate();  DrawableCompat.setTint(tintDrawable, Color.parseColor("#000000"));  mSamplesIv.setImageDrawable(tintDrawable);  </code></pre>    <p>可以看出,DrawableCompat 通过 setTint() 方法对 drawable 对象着色处理。值得注意的是,这里有两个特殊的方法需要特别说明一下:</p>    <p>DrawableCompat.wrap()</p>    <p>为了在不同的系统 API 上使用 DrawableCompat.setTint() 做图标的着色处理,必须使用这个方法处理现有的 drawable 对象。并且,要将处理结果重新通过 setImageDrawable() 或者 setBackground() 赋值给 View 才能见效;</p>    <p>drawable.mutate()</p>    <p>我们先来看一个有趣的现象:如果我们有两个 ImageView 使用相同一个图片资源作为 src 或者 background 的属性值,然后在 Java 代码中通过 DrawableCompat 类对其中一个做着色处理,就像上面所写的代码这样,运行后你会发现,只有当前被赋值的 ImageView 显示的是被着色处理后的图片;但是去掉 mutate() 方法时,再次运行,两个 ImageView 都显示的是被着色处理后的图片!事实上,不仅是两个,应用中所有使用到该图片资源的地方,都会显示成被着色处理过的样式。</p>    <p>这就是 mutate() 存在的必要性。要说到这个方法,就大有讲头啦。在此之前,我们必须先了解一下 <strong>constant state</strong> 这个概念。</p>    <p>Android 系统为了减少内存消耗,将应用中所用到的相同 drawable (可以理解为相同资源)共享同一个 state,并称之为 constant state。这里用图表演示一下,两个 View 加载同一个图片资源,创建两个 drawables 对象,但是共享同一个 constant state 的场景:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/22bf282af05e16a20c174417ff9d6d8d.png"></p>    <p>这种设计当然大大节省内存,但也存在一个弊端。就是,当 constant state 属性发生变化时,所有使用相同资源的关联 drawable 都会随之改变,比如前面所说的这种现象。</p>    <p>而 mutate() 方法的出现就是为了解决这种问题的。你可以理解为 mutate() 方法就是复制一份 constant state,允许你随意改变属性,同时不对其他 drawable 有任何影响。如图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d32329327ff3fdb8104fa885046ed647.png"></p>    <p>再回到本文主题,可见,drawable 的着色处理必然要使用到 wrap() 和 mutate() 两个方法,也就顺理成章啦。</p>    <p>注意:为了起到兼容所有 API 的作用,着色处理时,建议同时使用 wrap() 和 mutate() 方法。可能,你在实际测试时,某些级别的系统 API 中,不会存在这种问题。</p>    <p>上面我们使用 setTint() 方法直接改变 drawable 的颜色,但是有时候,我们会给 Drawable 添加各种选择状态,比如点击时的 state_pressed 状态。DrawableCompat 类也提供有 setTintList() 方法,需要用到 ColorStateList。</p>    <p>举个例子,在 res/color 资源目录下定义一个 selector_home.xml 文件:</p>    <pre>  <code class="language-java"><?xml version="1.0" encoding="utf-8"?>  <selector xmlns:android="http://schemas.android.com/apk/res/android">        <item android:state_pressed="true" android:color="@android:color/black"/>      <item android:color="@android:color/white"/>    </selector>  </code></pre>    <p>在代码中通过 ContextCompat.getColorStateList 获取资源中的 ColorStateList 对象,并使用 DrawableCompat.setTintList() 方法着色处理即可:</p>    <pre>  <code class="language-java">Drawable originalDrawable = ContextCompat.getDrawable(this, R.mipmap.ic_home);  Drawable tintDrawable = DrawableCompat.wrap(originalDrawable).mutate();  DrawableCompat.setTintList(tintDrawable, ContextCompat.getColorStateList(this, R.color.selector_home));  mSamplesIv.setImageDrawable(tintDrawable);  </code></pre>    <p>当然你也可以直接在代码中手动创建一个 ColorStateList 对象:</p>    <pre>  <code class="language-java">int[] colors = new int[] { ContextCompat.getColor(this, android.R.color.black), ContextCompat.getColor(this, android.R.color.white)};  int[][] states = new int[2][];  states[0] = new int[] { android.R.attr.state_pressed};  states[1] = new int[] {};  ColorStateList colorStateList = new ColorStateList(states, colors);  </code></pre>    <p>效果都是一样的,如图:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/7d2a63d6fe4e1c3875ef439625fa0dee.gif"></p>    <h2>Tint 着色器原理</h2>    <p>前面讲到,使用 DrawableCompat 可以起到版本兼容效果。实际上,还有一种办法,就是使用 android.support.v7.widget 兼容包中的 AppCompatXXX 控件,比如 AppCompatImageView。这种控件提供有如下方法可用于着色处理:</p>    <ul>     <li>setSupportBackgroundTintList(@Nullable ColorStateList tint)</li>     <li>setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode)</li>    </ul>    <p>其实,不管是 DrawableCompat 还是 AppCompatXXX 控件,底层实现原理都是一样的。我们随便找一个看一下,就拿 AppCompatImageView 来看。看下 setSupportBackgroundTintList() 源码:</p>    <pre>  <code class="language-java">@Override  public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {      if (mBackgroundTintHelper != null) {          mBackgroundTintHelper.setSupportBackgroundTintList(tint);      }  }  </code></pre>    <p>调用 AppCompatBackgroundHelper 类的 setSupportBackgroundTintList 方法,继续深入源码:</p>    <pre>  <code class="language-java">void setSupportBackgroundTintList(ColorStateList tint) {      if (mBackgroundTint == null) {          mBackgroundTint = new TintInfo();      }      mBackgroundTint.mTintList = tint;      mBackgroundTint.mHasTintList = true;      applySupportBackgroundTint();  }  </code></pre>    <p>继续深入 applySupportBackgroundTint() 方法的源码:</p>    <pre>  <code class="language-java">void applySupportBackgroundTint() {      final Drawable background = mView.getBackground();      if (background != null) {          if (shouldApplyFrameworkTintUsingColorFilter()                  && applyFrameworkTintUsingColorFilter(background)) {              // This needs to be called before the internal tints below so it takes              // effect on any widgets using the compat tint on API 21 (EditText)              return;          }            if (mBackgroundTint != null) {              AppCompatDrawableManager.tintDrawable(background, mBackgroundTint,                      mView.getDrawableState());          } else if (mInternalBackgroundTint != null) {              AppCompatDrawableManager.tintDrawable(background, mInternalBackgroundTint,                      mView.getDrawableState());          }      }  }  </code></pre>    <p>该方法的重心在于 AppCompatDrawableManager.tintDrawable() 方法,继续深入:</p>    <pre>  <code class="language-java">static void tintDrawable(Drawable drawable, TintInfo tint, int[] state) {      if (DrawableUtils.canSafelyMutateDrawable(drawable)              && drawable.mutate() != drawable) {          Log.d(TAG, "Mutated drawable is not the same instance as the input.");          return;      }        if (tint.mHasTintList || tint.mHasTintMode) {          drawable.setColorFilter(createTintFilter(                  tint.mHasTintList ? tint.mTintList : null,                  tint.mHasTintMode ? tint.mTintMode : DEFAULT_MODE,                  state));      } else {          drawable.clearColorFilter();      }        if (Build.VERSION.SDK_INT <= 23) {          // Pre-v23 there is no guarantee that a state change will invoke an invalidation,          // so we force it ourselves          drawable.invalidateSelf();      }  }  </code></pre>    <p>找到这里,已经能看出一些端倪。原来是使用 drawable.setColorFilter() 进行颜色渲染处理的。并且通过 createTintFilter() 方法创建颜色过滤器:</p>    <pre>  <code class="language-java">private static PorterDuffColorFilter createTintFilter(ColorStateList tint,          PorterDuff.Mode tintMode, final int[] state) {      if (tint == null || tintMode == null) {          return null;      }      final int color = tint.getColorForState(state, Color.TRANSPARENT);      return getPorterDuffColorFilter(color, tintMode);  }    public static PorterDuffColorFilter getPorterDuffColorFilter(int color, PorterDuff.Mode mode) {      // First, lets see if the cache already contains the color filter      PorterDuffColorFilter filter = COLOR_FILTER_CACHE.get(color, mode);        if (filter == null) {          // Cache miss, so create a color filter and add it to the cache          filter = new PorterDuffColorFilter(color, mode);          COLOR_FILTER_CACHE.put(color, mode, filter);      }        return filter;  }  </code></pre>    <p>PorterDuffColorFilter 类!这就是我们要找的目标。PorterDuffColorFilter 可以获取 drawable 中的像素点,并使用相应的颜色过滤器予以处理。</p>    <p>知道原理之后,不妨试想一下,仅仅这样一句代码,是不是也能帮助我们实现着色处理呢:</p>    <pre>  <code class="language-java">mSamplesIv.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(this, android.R.color.black), PorterDuff.Mode.SRC_IN));  </code></pre>    <p>或者自定义 View 时也能将 AppCompatXXX 控件的相关源码复制过来,实现着色器功能。</p>    <p>当然,如果再去翻看 DrawableCompat 源码,虽然寻找路径不同,但最终还是会走到 drawable.setColorFilter() 方法。并且从 DrawableCompat 源码中,你还能看到为什么 wrap() 方法能够兼容处理不同系统 API 的原因。这里就不细细展示啦,感兴趣的朋友可以自己阅读源码。</p>    <p>这就是 Android SDK 中的 tint 着色器相关知识。事实上,我们也经常用到这个东西。举个最常见的例子,为什么不同主题下 EditText 背景的底部颜色条会不一样呢?其实,这也是一张点九图,只是不同主题下使用不同颜色的着色器处理过而已。</p>    <p> </p>    <p>来自:http://yifeng.studio/2017/03/30/android-tint/</p>    <p> </p>