前言
最近做项目的时候用到了PrgressBar的进度展示图片,可以使用progressDrawable属性配置它的进度图片,配置XML写了好几遍都没有达到预期效果,Drawable用起来非常简单,实践中还是会遇到不少坑,这里来总结下平时经常使用到的Drawable对象。
ShapeDrawable
ShapeDrawable可以用来定一个基本的几何图形,比如android:shape=[“rectangle” | “oval” | “line” | “ring”]分别代表矩形、椭圆、线段和环形;除了基本形状外还可以在内部定义size 、padding、solid、corners、stroke和gradient几个子标签,如果内部定义了gradient标签,那么实际上生成的是GradientDrawable对象。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="@color/colorAccent" android:width="1dp" />
</shape>
LayerDrawable
LayerDrawable可以将内部的Drawable对象一层一层地叠加起来,它的实现非常类似于FrameLayout,就是后加入的Drawable展示之前加入Drawable上方,而且内部的每个Drawable都可以在item标签里定义自己的重力和上下左右的分隔。
<?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/colorAccent" />
</shape>
</item>
<item android:gravity="top|center_horizontal"
android:bottom="2dp"
android:left="2dp"
android:right="2dp">
<shape android:shape="rectangle">
<solid android:color="#ffffff" />
</shape>
</item>
<item android:gravity="top"
android:bottom="6dp">
<shape android:shape="rectangle">
<solid android:color="#ffffff" />
</shape>
</item>
</layer-list>
上面的例子有三个图层,第一层是全部带颜色的色块,第二层则是左右下三个位置都留有2dp间隙的白色色块,这样就实现了一个带左右下三个边框的背景,最后再加一个只有底部留6dp间隙的白色色块,最终就形成了一个常见的TextView背景图。
StateListDrawable
selector标签内部可以包含多个Drawable对象,每个Drawable对象都对应着某种状态,最常见的就是用户按下和松开的按钮背景变色效果,其中按下对应的是state_pressed。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/border_bg" android:state_pressed="true" />
<item android:drawable="@drawable/rectangle_bg" />
</selector>
// rectangle_bg
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorAccent" />
</shape>
// border_bg
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:color="@color/colorAccent" android:width="1dp" />
</shape>
下面列举一下常用的状态值,开发者可以在Selector中定义各种状态对应展示的Drawable,系统会根据View所处的状态自动切换展示。
状态 | 意义 |
---|---|
enable | 是否处于可用状态,比如Button在提交数据未返回时可以设置为disabled,防止用户重复提交,通过setEnable设置 |
focused | 是否处于聚焦状态,通常都是通过硬件上下左右键等才会触发,一般用户不用关心 |
pressed | 是否处于按下状态,通常都是用户在触摸屏上按下可点击View才会触发 |
selected | 是否处于选中状态,这个通常都是开发者通过setSelect这个方法来实现逻辑选中 |
checked | 是否处于签中状态,常用的CheckBox会有这种状态,也就是有没有被勾选 |
InsetDrawable
inset标签可以为Drawable添加边缘补白,通常为了某些View之间添加间隔需要增加View树的深度,使用InsetDrawable之后会自动在Drawable内部增加间隔,减少了View树深度,提高显示效率。
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/moon"
android:insetLeft="10dp"
android:insetRight="10dp"
android:insetTop="10dp"
android:insetBottom="10dp">
</inset>
TransitionDrawable
transition标签能够很自然地切换两个不同的Drawable背景,提升用户体验,查看它内部的实现源码会发现其实是利用了Drawable的setAlpha接口,通过不断的改变第一个图片的alpha值到0同时增加第二个图片的alpha值到255,实现了无缝的背景切换效果。
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/moon" />
<item android:drawable="@drawable/pool" />
</transition>
TransitionDrawable transitionDrawable = (TransitionDrawable) transiteImg.getDrawable();
transitionDrawable.startTransition(300);
LevelDrawable
level-list标签能够定义不同level等级下展示的不同图片,下面的里子就是根据等级不同展示白天或者晚上的图片,用户可以不断的修改level值,View内部会自动根据当前设置的level修改展示的图片。
<level-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/moon" android:minLevel="0" android:maxLevel="50" />
<item android:drawable="@drawable/pool" android:minLevel="51" android:maxLevel="100" />
</level-list>
ClipDrawable
clip标签能够通过level等级决定展示的图片大小,其他的部分都被剪切掉了,它有两个重要的参数android:clipOrientation和android:gravity,对它们设置不同的值就能够实现不同的剪切位置和剪切方式。查看其内部实现源码会发现其实就是在Drawable的onDraw的时候调用了Canvas.clipPath方法剪切掉其他部分。
<clip xmlns:android="http://schemas.android.com/apk/res/android"
android:clipOrientation="horizontal"
android:gravity="left">
<shape android:shape="rectangle">
<gradient
android:startColor="#00ffff"
android:endColor="#ff517e" />
<corners android:radius="5dp" />
</shape>
</clip>
clipImg.setImageLevel(level);
ScaleDrawable
scale标签能够缩放包裹在内部的Drawable对象,它也需要根据图片level等级值调整内部Drawable的缩放大小。在使用过程感觉这个缩放功能的特别费解,图片要么就是根本不展示,要么就是展示了根本不会改变大小。
<scale xmlns:android="http://schemas.android.com/apk/res/android"
android:scaleWidth="100%">
<shape
android:shape="rectangle">
<gradient
android:startColor="#00ff00"
android:endColor="#ff517e" />
<corners android:radius="5dp" />
</shape>
</scale>
// 先初始化
scaleImg.getDrawable().setLevel(10000);
// 再设置具体值
scaleImg.getDrawable().setLevel(level);
查看它的onDraw方法会发现它在图片的level等级为0是根本不会做绘制操作,所以即使配置正确了scale标签还需要在代码里为它设置初始的图片level等级。还有就是如果即设置了android:scaleWidth又设置了android:scaleHeight发现即使改变level等级值也不会缩放,这时因为一旦宽高都设置了就会把宽度和高度都随着level变化,然而上面要缩放的Drawable高度太小减去了缩放值之后变成了负数或零,最后一句因为要求宽高都是正数就导致没有实现缩放效果,因而在缩放Drawable时要避免出现计算结果为负值的情况。
@Override
public void draw(Canvas canvas) {
final Drawable d = getDrawable();
if (d != null && d.getLevel() != 0) {
d.draw(canvas);
}
}
@Override
protected void onBoundsChange(Rect bounds) {
final Drawable d = getDrawable();
final Rect r = mTmpRect;
final boolean min = mState.mUseIntrinsicSizeAsMin;
final int level = getLevel();
// 根据level计算应该展示的宽度
int w = bounds.width();
if (mState.mScaleWidth > 0) {
final int iw = min ? d.getIntrinsicWidth() : 0;
w -= (int) ((w - iw) * (MAX_LEVEL - level) * mState.mScaleWidth / MAX_LEVEL);
}
// 根据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);
}
}