参考书籍:《Android开发艺术探索》 任玉刚
如有错漏,请批评指出!
在Android中,Drawable是一个抽象类,代表的是一种图像的概念。官方 Guide 对它的定义是:
A Drawable is a general abstraction for something that can be drawn.
Drawable 是对可绘制事物的一般抽象。
Drawable 的使用比较简单,比自定义View成本低;并且,非图片类型的Drawable占用内存小,对“apk瘦身”有帮助。因此熟练掌握各种类型的Drawable可以方便我们做一些特殊的UI效果。
Drawable 简介
-
Drawable一般都是通过xml来定义,设置为View的 background 属性值使用。在Android设计中,Drawable是一个抽象类,它是所有Drawable对象的基类。Drawable的层次关系如下:
Drawable 的分类
常见的Drawable如下:
- BitmapDrawable
- ShapeDrawable
- LayerDrawable
- StateListDrawable
- LevelListDrawable
- TransitionDrawable
- InsetDrawable
- ScaleDrawable
- ClipDrawable
1. BitmapDrawable
-
BitmapDrawable 实际就是引用一张图片,不过可以通过属性设置很多效果。
常用属性如下:属性 功能 src 指定资源文件(只能是 .png 或 .xml) antialias 抗锯齿,开启后会让图片变得平滑,会降低图片清晰度(可以忽略,肉眼几乎看不出来),建议开启 dither 防抖动,会根据手机屏幕像素配置优化图片显示效果,建议开启 filter 过滤效果,当图片被拉伸或压缩时,可以保持较好的显示效果,建议开启 gravity drawable在容器中的位置,可选值有top、bottom、start、end、center、center_vertical、center_horizontal、fill(填充满容器,默认值)、fill_vertical(竖直方向填充容器)、fill_horizontal(水平方向填充容器)、clip_vertical(竖直方向裁剪)、clip_horizontal(水平方向裁剪) tileMode 平铺模式,可选值有:disabled(关闭平铺模式,默认值, 开启后gravity属性失效)、clamp(像素扩展)、repeat(重复)、mirror(镜面投影) -
下面着重看看 tileMode 属性的区别:
-
示例:
<?xml version="1.0" encoding="utf-8"?> <bitmap xmlns:android="http://schemas.android.com/apk/res/android" android:src="@drawable/ic_android" android:antialias="true" android:dither="true" android:filter="true" android:gravity="" android:tileMode="repeat"/>
2. ShapeDrawable
-
ShapeDrawable 可以理解为通过颜色来构造的图形,既可以是纯色的图形,也可以是具有渐变效果的图形。
常用属性如下:属性 功能 shape 图形的形状,可选值有:rectangle(矩形,默认值)、oval(椭圆)、line(横线)、ring(圆环) corners 指定shape的四个角的圆角半径 solid 纯色填充,只有一个属性 color ,指定填充颜色 gradient 与<solid/>标签相对,表示渐变填充,包含如下属性:type(渐变类型,有linear(线性渐变)、radial(径向渐变)、sweep(扫描线渐变)三种)、centerX(渐变中心点X坐标)、centerY(渐变中心点Y坐标)、startColor(渐变起始颜色)、centerColor(渐变过渡颜色)、endColor(渐变结束颜色)、gradientRadius(渐变半径,类型为径向渐变时有效)、useLevel(当Drawable作为StateListDrawable使用时为true,一般为false) stroke 描边,包含如下属性:width(描边宽度)、color(描边颜色)、dashWidth(虚线长度,与dashGap均不为0时描边为虚线)、dashGap(虚线间隔) padding 包含这个Drawable的View的padding值 size ShapeDrawable大小,包含width和height两个属性,不过作为background属性指定给View时会充满View innerRadius 类型为 ring 时生效,表示圆环内半径 thickness 类型为 ring 时生效,表示圆环的宽度,即外半径减去内半径的宽度 innerRadiusRatio 内半径占整个Drawable宽度的比例,默认为9,与innerRadius同时存在时无效 thicknessRatio 圆环宽度占整个Drawable的比例,默认为3,与thickness同时存在时无效 useLevel 为false时圆环才能显示 详细使用及效果可以参考博客:Android XML shape 标签使用详解
3. LayerDrawable
- LayerDrawable 对应的 xml 标签是 <layer-list>,它表示一种层次化的 Drawable 集合,通过将不同的 Drawable 放置在不同的层上面从而达到一种叠加后的效果。
- 一个 layer-list 中可以包含多个 item,每个 item 表示一个 Drawable。
item 的常用属性:属性 功能 drawable 资源文件 top 顶部偏移量(px) right / end 右面偏移量 bottom 底部偏移量 left / start 左面偏移量 gravity Drawable在View中的位置 - 简单示例——输入框
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item> <shape android:shape="rectangle"> <solid android:color="@color/colorPrimary"/> </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 中可以包含多个 item,每个 item 表示一个 Drawable。
4. StateListDrawable
- StateListDrawable 对应于 <selector> 标签,它也表示 Drawable 集合。每个 item 对应 View 的一种状态,这样系统就会根据 View 的状态来选择合适的 Drawable。
- StateListDrawable 主要用于设置可单击的 View 的背景,最常见的就是 Button。
状态 含义 state_pressed 表示按下状态,比如Button被按下而没有松开的状态 state_focused 表示View获取了焦点 state_selected 表示用户选中了View state_checked 表示用户选中了View,一般适用于CheckBox 这类在选中和非选中状态之间进行切换的View state_enabled 表示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="@color/colorPrimary"/> <item android:state_pressed="false" android:drawable="@color/colorGray"/> </selector>
5. LevelListDrawable
- LevelListDrawable 对应于 <level-list> 标签,同样表示一个 Drawable 集合。每个 item 对应一个等级范围的概念,根据不同等级范围,LevelListDrawable 会切换为对应的 Drawable 效果。
- 关于 LevelListDrawable的控制,有三个属性:
属性 含义 drawable 资源文件 maxLevel 等级范围最大值 minLevel 等级范围最小值 - 示例:
<?xml version="1.0" encoding="utf-8"?> <level-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/colorGray" android:maxLevel="5"/> <item android:drawable="@color/colorTextYellow" android:maxLevel="10"/> <item android:drawable="@color/colorPrimary" android:minLevel="15"/> </level-list>
- 对于一般View,将 drawable 设置为 background,通过LevelListDrawable的setLevle()方法来控制等级。
Button but = findViewById(R.id.but_levellist); LevelListDrawable drawable = (LevelListDrawable) but.getBackground(); drawable.setLevel(10);
- 特别地,对于ImageView,将 drawable 设置为 src,通过ImageView的 setImageLevel() 方法控制等级。
ImageView img = findViewById(R.id.but_levellist); img.setImageLevel(10);
6. TransitionDrawable
- TransitionDrawable 对应于 <transition> 标签,用于实现两个Drawable之间的淡入淡出效果。
- 他的属性和layerDrawable相似,有drawable、top、start / left、bottom、end / right 等。
- 示例
同样可以作为一般View的 background 或者 ImageView 的 src,这里以一般View为例:<?xml version="1.0" encoding="utf-8"?> <transition xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/colorPrimary"/> <item android:drawable="@color/colorTextYellow"/> </transition>
TransitionDrawable 有两个方法来控制Drawable,startTransition() 和 reverseDrawable(),即开始淡入淡出效果及其逆过程。View view = findViewById(R.id.view); TransitionDrawable drawable = (TransitionDrawable) view.getBackground(); drawable.startTransition(1000);
7. InsetDrawable
- InsetDrawable 对应于 <inset>标签,它可以将其他Drawable内嵌到自己当中。
- InsetDrawable 有inset(设置四周边距)、insetTop、insetLeft、insetBottom、insetRight、drawable等属性
- 示例:
这是一个类似于卡片的效果,可以作为ListView 的 item 背景,实现卡片式的item布局。<?xml version="1.0" encoding="utf-8"?> <inset ="http://schemas.android.com/apk/res/android" android:inset="10dp" android:drawable="@color/colorWhite"> <shape android:shape="rectangle"> <solid android:color="@color/colorPrimary"/> <stroke android:color="@color/colorWeakBlack" android:width="1dp"/> <corners android:radius="16dp"/> </shape> </inset>
8. ScaleDrawable
- ScaleDrawable 对应于 <scale> 标签,包含以下属性:
属性 含义 drawable 资源文件 scaleGravity 等同于 shape 中的 gravity scaleHeight 高度缩小的比例(如:25%,表示缩小25%,而不是缩小到25%) scaleWidth 宽度缩小的比例(同 scaleHeight) useIntrinsicSizeAsMinimum 是否以drawable的固有宽高作为最小值(默认false,后面解释) level 等级,API24 新增(后面解释) - 要理解 ScaleDrawable,我们先来解释几个概念
- IntrinsicSize:drawable 的固有宽高,对于图片等资源而言,固有宽高就是实际的图片宽高;需要注意,大多drawable是没有宽高的概念的,比如只设置了 solid 属性的<shape>,它表示一个纯色填充,如果给这个 <shape> 的 <size> 标签设置了宽高属性,那么这个 <shape> 就有了固有宽高了。
- level:Drawable 中的这个 level 变量,在不同的Drawable实现类中有不同的含义,在这里用来计算缩放的比例,它的范围是:0~10000,在计算时 (10000 - levle) / 10000 表示缩小的比例。
- 为了更好地理解,下面来看看源码:
上面是 ScaleDrawable 的 draw() 方法,可以看到,只有当 level 不为 0 时,才会绘制。而 level 的默认值就是0,因此必须设置 level 值,ScaleDrawable才能显示。@Override public void draw(Canvas canvas) { final Drawable d = getDrawable(); if (d != null && d.getLevel() != 0) { d.draw(canvas); } }
这一段是 onBoundsChange() 方法,在 drawable 绘制时会被回调,上面的代码中注释很详细,下面就给出几个结论:@Override protected void onBoundsChange(Rect bounds) { final Drawable d = getDrawable(); final Rect r = mTmpRect; // 对应 xml 中的 useIntrinsicSizeAsMinimum 属性 final boolean min = mState.mUseIntrinsicSizeAsMin; final int level = getLevel(); // Drawable 最终的显示区域宽度 int w = bounds.width(); // 这个 mScaleWidth 就是 xml 中指定的 scaleWidth 属性,也必须是大于 0 的,否则就不会缩放 if (mState.mScaleWidth > 0) { // 在这里 IntrinsicWidth 就起作用了,不过 IntrinsicWidth 大多时候为 0 // 因此,min 大多时候是 0 final int iw = min ? d.getIntrinsicWidth() : 0; // MAX_LEVEL 就是10000, 这个公式是关键, 从这里可以得出下面的结论 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); } }
- drawable 的缩放比例由 xml 中的 scaleWidth / scaleHeight 和 level 共同决定。并且,scaleWidth / scaleHeight 越大,drawable越小;level 越大,drawable越大。
- 在使用时,为了方便,我们一般将 level 设置为1,这样 (MAX_LEVEL - level) / MAX_LEVEL 就约等于 1,因此,level对缩放的影响几乎没有了。在API 23以下,我们需要使用Java代码设置level,在 API24 以上,则可以在xml 中设置。
- 示例:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <View android:id="@+id/view" android:layout_width="match_parent" android:layout_height="200dp" android:background="@drawable/drawable_scale" /> <View android:layout_width="match_parent" android:layout_height="0.1dp" android:background="@color/colorWeakBlack"/> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <scale xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@color/colorPrimary" android:scaleGravity="center" android:scaleHeight="30%" android:scaleWidth="30%" />
上面的示例中,drawable的显示区域就是View的宽高,缩小30%,因此最终显示的大小就是View的70%。View view = findViewById(R.id.view); ScaleDrawable drawable = (ScaleDrawable) view.getBackground(); drawable.setLevel(1);
9. ClipDrawable
- ClipDrawable 对应于 <clip> 标签,他可以对drawable进行裁剪。
- 它包含三个属性:
属性 含义 drawable 资源文件 gravity 控制裁剪位置 clipOrientation 裁剪方向(vertical(竖直),orientation(水平)) - 在 ClipDrawable 中,level 的作用是控制 Drawable 的裁剪百分比,值越大,裁剪区域越小。(0表示不裁剪,10000表示全部裁剪,不显示)
- 下面通过一个示例来看看:
<?xml version="1.0" encoding="utf-8"?> <clip xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/ic_image" android:gravity="left" android:clipOrientation="horizontal"/>
View view = findViewById(R.id.view); ClipDrawable drawable = (ClipDrawable) view.getBackground(); drawable.setLevel(8000);
这种 Drawable 中,gravity 属性的控制效果有很多,可以自行尝试,看看效果。