[关闭]
@TryLoveCatch 2022-05-05T16:42:22.000000Z 字数 7821 阅读 2354

Android知识体系之一张图片实现点击效果

Android知识体系


前言

一般情况下,我们都会遇到,在点击一个icon的时候,要实现一个点击效果。正常的流程,就是UI给两张图片,一张默认的,一张点击的,然后我们实现一个selector.xml来实现点击切换的效果,如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3. <item android:state_pressed="true"
  4. android:drawable="@drawable/card_bg_selector" />
  5. <item android:drawable="@drawable/card_bg_default" />
  6. </selector>

这样写,是没有任何问题的,但是我们会发现,其实,大多数情况,这两张图card_bg_defaultcard_bg_selector,基本上没有差别,有的也就是颜色的变化,例如变暗,或者某些区域变色之类的,所以,问题来了:

有没有可能,只使用一张图片,来实现点击效果呢?

下面有几种方法,可以实现这个功能。

layer-list

如上图,我们就可以利用layer-list来实现这种点击变暗的效果,如下:

  1. <!--?xml version="1.0" encoding="utf-8"?-->
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3. <item android:state_pressed="true">
  4. <layer-list>
  5. <item android:drawable="@drawable/test" />
  6. <item>
  7. <shape android:shape="oval" >
  8. <solid android:color="#51000000" />
  9. </shape>
  10. </item>
  11. </layer-list>
  12. </item>
  13. <item android:drawable="@drawable/test" />
  14. </selector>

在pressed为true的状态时,使用了layer-list对应的item,里面包含了一张图片和一个shape实现的半透明覆盖层,这样点击的时候效果就是图片变暗从而实现点击效果。

在来一个例子,看下图:

点击的时候,有一个缩小的动画,也可以用layer-list来实现,如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3. <item android:state_pressed="true">
  4. <layer-list>
  5. <item android:drawable="@drawable/test"
  6. android:top="3dp"
  7. android:bottom="3dp"
  8. android:left="3dp"
  9. android:right="3dp"/>
  10. </layer-list>
  11. </item>
  12. <item android:drawable="@drawable/test" />
  13. </selector>

PorterDuffColorFilter

  1. PorterDuffColorFilter filter = new PorterDuffColorFilter(color, mode);
  2. Drawable drawable = getDrawable(R.mipmap.ic_launcher);
  3. drawable.setColorFilter(filter);

关于这个Mode,就是指PorterDuff.Mode,具体value和效果如下:

这里需要理解src和dst的意思

Src为源图像,意为将要绘制的图像
Dst为目标图像,意为我们将要把源图像绘制到的图像

在我们这个代码里面,src指color,dst就是R.mipmap.ic_launcher

所以,我们如果要实现改变icon颜色的功能,就应该是用SrcIn,所以代码更改如下:

  1. PorterDuffColorFilter filter = new PorterDuffColorFilter(
  2. color,
  3. PorterDuff.Mode.SRC_IN);
  4. Drawable drawable = getDrawable(R.mipmap.ic_launcher);
  5. drawable.setColorFilter(filter);

Tint

Tint,即为着色。
最常见的,当我们开发 App 的时候,ActionBar 或者 Toolbar中设置的 colorPrimary, colorPrimaryDark等这些颜色,就全是 Tint 的功劳了!

在Android 5.0(API 级别 21)之后,android提供了setTint()来给 BitmapDrawableNinePatchDrawable 对象着色,也可以使用android:tint 以及 android:tintMode属性设置您的布局中的着色颜色和模式,这个模式就是我们上面讲的PorterDuff.Mode

如何兼容
Android Support V4 的包中提供了 DrawableCompat 类,我们可以利用这个兼容类,来实现着色。

  1. public static Drawable tintDrawable(Drawable drawable,
  2. ColorStateList colors) {
  3. final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
  4. DrawableCompat.setTintList(wrappedDrawable, colors);
  5. return wrappedDrawable;
  6. }

使用例子如下:

  1. EditText editText1 = (EditText) findViewById(R.id.edit_1);
  2. final Drawable originalDrawable = editText1.getBackground();
  3. final Drawable wrappedDrawable = tintDrawable(originalDrawable, ColorStateList.valueOf(Color.RED));
  4. editText1.setBackgroundDrawable(wrappedDrawable);

