Android 开发艺术探索读书笔记 6 -- Android 的 Drawable

本篇文章主要介绍以下几个知识点:

  • Drawable 简介
  • Drawable 的分类
  • 自定义 Drawable
hello,夏天 (图片来源于网络)

Drawable,使用简单,比自定义 View 成本低,非图片类型的 Drawable 占用空间小。

6.1 Drawable 简介

Drawable,一种图像的概念(不一定是图片,如通过颜色也可构造出图像)。

在 Android 设计中,Drawable 是一个抽象类,是所有 Drawable 对象的基类,其层次关系如下:

Drawable 的层次关系

通过 getIntrinsicWidthgetIntrinsicHeight 可获取 Drawable 的内部宽高。一张图片的宽高是其内部宽高,但一个颜色形成的 Drawable 没有内部宽高概念。(注:Drawable 的内部宽高不等同于其大小,一般是无大小概念,如作 View 的背景时会被拉伸至 View 的同等大小)

6.2 Drawable 的分类

Drawable 的种类繁多,如 BitmapDawableShapDrawableLayerDrawableStateListDrawable等。

6.2.1 BitmapDrawable

表示一张图片。实际开发中直接引用原始图片即可,也可通过 XML 的方式来描述,如下:

<?xml version="1.0" encoding="utf-8"?>
<!-- src  图片的资源id
     antialias  是否开启图片抗锯齿功能
     dither  是否开启抖动效果
     filter  是否开启过滤效果
     gravity  对图片进行定位
     mipMap  纹理映射(默认为 false)
     tileMode  平铺模式(默认 disabled),开启后 gravity 属性会被忽略-->
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@mipmap/ic_header"
    android:antialias="true"
    android:dither="true"
    android:filter="true"
    android:gravity="center"
    android:mipMap="false"
    android:tileMode="disabled"/>

其中 gravity 属性的可选项如下:

gravity 属性的可选项

其中 tileMode 属性开启后各效果如下:

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

NinePatchDrawable,.9格式的图片,可自动根据所需的宽高进行相应的缩放而不失真,和 BitmapDrawable 一样,在实际开发中直接引用图片即可,也可通过 XML 来描述如下:

<?xml version="1.0" encoding="utf-8"?>
<!-- src  图片的资源id
     dither  是否开启抖动效果 -->
<nine-patch xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/message_left"
    android:dither="true" />

实际使用中在 bitmap 标签中也可用 .9 图,即 BitmapDrawable 也可代表一个 .9 格式的图片。

6.2.2 ShapeDrawable

通过颜色来构造的图形,可以是纯色的图形,也可以是具有渐变效果的图形。其语法如下:

<?xml version="1.0" encoding="utf-8"?>

<!-- shape  图形的形状:rectangle 矩形、 oval 椭圆、
            line 横线、 ring 圆环
     注:ling 和 ring 需要<stroke>标签来指定线的宽度和颜色等信息-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
   android:shape="rectangle">

    <!-- 表示 shape 的四个角的角度,只适用于矩形 shape
         radius 为四个角设定相同的角度,优先级低
         其他分别指设定左上角、右上角、左下角、右下角的角度-->
    <corners
        android:radius="32px"
        android:topLeftRadius="32px"
        android:topRightRadius="32dp"
        android:bottomLeftRadius="32px"
        android:bottomRightRadius="32px"/>

    <!-- 表渐变效果,与 <solid> 标签是互相排斥的
         angle  渐变的角度,默认0,其值必须为45的倍数(0:从左到右 90:从上到下)
         centerX、centerY  渐变的中心点的横坐标、纵坐标
         startColor、centerColor、endColor  渐变的起始、中间、结束色
         gradientRadius  渐变的半径,仅当 android:type="radial" 时有效
         type  渐变的类别,1.linear:线性  2.radial:径向  3.sweep:扫描线
         useLevel  一般为false,当Drawable作为StateListDrawable使用时为true -->
    <gradient
        android:angle="0"
        android:centerX="32"
        android:centerY="32"
        android:startColor="@color/colorPrimary"
        android:centerColor="@color/colorPrimaryDark"
        android:endColor="@color/colorAccent"
        android:gradientRadius="32dp"
        android:type="linear"
        android:useLevel="false"/>

    <!-- 表示包含它的 View 的空白-->
    <padding
        android:left="32dp"
        android:right="32dp"
        android:top="32dp"
        android:bottom="32dp"/>

    <!-- shape 的大小
         width、height  宽、高
        (注:ShapeDrawable 的固有宽高,但作为View背景时无效)-->
    <size
        android:width="32dp"
        android:height="32dp" />

    <!-- 表示纯色填充
         color  指定 shape 中填充的颜色-->
    <solid
        android:color="@color/colorAccent" />

    <!-- 描边
         width  描边的宽度
         color  描边的颜色
         dashWidth  组成虚线的线段的宽度
         dashGap  组成虚线的线段间的间隔
        (注:dashWidth 和 dashGap 有任何一个为 0,则虚线效果无效)-->
    <stroke
        android:width="32dp"
        android:color="@color/colorAccent"
        android:dashWidth="32dp"
        android:dashGap="32dp"/>

