Android的Drawable详解

前言

本篇文章是《Android开发艺术探索》第6章的读后感和实践Demo,是关于Android的Drawable的相关知识。

在之前写Bitmap的文章时,有个很有意思的问题,就是在Android中,Bitmap、Drawable和自定义View有什么区别?好像在平时开发中,我们遇到简单的UI需求,比如圆角的按钮背景,我们第一反应就是使用Drawable;然后稍微复杂点的UI需求,我们就自定义View来实现;但是简单的UI需求,我们也可以使用自定义View来实现,而且BitmapDrawable还可以加载Bitmap,那这个Drawable存在的意义是什么呢

Bitmap就是用来保存图片的位图数据,是保存图片;而View是通过测量、布局和绘制的步骤来实现效果;而Drawable则是一种可以在Canvas上进行绘制的抽象概念,可以这么说,它有了View的一半的功能;既然只有View的一半功能,它存在有必要吗?

当然有必要,Drawable可以简化实现一些图像效果,最大的好处就是方便。Drawable一般被用作View的背景来使用,而实现就是在XML文件中进行定义即可,如果这些效果用自定义View的话,会麻烦很多。

Drawable的内置种类在源码中有很多,这些Drawable可以通过图片、颜色构造各式各样的图像效果,熟练掌握这些Drawable,可以避免在开发中重复造轮子。

正文

我们先介绍一个Drawable比较重要的参数,然后再挨个介绍和使用内置实现的各种Drawable。

内部宽高

一般来说Drawable是没有大小概念的,因为Drawable可以通过图片或者颜色构成,当使用颜色构成时,把Drawable作为View的背景,其大小会被拉伸到View的同等大小

但是其有内部宽/高的这个参数,可以通过getIntrinsicWidth和getIntrisicHeight这俩个方法来获取,当然也不是所有Drawable都有这个概念;比如使用一张图片构成的Drawable,其内部宽高就是图片的宽高,而颜色构造,则没有内部宽高的概念。

Drawable的分类

Drawable的种类非常多,我们现在来一一列举,分别说一下其使用细节。

BitmapDrawable

首先就是最简单的BitmapDrawable,它表示一张图片,在实际开发中我们直接引用原始图片即可,也可以使用XML的方式来描述BitmapDrawable。

或许会说,我直接使用图片不就可以了吗,为啥还要使用BitmapDrawable呢?这就和Drawable的最常见使用场景有关了,即可以当成View的背景,我们可以通过BitmapDrawable来配置一些属性,来达到不同的效果。

使用XML定义BitmapDrawable如下:

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

然后把它作为一个全屏页面的背景:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BitmapDrawableActivity"
    android:id="@+id/drawableBitmapLl"
    android:background="@drawable/drawable_bitmap"
    android:orientation="horizontal">

</LinearLayout>

得到的效果如下:

image.png

初步可以判断,这里是把原来的苹果图片进行了拉伸,来适应整个页面大小。

可以发现上面XML中一共有7个属性,我们分别来说一下:

  • android:src,就是图片的资源id。
  • android:antialias,是否开启图片抗锯齿功能,开启后会让图片变得平滑,同时会在一定程度上降低图片的清晰度,但是这个降低的幅度可以忽略不计,所以抗锯齿选项应该开启。
  • android:dither,是否开启抖动效果。当图片的像素配置和手机屏幕的像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果。比如Bitmap默认的色彩模式为ARGB_8888,当设备的色彩模式是RGB_565,这个时候开启抖动选项可以让图片不会过于失真,所以默认应该开启。
  • android:filter,是否开启过滤效果,当图片尺寸被拉伸或者压缩时,开启过滤效果可以保持较好的显示效果,默认应该开启。
  • android:gravity,当图片小于容器尺寸时,设置该选项可以队图片进行定位,这也是我们把图片设置为背景时,处理图片位置的重要API,可选项如下,不同的选项可以使用|来组合使用:
  1. top/bottom/left/right,将图片放在容器的顶部/底部/左部/右部,不改变图片的大小。
  2. center_vertical/center_horizontal,将图片竖直居中/水平居中,不改变图片的大小。
  3. fill_vertical/fill_horizontal,将图片竖直/水平方向填充容器。
  4. fill,图片在水平和竖直方向均填充,这是默认值。
  • android:mipMap,这是一种图像相关的处理技术,默认为false,这里不做深入探究了。
  • android:tileMode,平铺模式,默认是关闭,当选择是非关闭时,前面的gravity属性将失效,其他3种选项以及效果如下:
  1. repeat,重复模式,效果:

image.png

  1. mirror,镜像模式,效果:

image.png

  1. clamp,拉伸模式,它会将图片四周的像素扩散到周围区域,效果:

image.png

还有一个类似的叫做NinePatchDrawable,即.9格式的图片,其属性和使用和BitmapDrawable一样。

ShapeDrawable

这种Drawable可以说是我们平时用的最多的一种了,可以理解为通过颜色来构造的图形,它既可以是纯色的图形,也可以是具有渐变效果的图形。

同样我们也是用XML来定义,它的标签是shape标签,XML代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle"
    >
    <!--4个角的角度,适用于矩形-->
    <corners
        android:radius="2dp"/>

    <!--渐变效果,和solid标签互斥,solid表示纯色-->
    <gradient
        android:angle="0"
        android:startColor="@color/blue"
        android:endColor="@color/orange"
        android:centerColor="@color/white"
        android:type="linear"
        android:gradientRadius="200"
        />

    <!--纯色填充-->
    <solid/>

    <!--描边颜色-->
    <stroke
        android:width="1dp"
        android:color="@color/red"
        android:dashWidth="10dp"
        android:dashGap="2dp"
        />
</shape>

这里有不少非常熟悉的属性,这个效果如下:

image.png

我们还是分别介绍:

  • android:shape,表示图形的形状,分别是默认的rectangle(矩形),oval(椭圆),line(横线)和ring(圆环)。其中line和ring必须要通过<stroke>标签来指定线的宽度和颜色,否则没有显示效果。

在XML中,我们先以矩形为例。

  • <corners>,表示4个角的角度,只适用于矩形shape,其实就是圆角角度。
  • <gradient>,表示内容的渐变效果,注意这个和<solid>是互斥的,solid表示是纯色填充,它的属性较多:
  1. android:angle,渐变的角度,默认是0,其值必须是45的倍数,0就是表示从左到右,90就是从下到上,比如上图改成90效果如图:

image.png

  1. android:startColor/centerColor/endColor,表示的就是渐变的起始色、中间色和结束色。
  2. android:centerX/centerY,渐变的中心点的横/纵坐标,渐变的中心点会影响渐变的具体效果。
  3. android:gradientRadius,渐变半径,仅当type为"radial"时有效。
  4. android:type,渐变的类别,有linear(线性渐变)、radial(径向渐变)和sweep(扫描渐变)这3种。

这里默认是线性渐变,其他2种效果如图:

image.png

其中这个径向渐变的半径需要特别注意,在实际使用中,因为Drawable作为View的背景时,其大小是不固定的,所以想其渐变色的大小正好和View一样大,一般需要手动设置:

//设置渐变半径为View的宽度一半
gradientView2.post {
    val gradientDrawable = gradientView2.background as GradientDrawable
    gradientDrawable.gradientRadius = gradientView2.width / 2F
}

注意这里ShapeDrawable的代码实现类是GradientDrawable

  • <stroke>,描边信息,这里有如下几个属性:
  1. android:width,描边的宽度,越大则边缘线看起来越粗。
  2. android:color,描边的颜色。
  3. android:dashWidth,组成虚线的线段的宽度。
  4. android:dashGap,组成虚线的线段之间的间隔,和上面属性有一个为0,则没有虚线效果。

除了上面所说的矩形,我们常用的还有圆形和环形,其中圆形没啥说的,比如下图效果:

image.png

对于环形使用比较特殊,它额外多了5个属性,如下:

  1. android:innerRadius,圆环的内半径,如果和innerRadiusRation同时存在,以该属性为准;
  2. android:thickness,圆环的厚度,即半径减去内径的大小,如果和thicknessRatio同时存在,以该属性为准;
  3. android:innerRadiusRatio,内径占整个Drawable的比例。
  4. android:thicknessRatio,厚度占的比例。
  5. android:useLevel,这个一般使用必须是false,除非被当作LevelListDrawable来使用。

效果如下图:

image.png

总的来说,ShapeDrawable是我们平时用的非常多的地方,对于其中的一些属性都必须要掌握。

LayerDrawable

