Android Drawable 详解

1、Drawable 简介

Drawable——可简单理解为可绘制物,表示一些可以绘制在 Canvas 上的对象。在日常的工作开发中,我们为 UI 配置背景、图片、动画等等界面效果的时候,需要和众多的 Drawable 打交道。每种 Drawable 的适用范围不同,我们有必要了解每种 Drawable 的特点以及使用方式,才能在工作中得心应手,少走弯路。

具体的配置、使用方法以及最终的界面效果大家可以在本文的附件里面看到。Drawable 在 Android 中的继承关系如下,其中,红框标注的几种 Drawable 是我们在开发中比较常用的一些:

常用 Drawable

Drawable 中比较重要的方法有以下几种:

Drawable
    |- createFromPath
    |- createFromResourceStream
    |- createFromStream
    |- createFromXml
    |
    |- inflate   : 从XML中解析属性,子类需重写
    |- setAlpha  : 设置绘制时的透明度
    |- setBounds : 设置Canvas为Drawable提供的绘制区域
    |- setLevel  : 控制Drawable的Level值,这个值在ClipDrawable、RotateDrawable、ScaleDrawable、AnimationDrawable等Drawable中有重要作用;区间为[0, 10000]
    |- draw(Canvas) : 绘制到Canvas上,子类必须重写

其中,比较重要的方法是inflatedrawinflate 方法用于从 XML 中读取 Drawable 的配置,draw 方法则实现了把一个 Drawable 确切的绘制到一个 Canvas 上面——draw 方法为一个abstract抽象方法,子类必须进行重写。inflate 方法在Drawable.createFromXmlInner中被调用:

createFromXmlInner

我们可以看出,在从 XML 中创建一个 Drawable 时,步骤如下:

  1. 先根据 XML 节点名称来决定创造什么类型的 Drawable;然后 new 出相应的 Drawable;
  2. 再为该 Drawable 调用 inflate 方法,让其把配置加载起来——因为每种 Drawable 会重写 inflate 方法,所以,可以正确加载到各项配置及属性。XML 的配置我们稍后再讲。

setAlpha方法用于设置一个 Drawable 的透明度,setBounds用来指定当执行绘制时,在 Canvas 上的位置和区域。比如我们自定义一个 View,在其onDraw中绘制一个BitmapDrawable,我们设置了 BitmapDrawable 的 Alpha 和 Bounds,代码如下:

Drawable baseDrawable = getResources().getDrawable(R.drawable.base);
baseDrawable.setAlpha(100);
baseDrawable.setBounds(10, 20, 500, 300);
imageContent.setDrawable(baseDrawable);

绘制后的表现如下:

alpha 绘制表现

上图中,第一个区域是正常绘制的,第二个我们为 Drawable 设置了Alpha和Bounds,可以看出,右边深蓝色的纯色部分为整个 Canvas 的大小,设置了 100 的 Alpha 透明度后,图片把后面深蓝色的颜色也给透过来了,并且 Bounds 决定了 Canvas 上绘制该 Drawable 的区域大小和位置。

2、ColorDrawable

接下来我们逐一介绍 Drawable,着重介绍几种常用的 Drawable。由于在开发中这些 Drawable 大多在 XML 中进行配置,所以我们结合 XML 的配置类介绍。先从ColorDrawable开始,这个应该是最简单的一种 Drawable 了,它用一个颜色值来表示

color
    |- color="#xxxxxx | @color/color_value"
    |

比如我们的一个 ColorDrawable 的 XML 配置如下,以<color>作为根节点:

<color
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#0000ff"/>

使用的时候和其他 Drawable 的使用方法类似,可以通过Resource.getDrawable来获取,或者在 XML 里面配置:

<RelativeLayout
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@drawable/blue_drawable"/>

这个 View 的界面表现如你所想,是一坨蓝色:

蓝色

Java代码实现:

Resources res = getResources();
ColorDrawable colorDrawable = new ColorDrawable();
colorDrawable.setColor(res.getColor(R.color.skin_black_item));

3、BitmapDrawable

BitmapDrawable<bitmap>作为根节点:

bitmap
    |- src="@drawable/res_id"
    |- antialias="[true | false]"
    |- dither="[true | false]"
    |- filter="[true | false]"
    |- tileMode="[disabled | clamp | repeat | mirror]"
    |- gravity="[top | bottom | left | right | center_vertical |
    |            fill_vertical | center_horizontal | fill_horizontal |
    |            center | fill | clip_vertical | clip_horizontal]"
    |

这个比较复杂一点了,我们逐一介绍各个属性:

  • src:表示该 BitmapDrawable 引用的位图,该图片为 png、jpg 或者 gif;
  • antialias:表示是否开启抗锯齿
  • dither:表示当位图和屏幕的像素配置不同时,是否允许抖动。比如一张位图的像素为 ARGB_8888 32 位色,而屏幕像素为 RGB_565;
  • filter:是否允许为位图进行滤波以获取平滑的缩放效果;
  • gravity:定义位图的 gravity,当位图小于容器时,该属性指定了位图在容器中的停靠位置绘制方式
  • tileMode:表示当位图小于容器时,执行“平铺”模式,并且指定铺砖的方法。该属性覆盖 gravity 属性——当指定了该属性后,gravity 属性即使设置了,也将不起作用

其中,gravitytileMode这两个属性比较有意思,我们着重来进行介绍。gravity 的默认值为fill——亦即在水平和垂直方向均进行缩放,使得图片可以填充到整个 View 里面。

比如我们有一张如下的图片:

car

为了比较好的展现clamp 钳位模式,注意这张图,我们在右边缘和下边缘用了黑白交替的边线。我们的 XML 配置极其简单,以<bitmap>作为根节点:

<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/car"
    android:tileMode="repeat"/>

当这个 BitmapDrawable 放入一个比它大的容器中时,tileMode 就起作用了:

  1. repeat模式:将重复贴该图,直到填充完容器:
repeat
  1. clamp模式:钳位模式,将沿用下边、右边边缘的像素值分水平、垂直两个方向扩展填充剩余位置:
clamp
  1. mirror模式:镜像模式,将按水平、垂直镜像重复来填充剩余位置:
mirror
  1. disabled:禁用任何填充方法,将使用整个位图进行缩放填充。
disabled

我们接着来看 gravity 属性,该属性也比较容易理解:

  1. top:在顶部水平中心绘制;其他类如 left、right、bottom 和 top 类似;
top

当然,我们可以使用“|”来组合,达到特殊的效果,比如当 gravity 为bottom|right时,表现如下:

bottom|right
  1. center_horizontalcenter_vertical将在水平、垂直两个方向上居中。当单独使用 top/left/right/bottom 四个值时,默认带了这两个中的值:比如 top == top|center_horizontal

  2. fill_horizontalfill_vertical将在水平、垂直两个方向上进行缩放填充,默认也是带了center_horizontal或者center_vertical这两个值的;

下面是“fill_vertical”的表现:

fill_vertical

下面是“fill_vertical|left”的表现:

fill_vertical|left
  1. clip_horizontalclip_vertical将在 Drawable 比容器大时,按水平、垂直方向进行裁剪,下面是 gravity 为“clip_vertical”的情况,可以看出,裁剪了小汽车的首尾:
clip_vertical

在实际的开发中,我们要活用这些 gravity 的值,可以通过“|”来获取各种想要的效果。

Java代码实现:

Resources res = getResources();
Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.adt_48);
BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bmp);
bitmapDrawable.setTileModeX(TileMode.MIRROR);
bitmapDrawable.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);

4、NinePatchDrawable

4.1、.9.png图片资源

这是一种比较高端的 Drawable。其实就是九宫贴图——这种 Drawable 契合了 Android 中的“.9.png”文件。这种图片资源的特点在于:

  1. 在一张普通的 png 图片四周,分别向外扩展了一个像素;
  2. 用这些扩展的像素,可以描边,描边用来规定可缩放区域内容padding区域
4.1.1、.9.png的扩展区域

比如我们现在有一张 .9.png 图片如下:

pic

