Android 中的各种 Drawable 你都知道了吗?

Drawable

Drawable 翻译成中文就是可绘制的意思,是“可绘制东西”的抽象(实际绘制通过 Canvas 进行)。Drawable 是一个抽象类,Andriod 也实现了很多具体的类型,下面一一介绍一下。

在这里插入图片描述

BitmapDrawable

BitmapDrawable 表示一张图片,可以从文件路径、输入流、通过 XML 填充或从 Bitmap 对象中创建 BitmapDrawable。

在实际的开发中,可以直接在 XML 中引用图片,或者通过 <bitmap>标签在 XML 文件中定义它,通过 XML 定义比直接在 XML 中引用图片资源可以有更多的设置选项,例如:


<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:antialias="true"
    android:dither="true"
    android:filter="true"
    android:src="@drawable/icon" />

XML 中可定义的属性有:

  • antialias:是否启用抗锯齿。旋转时可以使用抗锯齿来平滑位图的边缘。默认值为 false。

  • dither:是否开启抖动效果。如果图片的像素与屏幕像素不同,开启这个选项可以让高质量的图片在低质量的屏幕上保持较好的显示效果。默认值为 true。

  • filter:是否开启过滤效果。当图片尺寸被拉伸或压缩时,开启该效果会有更好的显示效果。默认值为 true。

  • gravity:当图片的小于容器的大小时(例如给布局设置背景),设置该选项可以让图片相对于容器进行定位。默认值是 fill。

    • top:将图片放在容器的顶部,不改变图片的大小。

    • bottom:将图片放在容器的底部,不改变图片的大小。

    • left:将图片放在容器的左侧,不改变图片的大小。

    • right:将图片放在容器的右侧,不改变图片的大小。

    • start:将图片放在容器的开头,不改变图片的大小。

    • end:将图片放在容器的末端,不改变图片的大小。

    • center:将图片放在容器的中心,不改变图片的大小。

    • center_horizontal:将图片放在容器的水平中心,不改变图片的大小。

    • center_vertical:将图片放在容器的垂直中心,不改变图片的大小。

    • fill:将图片的水平和垂直方向进行填充,直到填满容器。

    • fill_horizontal:将图片的水平方向进行填充,直到填满容器。

    • fill_vertical:将图片的垂直方向进行填充,直到填满容器。

    • clip_horizontal:可设置为附加的选项,基于水平方向的 gravity 的设置将超出的部分进行裁剪。

    • clip_vertical:可设置为附加的选项,基于垂直方向的 gravity 的设置将超出的部分进行裁剪。

  • mipMap:为负责渲染此图片的渲染器设置一个提示,表示在按比例缩小图片时,渲染器应该尝试使用 mipmap,如果我们事先知道会以比原图更小的比例绘制图片时,可以设置该属性以获得更高的图片质量,但会使用额外的内存。这仅仅是对渲染器的一个建议,不能保证效果。与 Bitmap.setHasMipMap(boolean) 方法是一样的效果。

  • src:设置图片的资源 ID,必须设置的。

  • tileMode:设置平铺的模式。默认为 disabled。

    • clamp:复制边缘的颜色进行平铺。
      在这里插入图片描述
    • mirror:以镜像效果平铺。
      在这里插入图片描述
    • repeat:对原图进行重复平铺。
      在这里插入图片描述
    • disabled:不平铺。
  • tileModeX:与 tileMode 效果类似,只不过只对水平方向进行平铺。

  • tileModeY:与 tileMode 效果类似,只不过只对垂直方向进行平铺。

NinePatchDrawable

表示一张 .9 图,可调整大小,拉伸定义的区域。更多关于 .9 图的信息可以看官方文档。XML 中定义示例:


<?xml version="1.0" encoding="utf-8"?>
<nine-patch xmlns:android="http://schemas.android.com/apk/res/android"
    android:antialias="true"
    android:dither="true"
    android:filter="true"
    android:src="@drawable/bg_chat" />

支持的属性同 BitmapDrawable 一样。

ShapeDrawable

