前面已经学习了Android绘图与动画中的一些基础知识,这次来学习下安卓中提供的 Drawable。其实本应该在学习Animation或者Bitmap之前学习Drawable的,在学习Animation的帧动画中我们也看到实现Animation中的帧动画所用到的类就是AnimDrawable,然后代码中调用AnimDrawable实例的start()以及stop()开始或停止播放帧动画,Bitmap其实除了之前学习的BitmapFactory方法外也可以通过BitmapDrawable获取。Android中提供了多达13种的 Drawable,这里就来学习下安卓提供的一些常用的Drawable。
Drawable
Drawable是一种可以在Canvas上进行绘制的对象,即可绘制物;Drawable类是抽象类,是接下来要说的各种Drawable的基类。它常常用于View的背景或者作为ImageView中的图像显示。
Drawable可分为两种:
- 1.常见的普通的图片资源,一般放在res/mipmap或者res/drawable下也可以,可通过R.drawable/mipmap.xxx方式使用
- 2.XML形式的Drawable资源,一般放在res/drawable下,然后在布局文件中通过android:background = "@drawable/xxx"引入布局中。
也可以在Java 代码中通过Resource的getDrawable(R.drawable/mipmap.xxx)获得drawable资源或者new一个所需Drawable并set相关属性,最后加载到布局中
例如之前学习帧动画时(用到的类为AnimDrawable)在res/drawable定义的meizi.xml:
然后在activity_main.xml中通过 android:background="@drawable/meizi"将上面的Drawable资源引入到布局中:
Drawable 没有大小概念,某些Drawable可以通过getIntrinsicWidth()和getIntrinsicHeight()获取其内部宽和高。一些特殊的例子例如图片所形成的Drawable的内部宽和高就是图片的宽和高,颜色所形成的Drawable没有内部宽和高的概念。
ColorDrawable
前面说过Drawable是一种可以在Canvas上进行绘制的对象,那么当我们将ColorDrawable绘制到Canvas上的时候, 会使用一种固定的颜色来填充Paint,然后在画布上绘制出一片单色区域。这里比较简单通过Demo来学习,布局文件比较简单就不写了:
修改color.xml文件:
MainActicity:
public class MainActivity extends AppCompatActivity {
private ColorDrawable colorDrawable;
private TextView textView;
private Button button1,button2,button3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindviews();
//TextView Java中定义ColorDrawable:
colorDrawable = new ColorDrawable(0xffff2200);
textView.setBackground(colorDrawable);
//Button1 在xml中定义ColorDrawable:
int mycolor = getResources().getColor(R.color.mycolor);
button1.setBackgroundColor(mycolor);
//使用系统定义好的color
int getcolor = Resources.getSystem().getColor(android.R.color.darker_gray);
button2.setBackgroundColor(getcolor);
//前面在学习Animation时提到的ARGB方式
button3.setBackgroundColor(Color.argb(0x00, 0x00, 0x00, 0x00));
}
private void bindviews() {
textView = findViewById(R.id.textview);
button1 = findViewById(R.id.button1);
button2 = findViewById(R.id.button2);
button3 = findViewById(R.id.button3);
}
}
效果如下:
ShapeDrawable
ShapeDrawable可表示纯色或有渐变效果的基础几何图形(矩形,圆形,线条等),根结点是shape节点,子节点较多。主要包含以下:
(1)< shape > 根节点 设置图形的形状 , 主要属性如下:
- visible:设置是否可见
- shape:形状,可选值包括rectangle(矩形,包括正方形),oval(椭圆,包括圆),line(线段),ring(环形)
- innerRadiusRatio:当shape为ring时有效,表示环内半径所占半径的比率,如果设置了innerRadius会被忽略
- innerRadius:当shape为ring时有效,表示环的内半径的尺寸
- thicknessRatio:当shape为ring时有效,表环厚度占半径的比率
- thickness:当shape为ring时有效,表示环的厚度,即外半径与内半径的差
- useLevel:当shape为ring时有效,表示是否允许根据level来显示环的一部分,通常为false
(2)< size >: 图形的固有大小
- width:设定shape宽度
- height:设定shape高度
(3) < corners > shape的四个圆角的角度,只适用于矩形
- radius:圆角半径,为上下左右四个角设置相同的角度
- topLeftRadius,topRightRadius,BottomLeftRadius,tBottomRightRadius: 依次为左上,右上,左下,右下的圆角值,优先级高于radius
(4) < gradient >渐变效果,与< solid >纯色填充相排斥
- startColor:渐变的起始颜色
- centerColor:渐变的中间颜色
- endColor:渐变的结束颜色
- type:渐变类型,可选linear线性渐变(可设置渐变角度angle),radial发散/径向渐变(gradientRadius必须设置),sweep平铺/扫描线渐变
- centerX:渐变中心点的x坐标,取值范围为:0-1
- centerY:渐变中心点的Y坐标,取值范围为:0-1
- angle:只有linear类型的渐变才有效,表示渐变角度,值必须为45的倍数;0表示从左到右,90表示从下到上
- gradientRadius:渐变效果的半径,只有radial和sweep类型的渐变才有效
- useLevel:判断是否根据level绘制渐变效果,当Drawable为StateListDrawable时值为true
(5)< solid >纯色填充,与< gradient >渐变效果排斥
- color:背景填充色,设置solid后会覆盖gradient设置的所有效果
(6)< stroke > 描边:
- width:边框的宽度,值越大,shape的边缘线越粗
- color:边框的颜色
- dashWidth:边框虚线段的长度
- dashGap:边框的虚线段的间距
(7)< padding >
- left,top,right,bottm:依次是左上右下方向上与四周空白的边距
这里也通过Demo学习下,在res/drawable下新建两个Drawable shape文件:
rect.xml:矩形渐变效果
rect_circle.xml:矩形纯色填充
activity_main.xml:
效果如下:
GradientDrawable
属性前面已经介绍过了,这里直接通过Demo来验证:
新建linear.xml:
radial.xml:
sweep.xml:
activity_main.xml:
三种效果如下:
BitmapDrawable
Bitmap的一种封装,用于表示图片,可以设置被它包装的Bitmap在BitmapDrawable区域中的绘制方式,包括:平铺填充,拉伸填或保持图片原始大小。根节点为< bitmap >,主要属性如下:
- src:图片资源id
- antialias:是否支持抗锯齿
- filter:是否支持位图过滤,当图片尺寸被拉伸或压缩时,开启过滤效果可保持较好的显示效果
- dither:是否对位图进行抖动处理,开启后高质量的图片在比较低质量的屏幕上不失真
- gravity:若位图比容器小,可以设置位图在容器中的相对位置
- tileMode:指定图片平铺填充容器的模式,设置这个的话,gravity属性会被忽略,有以下可选值:
disabled(整个图案拉伸平铺),clamp(原图大小), repeat(平铺),mirror(镜像平铺)
通过Demo来验证效果:
新建tilemode.xml:
activity_main.xml中设置一个View:
tilemode.xml指定不同的tileMode效果如下:
disabled:
clamp原图:
repeat平铺:
mirror镜像平铺:
Java 代码中:
public class MainActivity extends AppCompatActivity {
private LinearLayout myLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myLayout = findViewById(R.id.mylayout);
//创建BitmapDrawable
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);
bitmapDrawable.setDither(true);
bitmapDrawable.setTileModeXY(Shader.TileMode.MIRROR, Shader.TileMode.MIRROR);//MIRROR镜面平铺
//加载
myLayout.setBackgroundDrawable(bitmapDrawable);
}
}
效果如下:
InsetDrawable
表示把一个Drawable嵌入到另外一个Drawable的内部,并且在内部留一些间距, 与Drawable的padding属性相似,但padding属性表示的是Drawable的内容与Drawable本身的边距, 而InsetDrawable表示的是两个Drawable与容器之间的边距,当控件需要的背景比实际的边框 小的时候,比较适合使用InsetDrawable。根节点为< inset >,常用属性包括:
- drawable:引用的Drawable资源id,如果为空,必须有一个Drawable类型的子节点
- visible:设置Drawable是否留有边距
- insetLeft,insetRight,insetTop,insetBottm:设置距离容器左右上下的边距
通过Demo验证一下,在res/drawable新建inset.xml:
效果如下:
ClipDrawable
表示裁剪一个Drawable,根节点为< clip >。主要属性如下:
- clipOrietntion:设置剪切的方向,可以设置水平和竖直2个方向。并且需要在Java代码中调用setLevel()方法控制可见区大小。level为0表示完全裁剪,即不可见;值为10000表示不裁剪,level越大可见区越大。例如Android中的进度条就是使用ClipDrawable来实现的,它通过设置level的值来决定剪切区域即可见区域的大小
- gravity:从哪个位置开始裁剪,需要和clipOrientation配合使用
- drawable:引用的drawable资源id,为空的话需要有一个Drawable类型的子节点
通过Demo来学习一下,在res/drawable中定义clip.xml:
这里设置为水平方向从左开始裁剪
activity_main.xml:将imageview的src设置为clip
MainActivity:
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
private ClipDrawable clipDrawable;
int level;
private Handler handler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 0x123){
clipDrawable.setLevel(level);
level += 500;//每次裁剪500
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageview);
clipDrawable = (ClipDrawable) imageView.getDrawable();
level = clipDrawable.getLevel();
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(0x123);
if (level>10000){
timer.cancel();
}
}
},0,50);//50ms为一个周期去裁剪
}
}
效果如下图所示:
LayerDrawable
表示一种层次化的Drawable集合,包含一个Drawable数组,然后按照数组对应的顺序来绘制,索引值最大的Drawable会被绘制在最上层。虽然这些Drawable会有交叉或者重叠的区域,但它们位于不同的层,所以并不会相互影响,根节点为< layer-list >,< item >常用属性如下:
- drawable:引用的位图资源id,如果为空需要有一个Drawable类型的子节点
- id:层的id
- left:层相对于容器的左边距
- right:层相对于容器的右边距
- top:层相对于容器的上边距
- bottom:层相对于容器的下边距
Demo实例
res/drawable新建layer.xml:
activity_main.xml:
效果如下:
TransitionDrawable
TransitionDrawable是LayerDrawable的子类,TransitionDrawable只管理两层的Drawable,并且提供了透明度变化的动画,可以控制一层Drawable过度到另一层Drawable的动画效果。 根节点为< transition >,常用属性和LayerDrawable相同。并且需要调用startTransition方法才能启动两层间的切换动画, 也可以调用reverseTransition()方法反过来播放。
通过Demo来学习,在res/drawable创建一个TransitionDrawable的xml文件:
MainActivity:调用startTransition方法启动两层间的切换动画
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView = findViewById(R.id.imageview);
TransitionDrawable transitionDrawable = (TransitionDrawable) imageView.getDrawable();
transitionDrawable.startTransition(3000);
//transitionDrawable.reverseTransition(3000);
}
}
效果如下:
LevelListDrawable
用来管理一组Drawable的,我们可以为里面的drawable设置不同的level, 当他们绘制的时候,会根据level属性值获取对应的drawable绘制到Canvas上,根节点为:< level-list >没有可以设置的属性 ,但是可以设置每个< item > 的属性。item属性如下:
- drawable:引用的位图资源,如果为空需要有一个Drawable类型的子节点
- minlevel:level对应的最小值,取值范围为0~10000,默认为0
- maxlevel:level对应的最大值,取值范围为0~10000,默认为0
LevelListDrawable若作为View背景,需要在Java代码中调用setLevel()方法,若作为ImageView前景,需要调用setImageLevel()。
加载时当某item的android:maxLevel 等于 setLevel所设置的数值时就会被加载。若都没有匹配的则都不显示。
通过Demo来学习:
res/drawable新建三个shape文件:
MainActivity:
public class MainActivity extends AppCompatActivity {
private ImageView imageview;
private LevelListDrawable levelListDrawable;
int level = 0;
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
if (msg.what == 0x123) {
if (levelListDrawable.getLevel() > 6000) {
levelListDrawable.setLevel(0);
}
imageview.setImageLevel(levelListDrawable.getLevel() + 2000);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageview = findViewById(R.id.imageview);
levelListDrawable = (LevelListDrawable) imageview.getDrawable();
imageview.setImageLevel(0);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(0x123);
}
}, 0, 100);
}
}
效果如下:
StateListDrawable
表示一个Drawable的集合,每个Drawable对应着View的一种状态。根节点为< selctor >,常用属性包括:
- constantSize:固有大小是否随其状态的改变而改变。默认为false,表示固有大小会随着状态的改变而改变。为true,则表示固有大小是固定值,是内部所有Drawable的固有大小中的最大值
- dither:是否开启抖动效果。开启后让高质量的图片的比较低质量的屏幕上不失真
- variblePadding:其padding是否随状态的改变而改变。默认为false,表示padding是固定值,是其内部所有Drawable的padding中的最大值。为true,则表示padding会随着状态的改变而改变
< item >的属性包括:
- drawable:引用的Drawable位图id
- state_focused:是否获得焦点
- state_window_focused:是否获得窗口焦点
- state_enabled:控件是否可用
- state_checkable:控件可否被勾选,适用于checkbox这类组件
- state_checked:控件是否被勾选
- state_selected:控件是否被选择,针对有滚轮的情况
- state_pressed:控件是否被按下
- state_active:控件是否处于活动状态,适用于slidingTab这类组件
- state_single:控件包含多个子控件时,确定是否只显示一个子控件
- state_first:控件包含多个子控件时,确定第一个子控件是否处于显示状态
- state_middle:控件包含多个子控件时,确定中间一个子控件是否处于显示状态
- state_last:控件包含多个子控件时,确定最后一个子控件是否处于显示状态
简单的Demo,按钮点击改变颜色:
res/drawable新建shape_btn_normal.xml:代表普通状态下的shape
shape_btn_pressed.xml:代表按下状态下的shape
activity_main.xml:
除此之外,还要新建selector文件将两者结合起来:
state_pressed就代表着上面介绍属性说的控件是否被按下,效果如下:
AnimDrawable
参考之前的文章安卓开发之Animation学习(帧、补间、属性动画)中帧动画。
自定义Drawable
涉及到自定义View的一些知识,之后再系统的学习,这里先了解一下,参考《安卓开发艺术探索》。
前面说过Drawable 的使用范围比较单一,或作为ImageView中的图像来显示,或作为View的背景,后者用途居多。创建自定义Drawable,必须重写其draw()、setAlpha()、setColorFilter()、getOpacity()等方法,下面是一个自定义Drawable的实现过程,通过自定义Drawable来绘制一个圆形的Drawable,并且它的半径会随着View的变化而变化,这种Drawable可以作为View的通用背景,代码如下所示。
public class MyDrawable extends Drawable {
private Paint paint;
public MyDrawable(int color){
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(color);
}
@Override
public void draw(@NonNull Canvas canvas) {
//得到Drawable的实际大小,一般和View尺寸相同
final Rect rect = getBounds();
//精确获取矩阵中心点
float x = rect.exactCenterX();
float y = rect.exactCenterY();
canvas.drawCircle(x,y,Math.min(x,y),paint);
}
@Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
invalidateSelf();
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
paint.setColorFilter(colorFilter);
invalidateSelf();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
当自定义的Drawable有固有大小时,最好重写getIntrinsicWidth()和getIntrinsicHeight()这两个方法,因为它会影响到View的wrap_content布局。比如自定义Drawable是绘制一-张图片,那么这个Drawable的内部大小就可以选用图片的大小。在上面的例子中,自定义的Drawable是由颜色填充的圆形并且没有固定的大小,因此没有重写这两个方法,这个时候它的内部大小为-1,即内部宽度和内部高度都
为-1。需要注意的是,内部大小不等于Drawable的实际区域大小,Drawable 的实际区域大小可以通过它的getBounds方法来得到,一般来说它和View的尺寸相同。