我们在四周看到了一像素的黑点,这些黑点分别在四周围成四个边线。四个圆角处都是透明的。那么,左、上两条边规定了当按钮被缩放时的可缩放区域。比如下面红色边框圈出的矩形内的区域,就是可缩放区域,这个区域外的区域,在执行缩放时均保留原来的像素比例。

patch

比如一个按钮各个角度拉伸,都可以保留圆角的圆润,而不会发生锯齿或者糊掉。我们的布局文件如下:

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    
    <Button
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:background="@drawable/btn_normal"/>

    <Button
        android:layout_width="150dp"
        android:layout_height="80dp"
        android:layout_marginLeft="10dp"
        android:background="@drawable/btn_normal"/>
</LinearLayout>

当 btn_normal 是一个 .9.png 文件时,界面表现如下:

.9

但如果我们的 background 对应的图片如果是一张非 .9.png 的原图,那么,界面表现有些糟,明显看出,圆角部分糊掉了,并且圆角看起来很诡异:

not .9
4.1.2、.9.png 的 padding 区域

这就是 .9.png 的妙处。这种格式图片的右、下两个边缘的像素点,规定了padding区域,也就是说,内容的绘制时的 padding:

padding

上面的红色边框圈出的矩形区域规定了内容绘制的区域。比如我们把上面的图片用作一个 Button 的 background 时,整个按钮的文本将明显偏上:

padding result

通过padding区域的控制,我们可以轻松实现一个按钮按下后文字也相应下移几个像素的点击效果。

4.1.3、.9.png 的制作

制作简单提一下,在 AndroidSDK 的安装目录中,tools 文件夹下有一个“draw9patch.bat”文件,启动该 bat,就相应打开了 .9.png 的制作工具:

draw9patch

我们把一个简单的png拖入这个窗口,就可以编辑了。用鼠标左键在边缘点击以点出像素点,用鼠标右键删除像素点。在右边可以实时预览绘制效果:

draw9patch UI

4.2、NinePatchDrawable 的 XML 配置

这种 Drawable 的 XML 节点表述如下,以<nine-patch>作为根节点:

nine-patch
    |- src="@drawable/9_png_resid"
    |- dither="[true | false]"
    |

其中,dither 属性,和之前 BitmapDrawable 中将的一样,就是像素配置不同时,是否允许抖动。src 比较重要,这个值指向的必须是一个“.9.png”格式的图片,否则,底层NinePatchDrawable.inflate方法在解析的时候,会抛出一个XmlPullParserException异常:

XmlPullParserException

我们可以看出,上图中bitmap.getNinePatchChunk这个方法,获取 9 宫的各项信息,如果从一个 Bitmap 对象中得不到这些信息,则表示这个图片非“.9.png”格式的图片,就抛出异常。
其实,“.9.png”的图片,本质上是一张普通的 png 图片。比如,我们有一张名为“btn_normal.9.png”的图片,可以在代码中这样使用:

View imageContent = findViewById(R.id.xxx);

Resources res = getResources();
NinePatchDrawable normal = (NinePatchDrawable) res.getDrawable(R.drawable.btn_normal);
imageContent.setBackground(normal);

4.3、NinePatchDrawable 与 .9.png 图片的映射

那么,Android 是怎样把这张图片映射为一个 NinePatchDrawable 的呢?原来,这张图片开始被当作普通的 Bitmap,从 Resources.getDrawable 方法中可以看出端倪:

getDrawable

在 getDrawable 中调用了loadDrawable,在 loadDrawable 方法中有一个缓存策略,我们先不管,直接看加载资源的部分:

loadDrawable

可以看出,对 XML 配置类型的 Drawable,使用loadXmlResourceParse加载,然后使用Drawable.createFromXml这个静态方法进行创建,得到 Drawable 对象。对于其他类型的 Drawable,先使用openNonAsset得到一个流对象,然后使用Drawable.createFromResourceStream这个静态方法进行创建。Drawable.createFromXml 这个方法最终会调用Drawable.createFromXmlInner,这个方法我们前面 Drawable 简介里面已经介绍过了。我们着重看 Drawable.createFromResourceStream 这个方法:

getNinePatchChunk