这种方式支持几乎所有的 Drawable 类型,并且能够完美兼容几乎所有的 Android 版本

ColorStateList

使用 ColorStateList 着色,这样我们还可以根据 View 的状态着色成不同的颜色。
修改上面EditText的例子,获取焦点的时候变色。新建一个edittext_tint_colors.xml,如下:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">
  3. <item android:color="@color/red" android:state_focused="true" />
  4. <item android:color="@color/gray" />
  5. </selector>

修改代码:

  1. editText2.setBackgroundDrawable(
  2. tintDrawable(
  3. editText2.getBackground(),
  4. getResources().getColorStateList(R.color.edittext_tint_colors)
  5. ));

BitmapDrawable问题

我们举个例子来演示一个问题
测试用的icon,如下:

接下来,我们新建两个ImageView,一个不做任何处理,一个着色,如下:

  1. ImageView imageView01 = (ImageView) findViewById(R.id.image_1);
  2. final Drawable originalBitmapDrawable = getResources().getDrawable(R.drawable.icon);
  3. imageView01.setImageDrawable(originalBitmapDrawable);
  4. ImageView imageView02 = (ImageView) findViewById(R.id.image_1);
  5. originalBitmapDrawable = getResources().getDrawable(R.drawable.icon);
  6. imageView02.setImageDrawable(tintDrawable(originalBitmapDrawable, ColorStateList.valueOf(Color.MAGENTA)));

效果如下:

WTF!!! 怎么都变色?
简而言之,这是因为 Android 为了优化系统性能,资源 Drawable 只有一份拷贝,你修改了它,等于所有的都修改了。如果你给两个 View 设置同一个资源,它的状态是这样的:

看图可以知道,这两个ImageView的drawable是共享状态的,所以drawable的state改变了,会影响所以使用这个drawable的ImageView。

不过,Drawable 提供了一个方法mutate(),来打破这种共享状态,等于就是要告诉系统,我要修改(mutate)这个 Drawable。给 Drawable 调用 mutate() 方法以后。他们的关系就变成如下的图所示:

我们修改一下工具类方法tintDrawable()

  1. public static Drawable tintDrawable(Drawable drawable,
  2. ColorStateList colors) {
  3. Drawable newDrawable = drawable.getConstantState()
  4. .newDrawable()
  5. .mutate();
  6. final Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
  7. DrawableCompat.setTintList(wrappedDrawable, colors);
  8. return wrappedDrawable;
  9. }

Drawwable和ConstantState

上面说的那个问题,我们说是因为

Android 为了优化系统性能,资源 Drawable 只有一份拷贝

这个从源码中可以印证:

  1. @Nullable
  2. Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
  3. ...
  4. // First, check whether we have a cached version of this drawable
  5. // that was inflated against the specified theme.
  6. if (!mPreloading) {
  7. final Drawable cachedDrawable = caches.getInstance(key, theme);
  8. if (cachedDrawable != null) {
  9. return cachedDrawable;
  10. }
  11. }
  12. ...
  13. }

可以看出来,源码中会先判断是否缓存了该resIdTheme所对应的Drawable,如果缓存了,就返回了缓存这的对象,这也是getResources.getDrawable()方法返回同一个Drawable对象的原因。

我们上面也解决了这个问题了,里面涉及到了ConstantState,我们详细了解一下。如果把Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。

每个 Drawable 类对象类都关联有一个ConstantState类对象,这是为了保存 Drawable 类对象的一些恒定不变的数据,如果从同一个 res 中创建的 Drawable 类对象,为了节约内存,它们会共享同一个 ConstantState 类对象。

关键的源码如下:

  1. public abstract class Drawable {
  2. public ConstantState getConstantState() {
  3. return null;
  4. }
  5. public Drawable mutate() {
  6. return this;
  7. }
  8. public static abstract class ConstantState {
  9. public abstract Drawable newDrawable();
  10. public Drawable newDrawable(Resources res) {
  11. return newDrawable();
  12. }
  13. public Drawable newDrawable(Resources res, Theme theme) {
  14. return newDrawable(null);
  15. }
  16. public abstract int getChangingConfigurations();
  17. public boolean canApplyTheme() {
  18. return false;
  19. }
  20. }
  21. }

