Drawable 的分类

BitmapDrawable

       这几乎是最简单的 Drawable 了,它表示的就是一张图片。在实际开发中,我们可以直接引用原始的图片即可,但是也可以通过 XML 的方式来描述它,通过 XML 来描述的 BitmapDrawable 可以设置更多的效果,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<!--?xml version="1.0" encoding="utf-8"?-->
<bitmap
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:antialias=["true"|"false"]
android:dither=["true"|"false"]
android:filter=["true"|"false"]
android:gravity=["top"|"bottom"|"left"|"right"|"center_vertical"|
"fill_vertical"|"center_horizontal"|"fill_horizontal"|
"center"|"fill"|"clip_vertical"|"clip_horizontal"]
android:mipMap=["true"|"false"]
android:titleMode=["disable"|"clamp"|"repeat"|"mirror"]/>

       下面是它各个属性的含义:

android:src

       引用一个 drawable resource,就是图片的资源id。

android:antialias

       是否开启图片抗锯齿功能。开启后会让图片变得平滑,同时也会在一定程度上降低图片的清晰度,但是这个降低的幅度较低以至于可以忽略,因此抗锯齿选项应该开启。

android:dither

       是否开启抖动效果。当图片的像素配置和手机屏幕的像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果,比如图片的色彩模式为 ARGB8888,但是设备屏幕所支持的色彩模式为 RGB555,这个时候开启抖动选项可以让图片显示不会过于失真。在 Android 中创建的 Bitmap 一般会选用 ARGB8888 这个模式,即 ARGB 四个通道各占8位,在这种色彩模式下,一个像素所占的大小为4个字节,一个像素的位数总和越高,图像也就越逼真。根据分析,抖动效果也应该开启。

android:filter

       是否开启过滤效果。当图片尺寸被拉伸或者压缩时,开启过滤效果可以保持较好的显示效果,因此此选项也应该开启。

android:gravity

       当图片小于容器的尺寸时,设置此项可以对图片进行定位。这个属性的可选项比较多,不同的选项可以通过“|”来组合使用,如下表所示:

可选项 含义
top 将图片放在容器的顶部,不改变图片的大小
bottom 将图片放在容器的底部,不改变图片的大小
left 将图片放在容器的左边,不改变图片的大小
right 将图片放在容器的右边,不改变图片的大小
center_vertical 使图片竖直居中,不改变图片的大小
center_horizontal 使图片水平居中,不改变图片的大小
center 使图片在水平和竖直方向同时居中,不改变图片的大小
fill_vertical 使图片竖直方向填充容器
fill_horizontal 使图片水平方向填充容器
fill 使图片在水平和竖直方向均填充容器,这是默认值
clip_vertical 附加选项,表示竖直方向的裁剪,较少使用
clip_horizontal 附加选项,表示水平方向的裁剪,较少使用

android:mipMap

       这是一种图像相关的处理技术,也叫纹理映射,比较抽象,这里也不对其深究了,默认值为 false,在日常开发中此选项不常用。

android:tileMode

       平铺模式。这个选项有如下几个值:[“disabled” | “clamp” | “repeat” | “mirror”],其中 disable 表示关闭平铺模式,这也是默认值,当开启平铺模式后,gravity 属性会被忽略。这里主要说一下 repeat、mirror 和 clamp 的区别,这三者都表示平铺模式,但是它们的表现却有很大不同。repeat 表示的是简单的水平和竖直方向上的平铺效果;mirror 表示一种在水平和竖直方向上的镜面投影效果;而 clamp 表示的效果就更加奇特,图片四周的像素会扩展到周围区域。下面看一下这三者的实际效果:

BitmapDrawable平铺模式下的图片显示效果

       详见 bitmap_drawablebitmap_drawable_clampbitmap_drawable_mirroractivity_main

       代码应用案例如下(建议使用 xml 实现):

1
2
3
4
5
6
7
Bitmap mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image1);
BitmapDrawable mBitmapDrawable = new BitmapDrawable(getResources(),mBitmap);
mBitmapDrawable.setTileModeXY(TileMode.MIRROR, TileMode.MIRROR);//平铺方式
mBitmapDrawable.setAntiAlias(true);//抗锯齿
mBitmapDrawable.setDither(true);//防抖动
//设置到imageView上即可
imageView.setImageDrawable(mBitmapDrawable);

NinePatchDrawable

       它表示的是一张 .9 格式的图片,.9 图片可以自动地根据所需的宽/高进行相应的缩放并保证不失真,它和 BitmapDrawable 都表示一张图片。 和 BitmapDrawable 一样,在实际使用中直接引用图片即可,但是也可以通过 XML 来描述 .9 图,如下所示:

1
2
3
4
5
<!--?xml version="1.0" encoding="utf-8"?-->
<nine-patch
xmlns:android="https://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:dither=["true"|"false"]/>

       上述 XML 中的属性的含义 和 BitmapDrawable 中的对应属性的含义是相同的,这里就不再描述了,另外,在实际使用中发现在 bitmap 标签中也可以使用 .9 图,即 BitmapDrawable 也可以代表一个 .9 格式的图片。

       NinePatchDrawable 一般用来动态加载 .9 图,比如加载 sd 卡上的.9 图。

ShapeDrawable

       ShapeDrawable 是一种很常见的 Drawable,可以理解为通过颜色来构造的图形,它既可以是纯色的图形,也可以是具有渐变效果的图形。ShapeDrawable 的语法稍显复杂,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!--?xml version="1.0" encoding="utf-8"?-->
<shape
xmlns:android="https://schemas.android.com/apk/res/android"
android:shape=["rectangle"|"oval"|"line"|"ring"]>
  <corners
android:radius="integer"
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer"/>
<gradient
android:angle="integer"
android:centerX="integer"
android:centerY="integer"
android:centerColor="color"
android:endColor="color"
android:gradientRadius="integer"
android:startColor="color"
android:type=["linear"|"radial"|"sweep"]
android:useLevel=["true"|"false"]/>
<padding
android:left="integer"
android:top="integer"
android:right="integer"
android:bottom="integer"/>
<size
android:width="integer"
android:height="integer"/>
<solid
android:color="color"/>
<stroke
android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer"/> 
</shape>

       需要注意的是<shape>标签创建的 Drawable,其实体类实际上是 GradientDrawable,下面分别介绍各个属性的含义。

shape

       表示图形的形状,有四个选项:rectangle(矩形)、oval(椭圆)、line(横线) 和 ring(圆环)。它的默认值是矩形,另外 line 和 ring 这两个选项必须要通过<stroke>标签来指定线的宽度和颜色等信息,否则将无法达到预期的显示效果。

       针对 ring 这个形状,有 5 个特殊的属性,它们的含义如下所示:

可选项 含义
android:innerRadius 圆环的内半径,和 android:innerRadiusRatio 同时存在时,以 android:innerRadius 为准。
android:thickness 圆环的厚度,即外半径减去内半径的大小,和 android:thicknessRatio 同时存在时,以 android:thickness 为准。
android:innerRadiusRatio 内半径占整个 Drawable 宽度的比例,默认值为 9。如果为 n,那么内半径 = 宽度 / n
android:thicknessRatio 厚度占整个 Drawable 宽度的比例,默认值为 3,如果为 n,那么厚度 = 宽度 / n
android:useLevel 一般都应该使用 false,否则有可能无法达到预期的显示效果,除非它被当作 LevelListDrawable 来使用

<corners>

       表示 shape 的四个角的角度。它只适用于矩形 shape,这里的角度是指圆角的程度,用 px 来表示,它有如下 5 个属性:

  • android:radius —— 为四个角同时设定相同的角度,优先级较低,会被其它四个属性覆盖。
  • android:topLeftRadius —— 设定左上角的角度;
  • android:topRightRadius —— 设定右上角的角度;
  • android:bottomLeftRadius —— 设定左下角的角度;
  • android:bottomRightRadius —— 设定右下角的角度;

<gradient>

       它与<solid>标签是互相排斥的,其中 solid 表示纯色填充,而 gradient 则表示渐变效果,gradient 有如下几个属性:

  • android:angle —— 渐变的角度,默认值为 0,其值必须为 45 的倍数,0表示从左到右,90 表示从下到上,角度会影响渐变的方向;
  • android:centerX —— 渐变的中心点的横坐标;
  • android:centerY —— 渐变的中心点的纵坐标,渐变的中心点会影响渐变的具体效果;
  • android:startColor —— 渐变的起始色;
  • android:centerColor —— 渐变的中间色;
  • android:endColor —— 渐变的结束色;
  • android:gradientRadius —— 渐变半径,仅当 android:type=”radial” 时有效;
  • android:useLevel —— 一般为 false,当 Drawable 作为 StateListDrawable 使用时为 true;
  • android:type —— 渐变的类别,有 linear(线性渐变)、radial(径向渐变)、sweep(扫描线渐变)三种,其中默认值为线性渐变。

       angle=0和angle=90的区别(都为线性渐变):

ShapeDrawable颜色渐变1

       linear(线性)为默认值,radial(径内渐变),sweep(扫描渐变)区别如下:

ShapeDrawable颜色渐变2

       详见 drawable 中的相关代码。

       实现一个环形进度圈的案例,我们将shape属性设置为ring(圆环),然后再设置其内半径以及环的厚度,并设置渐变色调,代码见:ring_progress_drawable

       接着将该自定义环形圈设置给一个旋转动画,并利用该旋转动画自定义成一个环形进度圈的 style,最后将该自定义的 style 赋值给 Progress 组件。自定义旋转动画:ring_rotate_drawable,自定义 Progress 的 style:CustomProgressStyle,应用到 Progress 组件,效果如下:

ShapeDrawable的环形进度效果图

<solid>

       这个标签表示纯色填充,通过 android:color 即可指定 shape 中填充的颜色。

<stroke>

       Shape 的描边,有如下几个属性:

  • android:width —— 描边的宽度,数值越大则 shape 的边缘线就会看起来越粗;
  • android:color —— 描边的边框颜色;
  • android:dashWidth —— 组成虚线的线段的宽度;
  • android:dashGap —— 组成虚线的线段之间的间隔,间隔越大则虚线看起来空隙就越大。
1
注意:如果 android:dashWidth 和 android:dashGap 有任何一个为 0,那么虚线效果将不能生效。

<padding>

       这个表示空白,但是它表示的不是 shape 的空白,而是包含它的 View 的空白,即为内容设置与当前应用此 shape 的 View 的边距,而不是设置当前 View 与父元素的边距。有四个属性:android:left、android:top、android:right 和 android:bottom。

<size>

       shape 的大小,有两个属性:android:width 和 android:height,分别表示 shape 的宽/高。这个表示的是 shape 的固有大小,但是一般来说它并不是 shape 最终显示的大小,这个有点抽象,但是我们要明白,对于 shape 来说它并没有 宽/高 的概念,作为 View 的背景它会自适应 View 的 宽/高。我们知道 Drawable 的两个方法 getIntrinsicWidth 和 getIntrinsicHeight 表示的是 Drawable 的固有宽/高,对于有些 Drawable 比如图片来说,它的固有宽/高就是图片的尺寸。而对于 shape 来说,默认情况下它是没有固有宽/高这个概念的,这个时候 getIntrinsicHeight 和 getIntrinsicWidth 会返回 -1,但是如果通过<size>标签来指定宽/高信息,那么这个时候 shape 就有了所谓的固有宽/高。因此,总结来说,<size>标签设置的宽/高就是 ShapeDrawable 的固有宽/高,但是作为 View 的背景时,shape 还会被拉伸或者缩小为 View 的大小。

       下面显示了一个红色带数字的小圆圈提示,shape 属性设置为 oval,并设置其纯填充颜色为红色,给一个临时大小宽高大小相同(之所以称为临时大小,是因为其最终大小由使用的 View 决定的),这样一个圆形背景图就出现了,详见 remind_num_drawable,应用到 TextView 中。

       最终效果如下:

ShapeDrawable红色带数字的小圆圈提示

       实际上在开发中经常会利用 shapeDrawable 来自定义出所需要的各种背景图像或者显示图片,同时也有益于减少对美工图片的依赖,另外一个好处通过自定义 shapeDrawable 图片会比美工图片的 size 小很多,这样就能减少不必要的 size,以减轻 ap k的大小。

LayerDrawable

       LayerDrawable 对应的 XML 标签是<layer-list>,它表示一种层次化的 Drawable 集合,通过将不同的 Drawable 放置在不同的层上面从而达到一种叠加后的效果。LayerDrawable 的 drawable 资源按照列表的顺序绘制,列表的最后一个 drawable 绘制在最上层。它的语法如下所示:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@[package:]drawable/drawable_resource"
android:id="@[+][package:]id/resource_name"
android:top="dimension"
android:right="dimension"
android:bottom="dimension"
android:left="dimension"/>
</layer-list>

       一个 layer-list 中可以包含多个 item,每个 item 表示一个 Drawable。Item 的结构也比较简单,比较常用的属性有 android:top、android:bottom、android:left 和 android:right,它们分别表示 Drawable 相对于 View 的上下左右的偏移量,单位为像素。另外,我们可以通过 android:drawable 属性来直接引用一个已有的 Drawable 资源,也可以在 item 中自定义 Drawable。默认情况下,layer-list 中的所有的 Drawable 都会被缩放至 View 的大小,对于 bitmap 来说,需要使用 android:gravity 属性才能控制图片的显示效果。layer-list 有层次的概念,通过合理的分层,可以实现一些特殊的叠加效果。

       简单文本输入框效果示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/colorAccent" />
</shape>
</item>