在这个方法中,我们先从流中解析得到一个 Bitmap 对象——这个对象本质上和其他所有类型的图片资源没任何区别。区别在于接下来调用的Bitmap.getNinePatchChunkNinePatch.isNinePatchChunk这两个方法,通过这两个方法的结合调用,可以判断这个 Bitmap 是否是一个合格的“.9.png”图片。接下来进入drawableFromBitmap

drawableFromBitmap

最后,根据九宫信息 np 这个参数是否为 null,来决定创建什么对象。可以看出,对“.9.png”格式的图片,最终会创建一个 NinePatchDrawable 对象,对于其他普通的 png、jpg 等图片,创建相应的 BitmapDrawable 对象。一切一目了然。

Java代码实现:

这部分我们使用draw9patch工具很容易制作,一般不会在代码中进行创建NinePatchDrawable对象,也不推荐在代码中这样做。

5、StateListDrawable

这个 Drawable 类型几乎是我们开发中最常用的类型了,为什么呢?因为它是根据一系列的状态来控制绘制表现的,这一系列状态契合了我们界面控件的各个状态。界面控件的状态一般有:获取焦点、失去焦点、普通状态、按下状态、可点击状态、不可点击状态、选中状态、未选中状态、勾选状态、未被勾选状态、激活状态、未被激活状态等等。

StateListDrawable<selector>作为根节点:

selector
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- state_pressed="[true | false]"
    |    |- state_focused="[true | false]"
    |    |- state_selected="[true | false]"
    |    |- state_hovered="[true | false]"
    |    |- state_checked="[true | false]"
    |    |- state_checkable="[true | false]"
    |    |- state_enabled="[true | false]"
    |    |- state_activated="[true | false]"
    |    |- state_window_focused="[true | false]"
    |

一个selector以多个item来组成,每个 item 由 0 个或者多个状态和一个 drawable 来表示,当控件的状态变化后,将根据控件当前的状态,来进行匹配,匹配一个最适合当前状态的 item,然后用这个 item 的 drawable 来进行绘制。
比如,我们一个普通按钮的 selector 如下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:drawable="@drawable/pressed_btn" />
    
    <item android:drawable="@drawable/normal_btn" />
</selector>

我们定义了一个按钮的普通状态和按下状态的 Drawable,使用方法如下:

<Button
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:textColor="#e22"
    android:background="@drawable/flat_button_drawable"
    android:text="Flat Button" />

那么,在普通状态和按下状态中,界面表现分别如下:

普通状态    

normal

按下状态    

pressed

Cool!除了按下状态的红色有点刺眼外,看起来还不错,是吧。其实,我们可以通过控件状态,来控制普通态、按下态的按钮文字颜色。我们新建一个 XML,放入res/color文件夹下,比如起名为 btn_text_color.xml:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:color="#fff"/>
    <item android:color="#e22"/>
</selector>

我们在Button中的配置如下,通过设置android:textColor来控制按钮的文本颜色:

<Button
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:textColor="@color/btn_text_color"
    android:background="@drawable/flat_button_drawable"
    android:text="Flat Button" />

现在,一个高大上的扁平化的按钮效果出炉了:

普通状态    

normal

按下状态    

pressed

在实际操作中,我们可能要为多种状态来进行设置,可以灵活运用:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:state_pressed="true"
        android:state_selected="false"
        android:drawable="@drawable/pressed_btn"/>

    <item android:drawable="@drawable/normal_btn"/>
</selector>

这样,第一个item将只匹配未被禁用且当前为按下状态且未被选中状态。其他状态均使用第二个item。

当然,不踩几个坑,怎么能做一名合格的开发者呢?

注意:

如果有不带任何状态的 item 的话,这个item一定要放在整个 item 列表的最下面。否则,所有的状态均可优先匹配到这个 item,其他 item 将得不到匹配。因为匹配的时候是一个遍历操作,如果遍历找到和当前状态符合的 Drawable,就直接返回。

Java代码实现:

Resources res = getResources();
StateListDrawable stateListDrawable = new StateListDrawable();
stateListDrawable.addState(
    new int[] {android.R.attr.state_pressed},
    res.getDrawable(R.drawable.blue_drawable));