从源码中看,ConstantState其实是Drawable中的静态抽象类,并且getConstantState()方法默认返回null,这我们可以推测ConstantState类和getConstantState()方法会被具体的·Drawable·实现类继承和重写。这样不难想象,由于不同Drawbale,如BitmapDrawableGradientDrawable这些自身存储的绘制内容数据原则上是不一样的,这就意味着Drawable为了易于扩展,ConstantState对象应该会存储属性可变的数据。

比如一个 ColorDrawable 类对象,它会关联一个 ColorState 类对象,color 的颜色值是保存在 ColorState 类对象中的。如果修改 ColorDrawable 的颜色值,会修改到 ColorState 的值,会导致和 ColorState 关联的所有的 ColorDrawable。 的颜色都改变。就向我们上面遇到的问题一样,所有的drawable都会改变。
接下来,我们具体看一下ColorDrawable

  1. public class ColorDrawable extends Drawable {
  2. @Override
  3. public Drawable mutate() {
  4. if (!mMutated && super.mutate() == this) {
  5. mColorState = new ColorState(mColorState);
  6. mMutated = true;
  7. }
  8. return this;
  9. }
  10. @Override
  11. public void setTintList(ColorStateList tint) {
  12. mColorState.mTint = tint;
  13. mTintFilter = updateTintFilter(mTintFilter, tint,
  14. mColorState.mTintMode);
  15. invalidateSelf();
  16. }
  17. @Override
  18. public ConstantState getConstantState() {
  19. return mColorState;
  20. }
  21. final static class ColorState extends ConstantState {
  22. ColorStateList mTint = null;
  23. @Override
  24. public Drawable newDrawable() {
  25. return new ColorDrawable(this, null, null);
  26. }
  27. @Override
  28. public Drawable newDrawable(Resources res) {
  29. return new ColorDrawable(this, res, null);
  30. }
  31. @Override
  32. public Drawable newDrawable(Resources res, Theme theme) {
  33. return new ColorDrawable(this, res, theme);
  34. }
  35. }
  36. private ColorDrawable(ColorState state,
  37. Resources res,
  38. Theme theme) {
  39. if (theme != null && state.canApplyTheme()) {
  40. mColorState = new ColorState(state);
  41. applyTheme(theme);
  42. } else {
  43. mColorState = state;
  44. }
  45. mTintFilter = updateTintFilter(mTintFilter, state.mTint,
  46. state.mTintMode);
  47. }
  48. }

其实通过源码我们注意到,setTintList()其实就是设置的ColorState里面的成员变量,所以说,如果修改 ColorDrawable 的颜色值,会修改到 ColorState 的值,Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。
ColorStatenewDrawable()就是调用了一个私有的构造方法,传入了当前ColorDrawableColorState,在mutate()里面,也就是new了一个ColorState对象,重新赋值给当前ColorDrawablemColorState,也就是deepCopy了一份当前的数据。
所以说,我们先调用newDrawable(),然后会把当前的ColorDrawable1mColorState传递给新new的ColorDrawable2,然后我们调用ColorDrawable2mutate(),就会重新new一个ColorState,但是这些ColorState数据都是一样的,就实现了一个DeepCopy。

就像这张图所示的:

总结

  1. PorterDuff.Mode中,Src为源图像,意为将要绘制的图像,Dst为目标图像,意为我们将要把源图像绘制到的图像。
  2. Android Support V4 的包中提供了 DrawableCompat 类,我们可以利用这个兼容类,来实现着色。
  3. Drawable比作一个绘制容器,那么ConstantState就是容器中真正的内容。ConstantStateDrawable的静态内部类。具体的实现都在Drawable子类里面
  4. newDrawable()会新new一个Drawable,参数是当前DrawableConstantState
  5. mutate()会新new一个ConstantState,参数是当前的ConstantState

参考

Android开发中使用一张图片实现变暗或缩小的点击效果
自定义控件其实很简单1/6
Drawable 着色的后向兼容方案
View绘制Drawable原理分析记录

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注