</shape>

其中 shape 属性中的 ring 这个形状,有 5 个特殊属性如下:

ring 的属性值

其中 type 属性各类别的区别如下:

渐变的类别,从左到右依次为 linear、radial、sweep

6.2.3 LayerDrawable

其对应的 XML 标签是<layer-list>,表示一种层次化的 Drawable 集合,一种叠加的效果,其语法如下:

<?xml version="1.0" encoding="utf-8"?>

<!-- 一个 layer-list 中可以包含多个 item,每个 item 表示一个 Drawable 
     layer-list 有层次的概念,下面的 item 会覆盖 上面的 item -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- drawable  可以直接引用已有的 Drawable 资源
         top、bottom、left、right:Drawable相对于View的上下左右的偏移量,单位为像素-->
    <item
        android:drawable="@drawable/drawable_test"
        android:id="@id/btn_chapter06"
        android:top="@dimen/drawable_dimen"
        android:right="@dimen/drawable_dimen"
        android:bottom="@dimen/drawable_dimen"
        android:left="@dimen/drawable_dimen"/>
    
</layer-list>

下面是一个 layer-list 具体使用例子,它实现了微信中的文本输入框效果,代码如下:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item>
        <shape android:shape="rectangle">
            <solid android:color="#0ac39e"/>
        </shape>
    </item>

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

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

</layer-list>

运行效果如下:

layer-list 的应用

6.2.4 StateListDrawable

其对应的 XML 标签是<selector>,也是表示 Drawable 集合,每个 Drawable 对应着 View 的一种状态,其语法如下:

<?xml version="1.0" encoding="utf-8"?>

<!-- constantSize  StateListDrawable 的固有大小是否随着其状态的改变而改变
                   默认 false,即随着状态的改变而改变
     dither  是否开启抖动效果 默认 true
     layer-variablePadding  StateListDrawable 的 padding是否随着其状态的
                            改变而改变,默认 false
                            -->
<selector xmlns:android="http://schemas.android.com/apk/res/android"
    android:constantSize="true"
    android:dither="true"
    android:variablePadding="false">

    <!-- drawable  已有 Drawable 的资源id
         其他表示 View 的各种状态 -->
    <item
        android:drawable="@drawable/drawable_test"
        android:state_pressed="true"
        android:state_focused="true"
        android:state_hovered="true"
        android:state_selected="true"
        android:state_checkable="true"
        android:state_checked="true"
        android:state_enabled="true"
        android:state_activated="true"
        android:state_window_focused="true" />

</selector>

其中 <item> 标签中的 View 的常见状态如下:

View 的常见状态

StateListDrawable 主要用于设置可单击的 View 的背景,如 Button,常见的例子如下:

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

    <item
        android:drawable="@drawable/drawable_pressed"
        android:state_pressed="true" />

    <item
        android:drawable="@drawable/drawable_pressed"
        android:state_focused="true" />

    <!-- default -->
    <item android:drawable="@drawable/drawable_normal" />

</selector>

6.2.5 LevelListDrawable

其对应的 XML 标签是<level-list>,也是表示 Drawable 集合,每个 Drawable 都有一个等级的概念。LevelListDrawable 根据不同的等级切换对应的 Drawable,其语法如下:

<level-list xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- maxLevel、minLevel  指定等级范围,在最小值和最大值之间
                             的等级会对应此 item 中的 Drawable-->
    <item
        android:drawable="@drawable/drawable_test"
        android:maxLevel="100"
        android:minLevel="32"/>

</level-list>

LevelListDrawable 作为 View 的背景时可通过 Drawable 的 setLevel 来设置不同的等级,作为 ImageView 的前景时可通过 ImageView 的 setImageLevel 来切换 Drawable。

Drawable 等级范围为 0 ~ 10000,例子如下:

<level-list xmlns:android="http://schemas.android.com/apk/res/android">

    <item
        android:drawable="@drawable/drawable_off"
        android:maxLevel="0" />

    <item
        android:drawable="@drawable/drawable_on"
        android:maxLevel="1" />

</level-list>

6.2.6 TransitionDrawable

其对应的 XML 标签是<transition>,用于实现两个 Drawable 之间的淡入淡出效果,其语法如下:

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

    <!-- top、bottom、left、right 指 Drawable 四周的偏移量-->
    <item
        android:drawable="@drawable/drawable_test"
        android:top="@dimen/drawable_dimen"
        android:bottom="@dimen/drawable_dimen"
        android:right="@dimen/drawable_dimen"
        android:left="@dimen/drawable_dimen"/>

</transition>

使用步骤如下:

1. 定义 TransitionDrawable

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

    <item android:drawable="@drawable/drawable_test"/>

    <item android:drawable="@drawable/drawable_test_02" />

</transition>

2. 设置为 View 的背景

<!-- 注:在 ImageView 中也可直接作为 Drawable 来用 -->
<TextView
    android:id="@+id/tv_transition"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@drawable/chapter_06_transition_drawable"/>

3. 通过startTransitionreverseTransition 来开启效果及其逆过程