<item android:bottom="6dp">
<shape android:shape="rectangle">
<solid android:color="#ffffff"/>
</shape>
</item>

<item android:bottom="2dp"
android:left="2dp"
android:right="2dp">
<shape android:shape="rectangle">
<solid android:color="#ffffff" />
</shape>
</item>
</layer-list>

       下面是一个 layer-list 具体使用的例子,它实现了文本输入框的效果,如图所示,当然它只适用于白色背景上的文本输入框,另外这种效果也可以采用.9图来实现。

       edit_bg_drawable 代码应用到 EditText 上如下图所示:

LayerDrawable叠加效果1

       下面是利用<layer-list>标签来实现一个带阴影的圆角矩形,shadow_round_rectangle_bg_drawable 代码应用到 View 上如下图所示:

LayerDrawable叠加效果2

       代码中实现仅给出示例,不深究,还是建议采用 xml 的方式定义。

1
2
3
4
5
6
7
8
9
10
11
12
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.image1);
Drawable[] drawables=new Drawable[3];
drawables[0] = new BitmapDrawable(bitmap);
drawables[1] = new BitmapDrawable(bitmap);
drawables[2] = new BitmapDrawable(bitmap);
LayerDrawable layer = new LayerDrawable(drawables);
//设置图层边界距离
layer.setLayerInset(0, 20, 20, 0, 0);
layer.setLayerInset(1, 40, 40, 0, 0);
layer.setLayerInset(2, 60, 60, 0, 0);
ImageView imageView = (ImageView)findViewById(R.id.imgView);
imageView.setImageDrawable(layer);

StateListDrawable

       StateListDrawable 对应于<selector>标签,它也是表示 Drawable 集合,每个 Drawable 都对应着 View 的一种状态,这样系统就会根据 View 的状态来选择合适的 Drawable。StateListDrawable 主要用于设置可单击的 View 的背景,最常见的是 Button,它的语法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize=["true" | "false"]
android:dither=["true" | "false"]
android:variablePadding=["true" | "false"]>
<item
android:drawable="@[package:]drawable/drawable_resource"
android:state_pressed=["true" | "false"]
android:state_focused=["true" | "false"]
android:state_hovered=["true" | "false"]
android:state_selected=["true" | "false"]
android:state_checkable=["true" | "false"]
android:state_checked=["true" | "false"]
android:state_enabled=["true" | "false"]
android:state_activated=["true" | "false"]
android:state_window_focused=["true" | "false"]/>
</selector>

       针对上面的语法,下面做简单介绍。

android:constantSize

       StateListDrawable 的固有大小是否不随着其状态的改变而改变,因为状态的改变会导致 StateListDrawable 切换到具体的 Drawable,而不同的 Drawable 具有不同的固有大小。 True 表示 StateListDrawable 的固有大小保持不变,这时它的固有大小是内部所有 Drawable 的固有大小的最大值,false 则会随着状态的改变而改变。此选项默认值为 false。

android:dither

       是否开启抖动效果,这个在 BitmapDrawable 中也有提到,开启此选项可以让图片在低质量的屏幕上仍然获得较好的显示效果。此选项默认值为 true。

android:variablePadding

       StateListDrawable 的 padding 表示是否随着其状态的改变而改变,true 表示会随着状态的改变而改变,false 表示 StateListDrawable 的 padding 是内部所有 Drawable 的 padding 的最大值。此选项默认值为 false,并且不建议开启此选项。

<item>

       item 标签表示一个具体的 Drawable,它的结构也比较简单,其中 android:drawable 是一个已有 Drawable 的资源 id,剩下的属性表示的是 View 的各种状态,每个 item 表示的都是一种状态下的 Drawable 信息。item 的属性介绍如下表:

属性 含义
android:drawable 该状态下要显示的图像,可以是Drawable也可以是图片
android:state_pressed 表示按下状态,比如 Button 被按下后仍没有松开时的状态
android:state_focused 表示 View 已经获取了焦点
android:state_hovered 表示光标是否停留在 View 的自身大小范围内的状态
android:state_selected 表示用户选择了 View
android:state_checkable 表示是否处于可勾选状态
android:state_checked 表示是否处于已勾选状态,一般适用于 CheckBox 这类在选中和非选中状态之间进行切换的 View
android:state_enabled 表示 View 当前处于可用状态
android:state_active 表示是否处于激活状态
android:state_window_focused 表示是否窗口已得到焦点状态

       简单示例代码如下所示,系统会根据 View 当前的状态从 selector 中选择对应的 item,每个 item 对应着一个具体的 Drawable,系统按照从上到下的顺序查找,直至查找到第一条匹配的 item。一般来说,默认的 item 都应该放在 selector 的最后一条并且不附带任何的状态,这样当上面的 item 都无法匹配 View 的当前状态时,系统就会选择默认的 item,因为默认的 item 不附带状态,所以它可以匹配 View 的任何状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--pressed-->