绘制基本形状的 Drawable 对象。可以使用 <shape>标签在 XML 文件中定义此对象,例如:


<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="@color/colorAccent" />

</shape>

需要注意的是,虽然我们将通过 shape 标签定义的 Drawable 对象称为 ShapeDrawable,但是实际上构造出来的是 GradientDrawable 对象。

XML 中可定义的属性有:

  • shape:定义图形的形状。默认值是 rectangle。

    • rectangle:矩形。

    • oval:椭圆。

    • line:横线。

    • ring:圆环。针对圆环形状,还有配套的几个属性。

      • innerRadius:圆环的半径。

      • thickness:圆环的厚度。

      • innerRadiusRatio:圆环的半径占整个 Drawable 宽度的比例。

      • thicknessRatio:圆环的厚度占整个 Drawable 宽度的比例。

      • useLevel:是否使用 level 属性,一般都应该为 false,除非它被当做 LevelListDrawable 来使用。

  • corners:设置 shape 四个角的角度,只适用于 rectangle。

    • radius:为四个角同时设置相同的角度。

    • topLeftRadius:设置左上角的角度。

    • topRightRadius:设置右上角的角度。

    • bottomLeftRadius:设置左下角的角度。

    • bottomRightRadius:设置右下角的角度。

  • solid:使用颜色填充形状,通过 color 可以指定填充的颜色。

  • gradient:使用渐变颜色填充形状,与 solid 互斥。

    • type:渐变的类型。可以设置为 line(线性渐变,默认值)、radial(辐射渐变)、sweep(扫描渐变)三种。

    • angle:渐变的角度,默认为 0,值必须是 45 的倍数,仅当 type 为 line 时才有效。

    • gradientRadius:渐变半径,仅当 type 为 radial 时才有效。

    • centerX:渐变的中心点横座标。

    • centerY:渐变的中心点纵座标。

    • startColor:渐变的起始颜色。

    • centerColor:渐变的中间色。

    • endColor:渐变的结束色。

  • stroke:设置形状的描边。

    • width:描边的宽度。

    • color:描边的颜色。

    • dashWidth:虚线线段的宽度。

    • dashGap:虚线线段间的间隔。

  • padding:设置四周的边距,具体就不说了,大家都很熟悉。

  • size:设置 shape 的固有大小。并不代表最终显示的大小就是该大小,在作为 View 背景的情况下,Drawable 的大小会拉伸或缩小到和 View 大小一致。

LayerDrawable

LayerDrawable 表示一种层次化的 Drawable,可以使用 <layer-list>标签在 XML 文件中定义此对象,例如:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <stroke
                android:width="2dp"
                android:color="@color/colorAccent" />
        </shape>
    </item>
    <item
        android:bottom="2dp"
        android:left="2dp"
        android:right="2dp"
        android:top="2dp">
        <bitmap android:src="@drawable/icon" />
    </item>
</layer-list>

在上面的示例中,就通过叠加的特性加上偏移,为图片的四周增加了一圈边框。

StateListDrawable

StateListDrawable 表示一组 Drawable 集合,根据 View 不同的状态切换到不同的 Drawable,非常常用,例如做 Button 的按下效果等。可以在 XML 文件中使用 <selector>标签定义:


<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="@color/colorPrimaryDark" />
        </shape>
    </item>

    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/colorPrimary" />
        </shape>
    </item>
</selector>

View 会根据状态在 selector 中从上到下查找符合的状态,并切换到该状态对应的 Drawable,因此,默认的 Drawable 应该放在最下面,当找不到对应状态时进行兜底。

View 的各种状态就不细说了,因为大家都比较熟悉。来看看 StateListDrawable 支持的属性:

  • constantSize:设置 Drawable 的大小是否不随着状态的改变而改变,当切换状态时,也会切换到对应的 Drawable,而 Drawable 的大小可能不同,如果设置为 true 的话,那么就会固定大小为所有 Drawable 的最大大小。默认为 false。

  • variablePadding:设置 Drawable 的 padding 是否随着状态的改变而改变,如果设置为 true,则会随着改变。默认为 false, 固定 padding 为所有 Drawable 的最大 padding。