这里Layer翻译为"层级"的意思,所以LayerDrawable所表达的就是一种层次化的Drawable集合,它对应的标签是<layer-list>,通过将不同的Drawable放置在不同的层上面从而达到一种叠加后的效果,其叠加效果就和FrameLayout即帧布局是一样的。

LayerDrawable所对应的XML布局属性较少,我们直接看代码:

<?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/black_666666"
                />
        </shape>
    </item>

    <item
        android:bottom="6dp"
        >
        <shape
            android:shape="rectangle"
            >
            <solid
                android:color="@color/white"
                />
        </shape>
    </item>

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

</layer-list>

可以发现一个LayerDrawable由多个item标签组成,而每个item就代表一个Drawable,其中item的属性也非常简单,就是android:top/bottom/left/right,表示Drawable相对于View的上下左右的偏移量,单位为像素。

所以上述XML代码中,我们先放置了一个灰色背景,然后放置一个白色遮挡,和下面偏移为6px,然后最后再放一个白色遮挡,和左、下、右都偏移1px,这样就可以实现一个输入框背景的效果,如下:

image.png

对于LayerDrawable的使用,主要是要熟悉其他Drawable的使用,使用遮盖叠加的特性实现不同效果。

StateListDrawable

这个是状态列表Drawable,每个Drawable都对应着View的一种状态,这也是我们非常常用的一种,它的标签是<selector>,它的使用场景就是当View的状态改变时,改变其背景,我们还是来个简单代码:

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

    <!--按下状态-->
    <item
        android:state_pressed="true"
        android:drawable="@drawable/drawable_shape_button_pressed"
        >
    </item>

    <!--默认状态-->
    <item
        android:drawable="@drawable/drawable_shape_button_normal"
        >
    </item>

</selector>

这里也是多个item组成的结果,其中常见的状态有如下:

  1. state_pressed:表示按下状态。
  2. state_focused:表示View已经获取了焦点。
  3. state_selected:表示用户选择了View。
  4. state_checked:表示用户选中了View,一般适用于CheckBox这类在选中和非选中之间进行切换的View。
  5. state_enabled:表示View当前处于可用状态。

比如上述XML代码所实现的效果如下图:

image.png

点击改变背景:

image.png

这里会有个问题,就是在selector中会定义多个item,每当View的状态变化时,系统按照从上到下的顺序来找到第一条匹配的item,所以默认的item应该放在selector最后一条并且不要附带任何的状态

LevelListDrawable

LevelListDrawable对应于<level-list>标签,它表示一个Drawable集合,集合中的每个Drawable都有一个等级(level)的概念,根据不同的等级,LevelListDrawable会切换为对应不同的Drawable,使用比较简单,比如下面代码:

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

    <item
        android:drawable="@color/color_1"
        android:maxLevel="0"
        />
    <item
        android:drawable="@color/color_2"
        android:maxLevel="1"
        />
    <item
        android:drawable="@color/color_3"
        android:maxLevel="2"
        />
    <item
        android:drawable="@color/color_4"
        android:maxLevel="3"
        />
    <item
        android:drawable="@color/color_5"
        android:maxLevel="4"
        />
    <item
        android:drawable="@color/color_6"
        android:maxLevel="5"
        />
    <item
        android:drawable="@color/color_7"
        android:maxLevel="6"
        />

</level-list>

注意这里minLevel和maxLevel表示当前Drawable所显示的范围,其中最小值为0,最大值为10000,然后在代码中,获取到该Drawable,设置其level即可:

var color = 0

private fun startTimer(){
    val timeTask = object : TimerTask(){
        override fun run() {
            val level = color % 7
            runOnUiThread {
                (levelListView.background as LevelListDrawable).level = level
            }
            color ++
        }
    }
    Timer().schedule(timeTask, Date(),300)
}

这里300ms变化一次背景,效果如下:

17.gif

这个类型的Drawable当背景有较多情况需要切换时,可用使用该方法。

TransitionDrawable

TransitionDrawable对应于<transition>标签,它可用实现俩个Drawable之间的淡入淡出的效果,这个还是非常方便的,使用起来非常方便,使用2个item表示需要变化的Drawable即可:

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

    <item
        android:drawable="@color/blue"
        />

    <item
        android:drawable="@color/black_999999"
        />

</transition>

然后调用其startTransition和reverseTransition方法来实现淡入淡出的效果以及它的逆向过程:

val drawable = transitionDrawable.background as TransitionDrawable
drawable.startTransition(1000)

InsetDrawable

这个翻译过来就是内嵌Drawable,顾名思义就是可用把其他Drawable内嵌到自己当中,并且可用在四周留出一定的间距。

当一个View希望自己的背景比自己实际区域小的时候,可用采用InsetDrawable来实现,同理,我们可用使用LayerDrawable进行遮盖、重叠实现一样的效果。

ScaleDrawable

熟悉动画的开发者都知道这个是啥了,就是缩放Drawable,ScaleDrawable可用根据自己的等级(Level)将指定的Drawable缩放到一定的比例。

但是它的使用有点复杂,稍微不注意会出错,首先下面是我们定义一个简单ScaleDrawable,其中使用<scale>标签,XML代码如下:

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

其中有2个属性必须注意:

  1. android:scaleHeight/scaleWidth,这个表示高度/宽度的缩放比例,比如设置为50%,则图片宽可以缩放到原来的50%,当设置为90%,则图片可以缩放到原来的10%。
  2. android:scaleGravity,这个和gravity属性一个意思,即缩放后的位置。

定义上面XML,直接使用会发现还是无法使用,这时需要设置其等级(Level),等级的范围是0-10000,默认为0,是不可见,如何理解这个呢?

这里假如图片宽度是1000,在XML中设置了scaleWidth为90%,那么这时level设置为1时,显示宽度就是10,当level设置为10000时,宽度就是1000。

比如下图GIF,上图设置了缩放范围为50%,下图设置为了90%:

21.gif

然后对于scaleGravity也比较容易理解,比如像下图GIF,上图设置了为left,下图设置为bottom:

22.gif

对于scaleDrawable实现的效果和动画类似,所以它表示的东西是完全不一样的,这个只是改变Drawable的大小,即图像的大小。

ClipDrawable

ClipDrawable对应的是<clip>标签,表示裁剪Drawable,其中主要属性就android:clipOrientation表示裁剪方向,有水平方向和竖直方向可选,还有就是android:gravity表示裁剪后Drawable在哪个位置,关于这个gravity的可选项非常多,而且每一种搭配都会产生不同的效果,整个候选项如下图:

image.png

比如下面XML代码:

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

从表中可知,这将是竖直方向,从顶部开始裁剪,除了这个,还需要配置等级(Level),用法和上面ScaleDrawable一样,范围是0-10000,下图gif表示上面的效果:

25.gif

这种效果在我们平时开发中,也遇到过这种需求。

好了,上面就是源码中内置的所有Drawable,其中有一些不是很熟悉的,API又比较多,我们不必都记住,有点印象即可,在使用时能想到即可。

自定义Drawable

Drawable的使用范围还是比较单一,一个是作为ImageView中的图像来显示,另一个就是作为View的背景,大多数情况下Drawable就是作为View的背景。

而且Drawable的工作原理非常简单,其核心就是draw方法,所以我们可以自定义Drawable。注意这里自定义Drawable和自定义View的区别,在前面也说了,Drawable可以看成一半功能的View,所以适合比较简单的场景。

通常我们是不必要自定义Drawable,因为自定义的Drawable无法在XML中使用,这就大大降低了Drawable的使用范围。我们现在就自定义一个简单的Drawable,来说一下其使用,比如下面代码:

class CustomDrawable(val color: Int) : Drawable(){

    private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    init {
        mPaint.color = color
    }

    override fun draw(canvas: Canvas) {
        val rect = bounds
        val centerX = rect.exactCenterX()
        val centerY = rect.exactCenterY()
        canvas.drawCircle(centerX,centerY, centerX.coerceAtMost(centerY),mPaint)
    }

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

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

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

这里绘制一个圆当作背景,且其大小会随着View的变化而变化,效果如下:

image.png

可以看出一共需要重新4个方法,其中draw方法为主要方法,通过canvas进行绘制,这里也就不多说了。

总结

Drawable作为最常用的图像,通过本章学习知道了其实有很多内置的效果,并且它的实现比自定义View简单,效率也比自定义View高,在后续工作中,如果能使用Drawable实现的,可以灵活使用。

本篇demo代码的地址: github.com/horizon1234…

猜你喜欢

转载自juejin.im/post/7115323096554274852