<item
android:state_pressed="true"
android:drawable="@drawable/button_pressed"
/>
<!--focused-->
<item
android:state_focused="true"
android:drawable="@drawable/button_focused"
/>
<!--default-->
<item
android:drawable="@drawable/button_normal"
/>
</selector>

       下面是一个实际的例子,按钮点击前后状态 XML 见 button_select_bg_drawablebutton_select_bg_drawable2

       类似如下效果:

StateListDrawable 设置按钮状态1 StateListDrawable 设置按钮状态2

       最后给出一个通过代码实现的案例(建议尽量使用xml定义,代码定义比较复杂):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 设置Selector */
public static StateListDrawable newSelector(Context context, int idNormal, int idPressed, int idFocused, int idUnable) {
//相当于<selector>标签
StateListDrawable bg = new StateListDrawable();
Drawable normal = context.getResources().getDrawable(R.drawable.shape_drawable_for_btn_normal);
Drawable pressed = context.getResources().getDrawable(R.drawable.shape_drawable_for_btn_press);
Drawable focused =context.getResources().getDrawable(R.drawable.shape_drawable_for_btn_press);
Drawable unable = context.getResources().getDrawable(R.drawable.shape_drawable_for_btn_unable);
//设置每种状态下的Drawable显示
// View.PRESSED_ENABLED_STATE_SET
bg.addState(new int[] { android.R.attr.state_pressed, android.R.attr.state_enabled }, pressed);
// View.ENABLED_FOCUSED_STATE_SET
bg.addState(new int[] { android.R.attr.state_enabled, android.R.attr.state_focused }, focused);
// View.ENABLED_STATE_SET
bg.addState(new int[] { android.R.attr.state_enabled }, normal);
// View.FOCUSED_STATE_SET
bg.addState(new int[] { android.R.attr.state_focused }, focused);
// View.WINDOW_FOCUSED_STATE_SET
bg.addState(new int[] { android.R.attr.state_window_focused }, unable);
// View.EMPTY_STATE_SET
bg.addState(new int[] {}, normal);
return bg;
}

LevelListDrawable

       LevelListDrawable 对应于<level-list>标签,它同样表示一个 Drawable 集合,集合中的每个 Drawable 都有一个等级 (level)的概念。根据不同的等级,LevelListDrawable 会切换为对应的 Drawable,它的语法如下所示:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/drawable_resource"
android:maxLevel="integer"
android:minLevel="integer"/>
</level-list>

       上面的语法中,每个 item 表示一个 Drawable,并且有对应的等级范围,由 android:minLevel 和 android:maxLevel 来指定,在最小值和最大值之间的等级会对应此 item 中的 Drawable。下面是一个例子,当它作为 View 的背景时,可以通过 Drawable 的 setLevel 方法来设置不同的等级从而切换具体的 Drawable。如果它被用来作为 ImageView 的前景 Drawable,那么还可以通过 ImageView 的 setImageLevel 方法来切换 Drawable。最后,Drawable 的等级是有范围的,即 0~10000,最小等级是 0 ,这也是默认值,最大等级是 10000。

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/status_off"
android:maxLevel="0"/>
<item android:drawable="@drawable/status_on"
android:maxLevel="1"/>
</level-list>

       下面看一个实际案例:list_drawable 中,定义了4个 item,等级分别为0,1,2,3,它们都有与之对应的 Drawable,然后在 ShowActivity 中实现一个效果,每过2秒更好一个不同等级的图片,效果如下:

LevelListDrawable 实现图片切换效果

       实际上我们还可以设置等级范围,当等级在某个范围内时去显示对应范围内的图片,这也是可以的。

TransitionDrawable

       TransitionDrawable 对应于<transition>标签,它用于实现两个 Drawable 之间的淡入淡出效果,它的语法如下所示。

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@[package:]drawable/drawable_resource"
android:id="@[+][package:]id/resource_name"
android:top="dimension"
android:right="dimension"
android:bottom="dimension"
android:left="dimension"/>
</transition>

       上面语法中的属性前面已经都介绍过了,其中 android:top、android:bottom、android:right 和 android:left 仍然表示的是 Drawable 四周的偏移量。下面是简单的示例。
首先定义 TransitionDrawable,如下所示。

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/drawable1"/>
<item android:drawable="@drawable/drawable2"/>
</transition>

       接着将上面的 TransitionDrawable 设置为 View 的背景,如下所示。当然也可以在 ImageView 中直接作为 Drawable 来使用。

