Detailed explanation of Android's Drawable

foreword

This article is the post-reading and practical demo of Chapter 6 of "Android Development Art Exploration", which is related to Android's Drawable knowledge.

When I wrote the Bitmap article before, there was a very interesting question, that is, in Android, what is the difference between Bitmap, Drawable and custom View? It seems that in normal development, when we encounter simple UI requirements, such as rounded button backgrounds, our first reaction is to use Drawable; then for slightly more complex UI requirements, we customize View to achieve; but simple UI requirements , we can also use custom View to achieve, and BitmapDrawable can also load Bitmap, what is the meaning of this Drawable?

Bitmap is the bitmap data used to save pictures, which is to save pictures; View achieves the effect through the steps of measurement, layout and drawing; Drawable is an abstract concept that can be drawn on Canvas, so to speak, It has half the functionality of View; since it only has half the functionality of View, is it necessary to exist?

Of course it is necessary, Drawable can simplify the realization of some image effects, the biggest advantage is convenience . Drawable is generally used as the background of the View, and the implementation is to define it in the XML file. If these effects use a custom View, it will be a lot of trouble.

There are many built-in types of Drawables in the source code. These Drawables can construct various image effects through pictures and colors. Mastering these Drawables can avoid duplicating wheels during development.

text

We first introduce one of the more important parameters of Drawable, and then introduce and use the various Drawables built-in one by one.

Internal width and height

Generally speaking, Drawable has no concept of size, because Drawable can be composed of pictures or colors. When using color composition, drawable is used as the background of View, and its size will be stretched to the same size of View .

However, it has the parameter of internal width/height, which can be obtained through the two methods of getIntrinsicWidth and getIntrisicHeight. Of course, not all Drawables have this concept; for example, a Drawable composed of a picture, its internal width and height is the width of the picture. Height, and color construction, there is no concept of internal width and height.

Classification of Drawables

There are many types of Drawables. Let's list them one by one and talk about their usage details.

BitmapDrawable

The first is the simplest BitmapDrawable, which represents a picture. In actual development, we can directly refer to the original picture, or we can use XML to describe the BitmapDrawable.

It may be said that I can use the image directly, why use BitmapDrawable? This is related to the most common usage scenario of Drawable, that is, it can be used as the background of View. We can configure some properties through BitmapDrawable to achieve different effects.

Use XML to define BitmapDrawable as follows:

<?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"
    />

Then use it as the background of a fullscreen page:

<?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>

The effect obtained is as follows:

image.png

It can be initially judged that the original apple picture is stretched to fit the entire page size.

It can be found that there are a total of 7 attributes in the above XML, let's talk about them separately:

  • android:src is the resource id of the image.
  • android:antialias, whether to enable the image anti-aliasing function, after it is enabled, the image will be smoothed, and at the same time, it will reduce the clarity of the image to a certain extent, but the reduction rate can be ignored, so the anti-aliasing option should be enabled.
  • android:dither, whether to enable the dither effect. When the pixel configuration of the picture is inconsistent with the pixel configuration of the mobile phone screen, enabling this option allows high-quality pictures to maintain a better display effect on low-quality screens. For example, the default color mode of Bitmap is ARGB_8888. When the color mode of the device is RGB_565, turning on the dither option at this time can make the picture not too distorted, so it should be turned on by default.
  • android:filter, whether to enable the filter effect. When the image size is stretched or compressed, enabling the filter effect can maintain a better display effect. It should be enabled by default.
  • 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…

Guess you like

Origin juejin.im/post/7115323096554274852
Recommended