LevelListDrawable

LevelListDrawable 也是将一组 Drawable 集合在一起,并且每个 Drawable 都可以设置 level 的范围,然后当我们在代码中设置 level 时,就会切换到对应 level 的 Drawable。举个实际的场景就是电池电量,可以将电量对应 level 并且设置不同的 Drawable:


<?xml version="1.0" encoding="utf-8"?>
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/electric_quantity_0"
        android:maxLevel="10"
        android:minLevel="0" />
    <item
        android:drawable="@drawable/electric_quantity_70"
        android:maxLevel="70"
        android:minLevel="11" />
    <item
        android:drawable="@drawable/electric_quantity_100"
        android:maxLevel="100"
        android:minLevel="71" />
</level-list>

代码中修改 level:


val drawable = electricQuantity.background as LevelListDrawable
drawable.level = level

需要注意的是,level 这个属性是在抽象类 Drawable 中,所以在其他的 Drawable 中也有可能利用该属性做一些效果。另外,默认情况下, minLevel 为 0,maxLevel 为 10000。

TransitionDrawable

TransitionDrawable 是 LayerDrawable 的扩展类,在第一层和第二层 Drawable 的切换之间加了一层过渡的动画效果。可以在 XML 文件中使用 <transition>标签定义:


<?xml version="1.0" encoding="utf-8"?>
<transition xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/electric_quantity_0" />
    <item android:drawable="@drawable/electric_quantity_100" />
</transition>

通过下面的代码进行转换:


val drawable = imageView.drawable as TransitionDrawable
drawable.startTransition(100) // 开始转换,从第一层转换到第二层

还有其他的方法:


drawable.reverseTransition(100) // 翻转转换
drawable.resetTransition() // 仅显示第一层

InsetDrawable

InsetDrawable 可以以指定的间距将另外的 Drawable 内嵌进来,当我们希望 View 的背景比自己实际的区域小的时候就可以用到它。可以在 XML 文件中使用 <inset>标签定义:


<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:inset="10dp">

    <shape android:shape="rectangle">
        <solid android:color="@color/colorAccent" />
    </shape>
</inset>

除了 inset 属性直接设置四周的间距大小之外,还有单独设置不同方向间距的属性,就不赘述了。

ScaleDrawable

ScaleDrawable 可以将内部定义的 Drawable 进行缩放,根据自身的 scaleWidth 和 scaleHeight 属性定义的缩放比例还有 level 的级别进行缩放,level 必须设置为大于 0,否则不会显示。可以在 XML 文件中使用 <scale>标签定义:


<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/icon"
    android:scaleWidth="50%"
    android:scaleHeight="50%"
    android:scaleGravity="center" />

在上面的示例中,将内部的 Drawable 缩小了 50%,并且居中。

别忘了设置 level,level 越大,内部的 Drawable 看起来就越大:

(scaleView.background as ScaleDrawable).level = 1

ClipDrawable

ClipDrawable 可以根据 level 对内部的 Drawable 进行裁剪,可以用来实现进度条等效果。可以在 XML 文件中使用 <clip>标签定义:


<?xml version="1.0" encoding="utf-8"?>
<clip xmlns:android="http://schemas.android.com/apk/res/android"
    android:clipOrientation="horizontal"
    android:drawable="@drawable/icon"
    android:gravity="left" />

上面的示例代码中定义了一个从左到右展开的 Drawable。别忘了在代码中设置 level,最小为 0,表示不显示,最大为 10000,完全显示。

  • clipOrientation:设置裁剪的方向,可设置水平 horizontal 或垂直 vertical 方向。

  • gravity:该属性根据 clipOrientation 的不同而不同,例如上面的例子中,设置为水平方向为 left,就是从左到右进行展开,如果设置为 right 就是从右到左进行展开。

VectorDrawable