1
2
3
4
5
<TextView
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/transition_drawable"/>

       最后,通过它的 startTransition 和 reverseTransition 方法来实现淡入淡出的效果以及它的逆过程,如下所示:

1
2
3
TextView textView = (textView) findViewById(R.id.button);
TransitionDrawable drawable = (TransitionDrawable) textView.getBackground();
drawable.startTransition(1000);

       具体示例详见:gem_transition_drawableShowActivity

       效果如下:

TransitionDrawable 实现图片渐变

       最后给出代码实现的方式:

1
2
3
4
5
6
7
8
ImageView imageView= (ImageView) findViewById(R.id.tranImage);

Bitmap bitmap1= BitmapFactory.decodeResource(getResources(), R.drawable.image1);
Bitmap bitmap2= BitmapFactory.decodeResource(getResources(), R.drawable.image2);
final TransitionDrawable td = new TransitionDrawable(new Drawable[] { new BitmapDrawable(getResources(), bitmap1),
new BitmapDrawable(getResources(), bitmap2) });
imageView.setImageDrawable(td);
td.startTransition(4000);

InsetDrawable

       InsetDrawable 对应于<inset>标签,它可以将其它 Drawable 内嵌到自己当中,并可以在四周留出一定的间距。当一个 View 希望自己的背景比自己的实际区域小的时候,可以采用 InsetDrawable 来实现,同时我们知道,通过 LayoutDrawable 也可以实现这种效果。InsetDrawable 的语法如下所示:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:insetTop="dimension"
android:insetRight="dimension"
android:insetLeft="dimension"
android:insetBottom="dimension"/>

       上面的属性都比较好理解,其中 android:insetTop、android:insetRight、android:insetLeft 和 android:insetBottom 分别表示顶部、右边、左边和底部内凹的大小。在下面的例子中,inset 中的 shape 距离 View 的边界为 15dp。

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetBottom="15dp"
android:insetLeft="15dp"
android:insetRight="15dp"
android:insetTop="15dp">

<shape android:shape="rectangle">
<solid android:color=“#ff0000”/>
</shape>
</inset>

       代码详见:inset_drawable

ScaleDrawable

       ScaleDrawable 对应于<scale>标签,它可以根据自己的等级(level)将指定的 Drawable 缩放到一定比例,它的语法如下所示:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:scaleGravity=["top" | "bottom" | "left" | "right" | "center_vertical" | "center_horizontal" | "center" | "fill_vertical" | "fill_horizontal" | "fill" | "clip_vertical" | "clip_horizontal"]
android:scaleHeight="percentage"
android:scaleWidth="percentage"/>

       在上面的属性中,android:scaleGravity 的含义等同于 shape 中的 android:gravity,而 android:scaleWidth 和 android:scaleHeight 分别表示对指定 Drawable 宽和高的缩放比例,以百分比的形式表示,比如 25%。

       要明白等级对 ScaleDrawable 的影响。等级0表示 ScaleDrawable 不可见,这是默认值,要想 ScaleDrawable 可见,需要等级不能为0,这一点从源码中可以得出。来看一下 ScaleDrawable 的 draw 方法,如下所示。

1
2
3
4
5
6
7
@Override
public void draw(Canvas canvas) {
final Drawable d = getDrawable();
if (d != null && d.getLevel() != 0) {
d.draw(canvas);
}
}

       很显然,由于 ScaleDrawable 的等级和 Drawable 的等级是保持一致的,所以如果 ScaleDrawable 的等级为0 ,那么它内部的 Drawable 的等级也必然为0 ,这时 Drawable 就无法绘制出来,也就是 ScaleDrawable 不可见。下面再看一下 ScaleDrawable 的 onBoundsChange 方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protected void onBoundsChange(Rect bounds) {
final Drawable d = getDrawable();
final Rect r = mTmpRect;
final boolean min = mState.mUseIntrinsicSizeAsMin;
final int level = getLevel();

int w = bounds.width();
if (mState.mScaleWidth > 0) {
final int iw = min ? d.getIntrinsicWidth() : 0;
w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
}

int h = bounds.height();
if (mState.mScaleHeight > 0) {
final int ih = min ? d.getIntrinsicHeight() : 0;
h -= (int) ((h - ih) * (MAX_LEVEL - level) * mState.mScaleHeight / MAX_LEVEL);
}

final int layoutDirection = getLayoutDirection();
Gravity.apply(mState.mGravity, w, h, bounds, r, layoutDirection);

if (w > 0 && h > 0) {
d.setBounds(r.left, r.top, r.right, r.bottom);
}
}

       在 ScaleDrawable 的 onBoundsChange 方法中,我们可以看出 Drawable 的大小和等级以及缩放比例的关系,这里拿宽度来说,如下所示:

1
2
final int iw = min ? d.getIntrinsicWidth() : 0;
w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);

       由于iw一般都为0,所以上面的代码可以简化为:

1
w -=(int)(w*(10000-level)*mState.mScaleWidth /10000)

       由此可见,如果 ScaleDrawable 的级别为最大值 10000,那么就没有缩放的效果;如果 ScaleDrawable 的级别(level)越大,那么内部的 Drawable 看起来就越大;如果 ScaleDrawable 的 XML 中所定义的缩放比例越大,那么内部的 Drawable 看起来就越小。另外,从 ScaleDrawable 的内部实现来看, ScaleDrawable 的作用更偏向于缩小一个特定的 Drawable。在下面的例子中,可以近似地将一张图片缩小为原大小的30%,代码如下所示:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/image1"
android:scaleGravity="center"
android:scaleHeight="70%"
android:scaleWidth="70%"/>

       直接使用上面的 drawable 资源是不行的,还必须设置 ScaleDrawable 的等级为大于 0 且小于等于 10000 的值,如下所示:

1
2
3
View testScale = findViewById(R.id.test_scale);
ScaleDrawable testScaleDrawable = (ScaleDrawable) testScale.getBackground();
testScaleDrawable.setLevel(1);

       经过上面的两步可以正确地缩放一个 Drawable,如果少了设置等级这一步,由于 Drawable 的默认等级为 0 ,那么 ScaleDrawable 将无法显示出来。我们可以武断地将 Drawable 的等级设置为大于10000的值,比如20000,虽然也能正常工作,但是不推荐这么做,这是因为系统内部约定 Drawable 等级的范围为0到10000。

       下面是设置两组宽高分别缩放70%和30%,效果对比如下:

ScaleDrawable 缩放对比

       再来对比一下宽高同等缩放比例(50%)下,level 对 Drawable 的影响效果,分别设置 level 等级为1,5000,10000:

ScaleDrawable 等级对比

ClipDrawable

       ClipDrawable 对应于<clip>标签,它可以根据自己当前的等级(level)来裁剪另一个 Drawable,裁剪方向可以通过 android:clipOrientation 和 android:gravity 这两个属性来共同控制,它的语法如下所示:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<clip
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:clipOrientation=["horizontal" | "vertical"]
android:gravity=["top" | "bottom" | "left" | "right" | "center_vertical" | "center_horizontal" | "center" | "fill_vertical" | "fill_horizontal" | "fill" | "clip_vertical" | "clip_horizontal"]/>

       其中 clipOrientation 表示裁剪方向,有水平和竖直两个方向,gravity 比较复杂,需要和 clipOrientation 一起才能发挥作用。如下表所示。另外 gravity 的各种选项是可以通过 “|”来组合使用的。

选项 含义
top 将内部的Drawable 放在容器的顶部,不改变它的大小,如果为竖直裁剪,那么从底部开始裁剪。
bottom 将内部的Drawable 放在容器的底部,不改变它的大小,如果为竖直裁剪,那么从顶部开始裁剪。
left 将内部的Drawable 放在容器的左边,不改变它的大小,如果为水平裁剪,那么从右边开始裁剪,这是默认值。
right 将内部的Drawable 放在容器的右边,不改变它的大小,如果为水平裁剪,那么从左边开始裁剪。
center_vertical 将内部的Drawable 在容器中竖直居中,不改变它的大小,如果为竖直裁剪,那么从上下同时开始裁剪。
center_horizontal 将内部的Drawable 在容器中水平居中,不改变它的大小,如果为水平裁剪,那么从左右两边同时开始裁剪。
fill_vertical 使内部的 Drawable 在竖直方向上填充容器,如果为竖直裁剪,那么仅当 ClipDrawable 的等级为 0 (0表示 ClipDrawable 被完全裁剪,即不可见)时,才能有裁剪行为。
fill_horizontal 使内部的 Drawable 在水平方向上填充容器,如果为水平裁剪,那么仅当 ClipDrawable 的等级为 0 时,才能有裁剪行为。
center 使内部 Drawable 在容器中水平和竖直方向都居中,不改变它的大小,如果为竖直裁剪,那么从上下同时开始裁剪;如果为水平裁剪,那么从左右同时开始裁剪。
fill 使内部的 Drawable 在水平和竖直方向上同时填充容器。仅当 ClipDrawable 的等级为 0 时,才能有裁剪行为
clip_vertical 附加选项,表示竖直方向的裁剪,较少使用。
clip_horizontal 附加选项,表示水平方向的裁剪,较少使用。

       下面举个例子,实现将一张图片从上往下进行裁剪的效果,首先定义 ClipDrawable,XML 如下:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:clipOrientation="vertical"