TextView tvTransition = (TextView) findViewById(R.id.tv_transition);
TransitionDrawable drawable = (TransitionDrawable) tvTransition.getBackground();
drawable.startTransition(2000);

运行效果如下:

transition 效果

6.2.7 InsetDrawable

其对应的 XML 标签是<inset>,可以将其他 Drawable 内嵌到自己当中,并可在四周留出一定的间距,其语法如下:

<!-- insetTop、insetBottom、insetRight、insetLeft  
     表示 顶部、底部、右边、左边 内凹的大小 -->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:insetTop="@dimen/drawable_dimen"
    android:insetBottom="@dimen/drawable_dimen"
    android:insetRight="@dimen/drawable_dimen"
    android:insetLeft="@dimen/drawable_dimen"/>

当一个 View 的背景比自己的实际区域小时可采用 InsetDrawable 实现(也可采用 LayerDrawable 来实现),如下:

<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:insetTop="15dp"
    android:insetBottom="15dp"
    android:insetRight="15dp"
    android:insetLeft="15dp">
    <shape android:shape="rectangle">
        <solid android:color="#ff0000"/>
    </shape>
</inset>

运行效果:

InsetDrawable 效果

6.2.8 ScaleDrawable

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

<!-- scaleGravity  等同于 shape 中的 android:gravity
     scaleWidth、scaleHeight  指定对 Drawable 宽高的缩放比例,百分比表示 -->
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:scaleGravity="center"
    android:scaleHeight="32%"
    android:scaleWidth="32%" />

ScaleDrawable 的默认等级为 0,即不可见,要 ScaleDrawable 可见则等级不为 0。如将一张图片缩小为原来的30%,如下:

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:scaleGravity="center"
    android:scaleHeight="70%"
    android:scaleWidth="70%" />

然后设置 ScaleDrawable 的等级大于 0且小等于10000的值,如下:

TextView tvScale = (TextView) findViewById(R.id.tv_scale);
ScaleDrawable scaleDrawable = (ScaleDrawable) tvScale.getBackground();
scaleDrawable.setLevel(1);

6.2.9 ClipDrawable

其对应的 XML 标签是<clip>,它可以根据自己的等级(level)来剪裁另一个Drawable,其语法如下:

<!-- clipOrientation  剪裁方向:水平或竖直
     gravity  需要和 clipOrientation 一起才能发挥作用 -->
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:clipOrientation="horizontal"
    android:gravity="center" />

其中 gravity 的属性如下:

ClipDrawable 的 gravity 属性

下面实现将一张图片从上往下进行裁剪的效果:

1. 定义 ClipDrawable

<!-- 实现顶部的裁剪效果,方向设为竖直,gravity 设为 bottom -->
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/drawable_test"
    android:clipOrientation="vertical"
    android:gravity="bottom" />   

2. 设置给 ImageView (也可作为 View 的背景)

 <ImageView
     android:id="@+id/iv_clip"
     android:layout_width="100dp"
     android:layout_height="100dp"
     android:src="@drawable/chapter_06_clip_drawable"
     android:layout_gravity="center_horizontal" />

3. 在代码中设置 ClipDrawable 的等级

ImageView ivClip = (ImageView) findViewById(R.id.iv_clip);
ClipDrawable clipDrawable = (ClipDrawable) ivClip.getDrawable();
// 0-10000,其中0表示完全裁剪,10000表示不裁剪
clipDrawable.setLevel(5000);

运行效果如下:

ClipDrawable 的裁剪效果

6.3 自定义 Drawable

Drawable 使用范围单一,作为 ImageView 的图像显示或者作为 View 的背景。

Drawable 的工作原理很简单,其核心是 draw 方法,可通过重写其 draw 方法来自定义 Drawable。(注:自定义的 Drawable 无法在 XML 中使用)

下面自定义一个圆形的 Drawable,它的半径会随着 View 的变化而变化,可作为 View 的通用背景,如下:

public class CustomDrawable extends Drawable {

    private Paint mPaint;

    public CustomDrawable(int color) {
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(color);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        final Rect r = getBounds();
        float cx = r.exactCenterX();
        float cy = r.exactCenterY();
        canvas.drawCircle(cx, cy ,Math.min(cx, cy), mPaint);
    }

    @Override
    public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
        mPaint.setAlpha(alpha);
        invalidateSelf();
    }

    @Override
    public void setColorFilter(@Nullable ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
        invalidateSelf();
    }

    @Override
    public int getOpacity() {
        // not sure, so be safe
        return PixelFormat.TRANSLUCENT;
    }
}

在代码中设置自定义的 Drawale:

ImageView ivCustom = (ImageView) findViewById(R.id.iv_custom_drawable);
CustomDrawable customDrawable = new CustomDrawable(getResources().getColor(R.color.colorAccent));
ivCustom.setImageDrawable(customDrawable);

运行效果如下:

自定义圆形的 Drawable

以上便是完整的自定义 Drawable 的流程。值得注意的是,自定义的 Drawable 有固定大小时最好重写 getIntrinsicWidthgetIntrinsicHeight 这两方法。

本篇文章就介绍到这。

推荐阅读更多精彩内容