stateListDrawable.addState(
    new int[] {
        android.R.attr.state_pressed,
        android.R.attr.state_enabled},
    res.getDrawable(R.drawable.bmp_drawable));

stateListDrawable.addState(
    new int[] {},
    res.getDrawable(R.drawable.bkgnd_normal));

6、ClipDrawable

ClipDrawable允许我们对一个 Drawable 进行剪裁操作,在绘制的时候只绘制剪裁的部分。这里最关键的是Drawable.setLevel方法在起作用,在为一些控件比如进度条、音量控制条等设置 UI 效果的时候,一般会使用 ClipDrawable,否则,你的进度在界面上将得不到刷新。

ClipDrawable以<clip>作为根节点:

  clip
    |- drawable="@drawable/drawable_id"
    |- clipOrientation="[horizontal | vertical]"
    |- gravity="[ ... ]"
    |

clipOrientation决定了裁剪的方向,默认为horizontal——表示水平方向剪裁;而 gravity 的取值和之前介绍的类似,结合 clipOrientation 决定了剪裁发生的位置——默认为left,就是当 clipOrientation 为 horizontal 时,剪裁发生在 drawable 的右侧。

最主要的绘制我们来看ClipDrawable.draw方法:

ClipDrawable.draw

根据 AndroidSDK 的规范,setLevel的 level 值在[0, 10000]这个区间内。可以看出,在绘制的时候,根据 level 值和 gravity 算出要剪裁的区域,然后在 Canvas 上执行 clipRect,从而达到剪裁效果。

XML的配置也很简单:

<clip
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/normal_btn"
    android:clipOrientation="vertical"
    android:gravity="top" />

Java代码实现:

Resources res = getResources();
NinePatchDrawable btnNormal = (NinePatchDrawable) res.getDrawable(R.drawable.btn_normal);
ClipDrawable clipDrawable = new ClipDrawable(
    btnNormal, Gravity.TOP, ClipDrawable.VERTICAL);
clipDrawable.setLevel(500);

我们后续结合LayerDrawable来看ClipDrawable在进度条等 UI 上的配置方式。

7、LayerDrawable

LayerDrawable可以将一组 Drawable 按 XML 中定义的顺序层叠起来进行绘制,并可以设定每层 Drawable 的 id、位置等等。ProgressBar这个控件的背景切图,可以通过 LayerDrawable 来进行配置。LayerDrawable 以<layer-list>作为根节点:

layer-list
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- id="@+id/xxx_id"
    |    |- top="dimension"
    |    |- left="dimension"
    |    |- right="dimension"
    |    |- bottom="dimension"
    |

每组 Drawable 由<item>节点进行配置,item 中 drawable 表示了这层 Drawale 引用的绘图资源 ID,id属性表示了这层 Drawable 的ID,top、left、right、bottom这四个属性发布表示与各个方向的间距。比如一个简单的 LayerDrawable 如下:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/red_color"
        android:bottom="10dp"
        android:left="10dp"
        android:right="10dp"
        android:top="10dp"/>
    <item
        android:drawable="@drawable/green_color"
        android:bottom="20dp"
        android:left="20dp"
        android:right="20dp"
        android:top="20dp"/>
    <item
        android:drawable="@drawable/blue_color"
        android:bottom="30dp"
        android:left="30dp"
        android:right="30dp"
        android:top="30dp"/>
</layer-list>

那么,绘制出来的效果如下(其中,灰色那一层是Activity的背景):

绘制结果

接下来我们看看LayerDrawable在ProgressBar中的配置:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@android:id/background"
        android:drawable="@drawable/file_background"/>
    
    <item android:id="@android:id/progress">
        <clip android:drawable="@drawable/file_progress"/>
    </item>
    
    <item android:id="@android:id/secondaryProgress">
        <clip android:drawable="@drawable/file_cache_progress"/>
    </item>
</layer-list>

可以看出,我们在配置的时候,分别为每个 item 指定了 id——这些 id 对应表示了 ProgressBar 中每种进度状态:background对应整个 ProgressBar 的背景,progress对应当前的进度背景,而secondaryProgress对应 secondaryProgress 的进度背景(一般我们用来做缓冲进度——和优酷视频的缓冲类似)。另外我们看出,结合使用了 ClipDrawable——因为 ProgressBar 的实现中,正是结合Drawable.setLevel来进行刷新进度的,在前面讲过,ClipDrawable 恰好在onDraw绘制中,对 Level 做了相应的处理:

setLevel

这里有一个方法:LayerDrawable.findDrawableByLayerId,这个方法可以获取 id 对应的 Drawable。

Java代码实现:

Resources res = getResources();
LayerDrawable layerDrawable = new LayerDrawable(
        new Drawable[] {
            res.getDrawable(R.drawable.red_color),
            res.getDrawable(R.drawable.green_color),
            res.getDrawable(R.drawable.blue_color)
        });

layerDrawable.setId(0, R.id.action_settings);
layerDrawable.setId(1, R.id.switchBtn);
layerDrawable.setLayerInset(0, 10, 10, 10, 10);
layerDrawable.setLayerInset(1, 20, 20, 20, 20);

8、AnimationDrawable

8.1、AnimationDrawable 的使用

借助AnimationDrawable,我们可以轻松实现基于一系列 Drawable 帧的动画效果。AnimationDrawable 提供了一系列简单易用的接口来帮助我们:

AnimationDrawable
   |- setOneShot : 设置动画是否单次播放,默认为false,表示不循环
   |- start : 开始播放动画,如果已经在播放中,则不起作用
   |- end : 结束播放
   |

一般我们在 XML 里面进行配置动画,代码中手工写的方式不推荐。AnimationDrawable 以<animation-list>作为根节点:

animation-list
    |- oneshot="[true | false]"
    |- visible="[true | false]"
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- duration="xms"
    |

animation-list 节点内的oneshot属性表示该动画是否只播放一次,当这个值为 false 的时候,表示循环播放——这是默认值。其他的一系列动画效果,均由一组<item>节点来进行配置,item 中的duration表示这一帧和上一帧的时间间距,以 ms 为单位。比如我们有一个简单的动画配置如下:

<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    
    <item
        android:drawable="@drawable/red_color"
        android:duration="500"/>
    
    <item
        android:drawable="@drawable/green_color"
        android:duration="500"/>
    
    <item
        android:drawable="@drawable/blue_color"
        android:duration="500"/>
</animation-list>

我们可以将该动画效果施加到一个 View 上:

View imageContent = findViewById(R.id.xxx);

AnimationDrawable drawable = (AnimationDrawable) res.getDrawable(R.drawable.animation_drawable);
imageContent.setBackground(drawable);
drawable.start();

这样,我们在这个 View 上可以看到每隔 500ms 便变换一次颜色的动画效果。当然,这只是一个 demo,利用 AnimationDrawable,我们可以做出更酷的动画。

我们一般在 XML 里面配置 AnimationDrawable,通过 Resources.getDrawable 方法来获取它。虽然我们不推荐在代码里面手工创建 AnimationDrawable,但万一哪天你需要它呢?

Java代码实现:

Resources res = getResources();
AnimationDrawable animationDrawable = new AnimationDrawable();
animationDrawable.addFrame(res.getDrawable(R.drawable.red_color), 500);
animationDrawable.addFrame(res.getDrawable(R.drawable.green_color), 500);
animationDrawable.addFrame(res.getDrawable(R.drawable.blue_color), 500);

animationDrawable.setOneShot(false);
imageContent.setBackground(animationDrawable);
animationDrawable.start();

8.2、AnimationDrawable 的原理

我们只是把一个 AnimationDrawable 塞入了一个 View 的 background 中,那么这些动画的变换,是怎么响应到 View 上的呢?原来,这一切都是Drawable.Callback这个回调在起作用:

Callback

我们通过 Drawable.setCallback 来设置一个Callback,这个 Callback 中有三个方法:

  • invalidateDrawable:重绘 Drawable;
  • scheduleDrawable:在 when 规定的 ms 后,执行 what 这个Runnable;(这里可以看出动画的端倪了)
  • unscheduleDrawable:异步执行这个 what;用来结束动画等。

View 类实现了 Drawable.Callback 这个接口,在我们调用View.setBackground方法为 View 设置背景的时候,会把 View 的 this 塞入 Drawable 中作为 Callback:

Drawable.Callback
setCallback

而在 AnimationDrawable 自己实现了Runnable这个接口,在run方法中,通过调用AnimationDrawable.nextFrame方法,提供了动画帧的切换终止判断等操作。

setFrame
scheduleSelf

在这里首先使用selectDrawable把对应帧的 Drawable 选为激活的,然后在scheduleSelf中,通过调用Drawable.Callback.scheduleDrawable这个 Callback 方法,可以达到动画帧按时间间隔切换的效果。

9、其他 Drawable 及总结

基本上我们在工作中最常用的几类 Drawable 如上所示。其他的一些 Drawable 有时也会用到,也很有趣。比如ShapeDrawableRotateDrawableScaleDrawable以及InsetDrawable。这些 Drawable 可以在工作中确实需要用到的时候去参考 SDK 进行学习和灵活运用,在这里简单介绍下这几种 Drawable 的作用和使用方法,以及一些效果截图。

9.1、ShapeDrawable

通过在 XML 中配置 ShapeDrawable,我们可以轻松绘制矩形、线段、圆角矩形、渐变等图形作为 background 而不需要切图。ShapeDrawable 以<shape>作为根节点;需要熟悉子节点的有:cornersgradientpaddingsizesolidstroke等;比如下面是一个简单的配置及效果展现:

<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <corners android:radius="5dp"/>

    <gradient
        android:angle="90"
        android:endColor="#ddd"
        android:startColor="#343434"
        android:type="linear"/>
    
    <stroke
        android:width="2dp"
        android:color="#00f"/>
</shape>

我们通过android:shape指定这个 shape 是一个矩形(rectangle),用子节点corners为矩形加上圆角,使之变成一个圆角矩形;再使用gradient子节点来施加一个渐变效果,渐变的类型用android:type指定为线性渐变(linear);最后再使用stroke子节点为整个图形加上一个 2dp 宽的蓝色外边框。其效果图如下:

shape
9.2、RotateDrawable

RotateDrawable可以结合当前 Drawable 的 level 值,进行旋转。level 值每增加一,其旋转角度旋转(toDegrees – fromDegrees) / 10000。比如下图是一张正常的图片:

robot

我们通过一个 XML 进行旋转,其中android:fromDegreesandroid:toDegrees确定了旋转的起始角度和终止角度,android:pivotXandroid:pivotY确定了旋转中心点的位置:

<rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/android_robot"
    android:fromDegrees="0"
    android:toDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%"/>

我们在 UI 线程中控制这个 RotateDrawable 的 Level 值,可以获得一个旋转的动画效果:

RotateDrawable 例子

我们截取了部分动画效果的过程,如下:

RotateDrawable 示例运行截图
9.3、ScaleDrawable

ScaleDrawable可以结合当前 Drawable 的 level 值,进行图片的缩放,同样结合HandlerTimer,我们可以得到一个简单的缩放动画。

9.4、InsetDrawable

InsetDrawable可以把一个 drawable 资源嵌入到其他的资源内部,并且在四周可以留下边距。比如我们有时候需要一个左右各留白 15dp 的ListView的分隔线,我们可以用 InsetDrawable 来做。为什么不使用切图的方式来留白呢——注意,我们这里要求是 15dp,而不是 15pixel,如果切图的话,只能用像素单位留白,但这导致在不同的设备上可能用户看到的留白的间距不统一

<inset
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/seperator_line"
    android:insetLeft="15dp"
    android:insetRight="15dp"/>

我们应用到一个 ListView 的分隔符上:

<ListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/inset_drawable"
    android:dividerHeight="1dp"/>

这样,我们得到了一个首尾均留白 15dp 的分隔符,整个界面效果展现如下(灰色背景部分是整个 ListView 的轮廓):

ListView 结果

当然,还有其他诸如TransitionDrawableLevelListDrawableGradientDrawablePictureDrawablePaintDrawable没有详细介绍,但这几种一般不是很常用。经过前面一些 Drawable 的简介,即时我们在工作中需要用到这几类 Drawable,也可以轻松通过查看文档等方式来学习和使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容