android:drawable="@drawable/image"
android:gravity="bottom" />

       在上面的 XML 中,因为我们要实现顶部的裁剪效果,所以裁剪方向应该为竖直方向,同时从上面的表中可以知道,gravity 属性应该选择 bottom。有了 ClipDrawable,首先将它设置给ImageVIew,当然也可以作为普通 View 的背景,如下所示:

1
2
3
4
5
6
<ImageView        
android:id="@+id/test_clip"
android:layout_width="100dp"
android:layout_height="100dp"
android:gravity="center"
android:src="@drawable/clip_drawable"/>

       接着在代码中设置 ClipDrawable 的等级,如下所示:

1
2
3
ImageView testClip = (ImageView) findViewById(R.id.test_clip);
ClipDrawable clipDrawable = (ClipDrawable) testClip.getDrawable();
clipDrawable.setLevel(8000);

       在上面已经提到,Drawable 的等级(level)是有范围的,即 0-10000,最小等级是 0,最大等级是 10000,对于 ClipDrawable 来说,等级 0 表示完全裁剪,即整个 Drawable 都不可见了,而等级 10000 表示不裁剪。在上面的代码中的等级设置为 8000 表示裁剪了2000,即在顶部裁剪掉 20% 的区域,被裁剪的区域就相当于不存在了。

       对于本例来说,等级越大,表示裁剪的区域越小,因此等级 10000 表示不裁剪,这个时候整个图片都可以完全显示出来;而等级 0 则表示裁剪全部区域,这个时候整个图片将不可见。另外裁剪效果还受裁剪方向和 gravity 属性的影响。

       代码详见:clip_drawable

       最后实现一个案例,在实际开发中经常会使用到动画来实现一些特殊效果,比如我们可能需要实现一个徐徐展开的图片,这时我们的 ClipDrawable 就派上用场了。

       因为 ClipDrawable 的 setLevel(int level) 可以控制截取图片的部分,因此我们可以设置一个定时器,让程序每隔一段时间就调用 ClipDrawable 的 setLevel(int level) 方法,即可实现图片徐徐展开效果。

       我们首先来实现一个 clip_drawable_image,并设置 clipOrientation=”horizontal”,即水平剪切,以及设置 gravity=”center”,即左右同时裁剪。接着在文件布局中引用,java 代码详见 ShowActivity

       效果如下:

通过ClipDrawable裁剪实现图片徐徐展开

ColorDrawable

       ColorDrawable 是最简单的 Drawable,它实际上是代表了单色可绘制区域,它包装了一种固定的颜色,当 ColorDrawable 被绘制到画布的时候会使用颜色填充 Paint,在画布上绘制一块单色的区域。在 xml 文件中对应<color>标签,它只有一个 android:color 属性,通过它来决定 ColorDrawable 的颜色。xml 实现如下:

1
2
3
<?xmlversion="1.0" encoding="utf-8"?>
<color xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/normal"/>

       也可以使用代码实现,注意传入的颜色值为16进制的数字:

1
2
3
ColorDrawable cd = new ColorDrawable(0xff000000);
ImageView iv = (ImageView)findViewById(...);
iv.setImageDrawable(cd);

GradientDrawable

       GradientDrawable 表示一个渐变区域,可以实现线性渐变、发散渐变和平铺渐变效果,实际上这个在 shapeDrawable 中已经分析过了,其对应的标签为<gtadient>,一般都是配置 shapeDrawable 来使用,为其实现渐变颜色。这里给出简单案例如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient android:angle="90"
android:startColor="@color/colorPrimary"
android:centerColor="#fff"
android:endColor="@color/color_state"
android:type="linear"/>
</shape>

       当然 GradientDrawable 也可以作为 View 的背景图,案例代码实现如下:

1
2
3
//分别为开始颜色,中间夜色,结束颜色
int colors[] = { 0xff255779 , 0xff3e7492, 0xffa6c0cd };
GradientDrawable gd = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, colors);

       最后设置给View的背景图即可。

1
setBackgroundDrawable(gd);

       代码实现 GradientDrawable 还可以设置边框,圆角,边框宽度等等,这里就不深究了。

       相关代码请见 CustomDrawableTest

参考资料:
《Android 开发艺术探索》任玉刚 第6章 6.3 自定义Drawable
zejian_ 领略千变万化的Android Drawable (一)领略千变万化的Android Drawable (二)

Fork me on GitHub