1 Drawable概述
Drawable是一种图像的概念,但又不全是图片,也可能是各种颜色组合而成的图像。通常将Drawable作为View的背景,而这些Drawable我们一般通过XML来定义,当然也可以通过代码来实现,但是并没有XML来得方便。Drawable是一个抽象类,其子类有我们熟悉的BitmapDrawable等。
Drawable内部有固有宽高的概念,通过getIntrinsicWidth
和getIntrinsicHeight
来获得,然而并不是所有的drawable都有这两个值,BitmapDrawable这两个值的大小是图片的宽高,而单纯以颜色形成的Drawable的这两个值一般都是-1(不过是可以修改的)。但是注意,这两个值并不代表Drawable的宽高,一般而言Drawable没有宽高的概念,当用作背景的时候,Drawable会被拉伸到View的同等大小(注意:只是一般而言,存在特例,请看下文)。
那么IntrinsicWidth和IntrinsicHeight有什么用呢?还记得在View的默认的onMeasure方法吗?
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
当Spec的mode为UNSPECIFIED时,getDefaultSize会以getSuggestedMinimumWidth()
的返回值作为View的大小。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
而Drawable的getMinimumWidth()
方法如下:
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
总结一下就是,View默认的onMeasure方法在MeasureSpec为UNSPECIFIED时,会以背景Drawable的IntrinsicWidth和IntrinsicHeight作为宽和高,如果没有背景就以XML中设定的minWidth和minHeight作为宽高。
当然这只是默认的onMeasure方法了,而且还是UNSPECIFIED这么不常见的情况。但其实getSuggestedMinimumWidth这个方法在很多View的子类(比如说ImageView,TextView等)的重载的onMeasure方法中都有调用,用来作为View尺寸的下限,可以说应用的还是蛮普遍的。
另外注意一点,图片的IntrinsicWidth和IntrinsicHeight的值是已经按照图片所在的文件夹(drawable-mdpi,drawable-hdpi,drawable-xhdpi这些)和手机的像素密度进行缩放过的了。换言之一张放在mdpi文件夹中的图片在mdpi的手机和xhdpi的手机上获取的IntrinsicWidth大小比是1:2(因为获得的是像素数嘛)。
2 Drawable的子类们
Drawable的子类有很多,BitmapDrawable,ScaleDrawable,StateListDrawable等等,这些Drawable既可以在代码中直接使用,也可以通过他们对应的XML标签定义Drawable文件的方式来使用。这些标签单独使用功能还稍显乏力,但是由于有很多是可以互相嵌套的,所以最终也可以形成很复杂很有用的效果。
具体每个标签有什么属性,怎么使用就不提了。这里有一个网页,其中标签的使用方式和嵌套结构看起来一目了然。
http://idunnolol.com/android/drawables.html
但是有点需要补充的:
<layer-list>
中的<item>
标签的属性除了文中提到的还有width,height,gravity这三个。
我们可以用width或height设置每一层item内部drawable的size,如果应用这个drawable的View的宽高要大于某一item的宽高,可以用gravity来设置这一item的对齐方式(类似于View和ViewGroup的关系)。而这也就是前文提到“一般drawable没有宽高,作为背景会被拉伸到View的宽高”存在的特例。
具体可以看下面的示例:
这是作为背景的drawable文件layer_list_test_bg:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="100dp"
android:gravity="left"
android:left="30dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/holo_red_dark"></solid>
</shape>
</item>
<item
android:width="100dp"
android:height="100dp"
android:gravity="right">
<shape android:shape="rectangle">
<solid android:color="#AA00FF88"></solid>
</shape>
</item>
<item
android:width="100dp"
android:height="100dp"
android:gravity="left">
<bitmap android:src="@drawable/photo1" android:alpha="0.9"></bitmap>
</item>
<item>
<shape>
<stroke
android:width="1dp"
android:color="@android:color/black"></stroke>
</shape>
</item>
</layer-list>
这是应用上述drawable作为背景的Button的布局
<Button
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@drawable/layer_list_test_bg"/>
最终呈现的效果是这样的
特意将图片和绿色设成了半透明,并为按钮加上了边框,显然各层drawable并没有被拉伸,而是保持了<item>
标签中声明的size。
另外再说一下<shape>
标签的子标签中的<size>
标签。这个标签可以用来设置当前shape的instrincWidth和intrinsicHeight,这也就是我们上文提到的改变instrincWidth和intrinsicHeight的方法。
3 自定义Drawable
一般而言,上述几种标签的嵌套组合足以应对大多数Drawable的变换了。然而有些情况可能并无法得到满足,比如说我们想要通过标签嵌套的方式将图片变成圆角图片恐怕难以实现,这时就需要自定义Drawable了。自定义Drawable类似于自定义View,然而我们却只需要关注draw,而无需注意measure和layout这两个过程,因此可以说只需要专注于酷炫效果的实现即可。
以上面提到的将图片变成圆角作为背景作为例子,其实现方法也是多种多样,可以用BitmapShader,可以用ClipPath,也可以用XferMode(这几种方法在自定义View中也可以使用,不过就像上面说的,Drawable中不需要关心measure和layout)。栗子网上也有很多,就不重复造轮子了,可以看这里Android Drawable 那些不为人知的高效用法,和这里Bitmap in ImageView with rounded corners。
4 自定义DrawableState
想必大家都很熟悉android:state_pressed
和android:state_selected
这些状态,我们会为这些不同的状态设定不同的Drawable,这样就能带来很好的交互效果,但是如果我们需要自定义的状态又该如何去做呢?比如说Spinner,我想要区分它的展开和关闭这两种状态,并设置不同的背景色,该怎么做好呢?
1.首先我们需要自定义一个状态:
<declare-styleable name="MySpinner">
<attr name="state_expanded" format="boolean" />
</declare-styleable>
2.然后我们需要一个自定义的Spinner:
public class MySpinner extends Spinner {
/**
* Spinner展开时的状态值
*/
public static final int[] STATE_EXPANDED = new int[] {R.attr.state_expanded};
/**
* 当前Spinner是否是展开的状态
*/
private boolean mExpanded;
@Override
protected int[] onCreateDrawableState(int extraSpace) {
if (mExpanded) {
int[] state = super.onCreateDrawableState(extraSpace + 1);
mergeDrawableStates(state, STATE_EXPANDED); //将自定义状态加入到状态列表中
return state;
}
return super.onCreateDrawableState(extraSpace);
}
/**
* 当前Spinner是否是展开状态
* @return true 如果当前Spinner是展开状态
*/
public boolean isExpanded() {
return mExpanded;
}
/**
* 设置Spinner是否是展开的状态
* @param expanded
*/
protected void setExpanded(boolean expanded) {
if (this.mExpanded == expanded)
return;
this.mExpanded = expanded;
refreshDrawableState(); //刷新当前Spinner的状态,调用onCreateDrawableState
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (mExpanded && hasWindowFocus) //Window获得焦点时Spinner折叠
setExpanded(false);
super.onWindowFocusChanged(hasWindowFocus);
}
@Override
public boolean performClick() {
if (!mExpanded) { //受到点击时Spinner展开
setExpanded(true);
}
return super.performClick();
}
}
关键方法只有上面这些,还算浅显易懂吧。
3.接下来为不同的状态设置不同的背景:
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item app:state_expanded="true">
<color android:color="@android:color/holo_orange_light"></color>
</item>
<item app:state_expanded="false">
<color android:color="@android:color/holo_orange_dark"></color>
</item>
</selector>
4.最后只要使用这个自定义的Spinner和selector就可以了,效果如下:
好了,关于Drawable的内容就写到这里。如果文中有什么问题,欢迎留言区里告诉我,3Q~