VectorDrawable 是一种矢量图形,和 BitmapDrawable 一样也是表示一张图片,但是原理却大不相同。

  • 普通的 Bitmap 位图:由一个个像素组成,每个像素点都是一个很小的正方形,被分配了一种颜色。优点是表达能力强,细节可以很精细,缺点是缩放会失真。像 PNG、JPG、GIF 格式的图片都是位图图像。

  • Vector 矢量图:由直线、曲线和形状组等信息组成。优点是可以进行无损缩放,并且占用大小一般情况下都比位图图像小,缺点是不适合生成逼真的图像,因为逼真的图像会有很多很多的细节,如果硬要生成,耗时的时间是一个问题,矢量图的大小和复杂度也是一个问题。SVG 格式的图片就是矢量图像。

在 Android 中使用 VectorDrawable 意味着我们不用再为不同的设备准备不同分辨率的图片,这可以减少 APK 包的大小,节省开发时间。并且使用 AnimatedVectorDrawable 还可以利用 VectorDrawable 做一些动画效果,这会在 AnimatedVectorDrawable 当中介绍到。

另外为了优化重绘性能,将为每个 VectorDrawable 创建一个位图缓存。因此,引用相同的 VectorDrawable 意味着共享相同的位图缓存。如果这些引用在相同的大小上不一致,则每次更改大小时都会重新创建并重新绘制位图。换句话说,如果将 VectorDrawable 用于不同的大小,则创建多个 VectorDrawable(每个大小对应一个)的效率更高。

要在 Android 中使用 VectorDrawable 其实很简单,因为 AndroidStudio 已经给我们提供了创建的工具,直接右键 New -> Vector Asset 即可:

在这里插入图片描述

可以选择直接从素材库中创建对应的 VectorDrawable 或者从本地的 SVG、PSD 文件中导入,注意导入时如果有文件中有不支持的属性会报错,需要修改后再次导入。另外,还可以在创建 VectorDrawable 之前修改大小、颜色和透明度。点击下一步选择好放置的目录,完成后创建的 VectorDrawable 文件如下:

<vector android:height="24dp" android:tint="#33B5E5"
    android:viewportHeight="24.0" android:viewportWidth="24.0"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

VectorDrawable 与之前介绍的 Drawable 有一点不同的是,创建时通过工具创建而不是手动创建,因为 VectorDrawable 和其他 Drawable 相比都比较复杂,尤其是核心 pathData 属性指定的数据,感兴趣的可以自己研究下其中的意思。

使用时可以直接引用:

<androidx.appcompat.widget.AppCompatImageView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:srcCompat="@drawable/ic_arrow_back_black_24dp" />

虽然 Android 的 android.graphics.drawable 包下面已经有了 VectorDrawable,但是我还是强烈推荐使用 AndroidX 的 VectorDrawableCompat 实现,因为更加灵活,更新也会更频繁,能得到最新的 bug 修复和向后兼容性,添加依赖:

implementation "androidx.vectordrawable:vectordrawable:1.1.0"

在布局中应该使用带 compat 的属性,例如图片应该用 srcCompat 属性而不是 src。Button 应该用 buttonCompat 而不是 Button。如果没有对应的 compat 属性可以设置,那么可以通过代码进行设置:

AppCompatResources.getDrawable(context, R.drawable.ic_arrow_back_black_24dp)

Android 会在 API 21 到 API 23 之间使用 AndriodX 中的 VectorDrawableCompat 实现,从 API 24 开始,由于 Android 将 VectorDrawable 进行了重新实现,因此直接用的是 VectorDrawable。API 21 以下的兼容应该不用再说了吧,现在不会还有应用兼容 5.0 以下吧?

AnimatedVectorDrawable

AnimatedVectorDrawable 相当于一个“粘合剂”,将 VectorDrawable 和动画结合在了一起,使 VectorDrawable 中的属性可以动态变化,从而实现一些动画效果。和 VectorDrawable 一样,推荐引入 AndroidX 中的版本:

implementation "androidx.vectordrawable:vectordrawable-animated:1.1.0"

AnimatedVectorDrawable 使用也比较简单,首先定义一个要做动画的目标 VectorDrawable:


<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:height="64dp"
    android:width="64dp"
    android:viewportHeight="600"
    android:viewportWidth="600" >
    <group
        android:name="rotationGroup"
        android:pivotX="300.0"
        android:pivotY="300.0"
        android:rotation="45.0" >
        <path
            android:name="v"
            android:fillColor="#000000"
            android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
    </group>
</vector>

注意要给要做动画的部分加上 name,以便让动画文件知道要对哪部分做动画。

创建动画文件 rotation.xml 和 path_morph.xml:


<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="6000"
    android:propertyName="rotation"
    android:valueFrom="0"
    android:valueTo="360" />

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <objectAnimator
        android:duration="3000"
        android:propertyName="pathData"
        android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
        android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
        android:valueType="pathType"/>
</set>

上面的文件中分别让 VectorDrawable 中的 group 进行旋转,以及对 group 中的路径做动画,路径动画可以让图形平滑过渡到另一种图形,是 Vector 动画的一大亮点。

最后创建主角 AnimatedVectorDrawable 将 VectorDrawable 和动画组合在一起:


<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/anim_vector_drawable" >
    <target
        android:name="rotationGroup"
        android:animation="@animator/rotation" />
    <target
        android:name="v"
        android:animation="@animator/path_morph" />
</animated-vector>

除此之外,还可以通过 AAPT 将多个 XML 文件结合在一起,例如:


<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <target android:name="rotationGroup">
        <aapt:attr name="android:animation">
            <objectAnimator
                android:duration="6000"
                android:propertyName="rotation"
                android:valueFrom="0"
                android:valueTo="360" />
        </aapt:attr>
    </target>

    <target android:name="v">
        <aapt:attr name="android:animation">
            <set>
                <objectAnimator
                    android:duration="3000"
                    android:propertyName="pathData"
                    android:valueFrom="M300,70 l 0,-70 70,70 0,0 -70,70z"
                    android:valueTo="M300,70 l 0,-70 70,0  0,140 -70,0 z"
                    android:valueType="pathType" />
            </set>
        </aapt:attr>
    </target>

    <aapt:attr name="android:drawable">
        <vector
            android:width="64dp"
            android:height="64dp"
            android:viewportWidth="600"
            android:viewportHeight="600">
            <group
                android:name="rotationGroup"
                android:pivotX="300.0"
                android:pivotY="300.0"
                android:rotation="45.0">
                <path
                    android:name="v"
                    android:fillColor="#000000"
                    android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
            </group>
        </vector>
    </aapt:attr>
</animated-vector>

单个 XML 实际效果和上面的一致。

在代码中控制动画的播放和停止:

val drawable = imageView.drawable as AnimatedVectorDrawable
drawable.start()
drawable.stop()

API 23 开始又增加了一个新的 reset() 方法,可以将 AnimatedVectorDrawable 恢复到初始状态。

总的来说,通过 AnimatedVectorDrawable 进行动画,可以只对图像的一部分进行动画,非常灵活,并且可以对路径做动画,从而实现一些很酷的效果。另外,从 API 25 开始,AnimatedVectorDrawable 在 RenderThread 上运行,这意味着即使 UI 线程上的工作量很大,AnimatedVectorDrawable 中的动画也可以保持平滑。

自定义 Drawable

对于一些其他的效果,我们可以直接继承自 Drawable,然后实现自己想要的效果,例如下面的示例中实现了一个竖线的 Drawable:

class CustomDrawable : Drawable() {
  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

  init {
    paint.color = Color.RED
  }

  override fun draw(canvas: Canvas) {
    val r = bounds
    val width = r.width()
    val height = r.height()
    paint.strokeWidth = width.toFloat()
    canvas.drawLine(0f, 0f, 0f, height.toFloat(), paint)
  }

  override fun setAlpha(alpha: Int) {
    paint.alpha = alpha
    invalidateSelf()
  }

  override fun getOpacity(): Int {
    return PixelFormat.TRANSLUCENT
  }

  override fun setColorFilter(colorFilter: ColorFilter?) {
    paint.colorFilter = colorFilter
    invalidateSelf()
  }
}

在设置时,直接通过代码设置即可。

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章