詳解Android Drawable
1 Drawable概述
Drawable是一種影象的概念,但又不全是圖片,也可能是各種顏色組合而成的影象。通常將Drawable作為View的背景,而這些Drawable我們一般通過XML來定義,當然也可以通過程式碼來實現,但是並沒有XML來得方便。Drawable是一個抽象類,其子類有我們熟悉的BitmapDrawable等。
Drawable內部有固有寬高的概念,通過getIntrinsicWidth
和getIntrinsicHeight
來獲得,然而並不是所有的drawable都有這兩個值,BitmapDrawable這兩個值的大小是圖片的寬高,而單純以顏色形成的Drawable的這兩個值一般都是-1(不過是可以修改的
那麼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~