Android Drawable 詳解

1、Drawable 簡介

Drawable——可簡單理解為可繪制物,表示一些可以繪制在 Canvas 上的對象。在日常的工作開發中,我們為 UI 配置背景、圖片、動畫等等界面效果的時候,需要和眾多的 Drawable 打交道。每種 Drawable 的適用范圍不同,我們有必要了解每種 Drawable 的特點以及使用方式,才能在工作中得心應手,少走彎路。

具體的配置、使用方法以及最終的界面效果大家可以在本文的附件里面看到。Drawable 在 Android 中的繼承關系如下,其中,紅框標注的幾種 Drawable 是我們在開發中比較常用的一些:

常用 Drawable

Drawable 中比較重要的方法有以下幾種:

Drawable
    |- createFromPath
    |- createFromResourceStream
    |- createFromStream
    |- createFromXml
    |
    |- inflate   : 從XML中解析屬性,子類需重寫
    |- setAlpha  : 設置繪制時的透明度
    |- setBounds : 設置Canvas為Drawable提供的繪制區域
    |- setLevel  : 控制Drawable的Level值,這個值在ClipDrawable、RotateDrawable、ScaleDrawable、AnimationDrawable等Drawable中有重要作用;區間為[0, 10000]
    |- draw(Canvas) : 繪制到Canvas上,子類必須重寫

其中,比較重要的方法是inflatedrawinflate 方法用于從 XML 中讀取 Drawable 的配置,draw 方法則實現了把一個 Drawable 確切的繪制到一個 Canvas 上面——draw 方法為一個abstract抽象方法,子類必須進行重寫。inflate 方法在Drawable.createFromXmlInner中被調用:

createFromXmlInner

我們可以看出,在從 XML 中創建一個 Drawable 時,步驟如下:

  1. 先根據 XML 節點名稱來決定創造什么類型的 Drawable;然后 new 出相應的 Drawable;
  2. 再為該 Drawable 調用 inflate 方法,讓其把配置加載起來——因為每種 Drawable 會重寫 inflate 方法,所以,可以正確加載到各項配置及屬性。XML 的配置我們稍后再講。

setAlpha方法用于設置一個 Drawable 的透明度,setBounds用來指定當執行繪制時,在 Canvas 上的位置和區域。比如我們自定義一個 View,在其onDraw中繪制一個BitmapDrawable,我們設置了 BitmapDrawable 的 Alpha 和 Bounds,代碼如下:

Drawable baseDrawable = getResources().getDrawable(R.drawable.base);
baseDrawable.setAlpha(100);
baseDrawable.setBounds(10, 20, 500, 300);
imageContent.setDrawable(baseDrawable);

繪制后的表現如下:

alpha 繪制表現

上圖中,第一個區域是正常繪制的,第二個我們為 Drawable 設置了Alpha和Bounds,可以看出,右邊深藍色的純色部分為整個 Canvas 的大小,設置了 100 的 Alpha 透明度后,圖片把后面深藍色的顏色也給透過來了,并且 Bounds 決定了 Canvas 上繪制該 Drawable 的區域大小和位置。

2、ColorDrawable

接下來我們逐一介紹 Drawable,著重介紹幾種常用的 Drawable。由于在開發中這些 Drawable 大多在 XML 中進行配置,所以我們結合 XML 的配置類介紹。先從ColorDrawable開始,這個應該是最簡單的一種 Drawable 了,它用一個顏色值來表示

color
    |- color="#xxxxxx | @color/color_value"
    |

比如我們的一個 ColorDrawable 的 XML 配置如下,以<color>作為根節點:

<color
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#0000ff"/>

使用的時候和其他 Drawable 的使用方法類似,可以通過Resource.getDrawable來獲取,或者在 XML 里面配置:

<RelativeLayout
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@drawable/blue_drawable"/>

這個 View 的界面表現如你所想,是一坨藍色:

藍色

Java代碼實現:

Resources res = getResources();
ColorDrawable colorDrawable = new ColorDrawable();
colorDrawable.setColor(res.getColor(R.color.skin_black_item));

3、BitmapDrawable

BitmapDrawable<bitmap>作為根節點:

bitmap
    |- src="@drawable/res_id"
    |- antialias="[true | false]"
    |- dither="[true | false]"
    |- filter="[true | false]"
    |- tileMode="[disabled | clamp | repeat | mirror]"
    |- gravity="[top | bottom | left | right | center_vertical |
    |            fill_vertical | center_horizontal | fill_horizontal |
    |            center | fill | clip_vertical | clip_horizontal]"
    |

這個比較復雜一點了,我們逐一介紹各個屬性:

  • src:表示該 BitmapDrawable 引用的位圖,該圖片為 png、jpg 或者 gif;
  • antialias:表示是否開啟抗鋸齒
  • dither:表示當位圖和屏幕的像素配置不同時,是否允許抖動。比如一張位圖的像素為 ARGB_8888 32 位色,而屏幕像素為 RGB_565;
  • filter:是否允許為位圖進行濾波以獲取平滑的縮放效果;
  • gravity:定義位圖的 gravity,當位圖小于容器時,該屬性指定了位圖在容器中的停靠位置繪制方式
  • tileMode:表示當位圖小于容器時,執行“平鋪”模式,并且指定鋪磚的方法。該屬性覆蓋 gravity 屬性——當指定了該屬性后,gravity 屬性即使設置了,也將不起作用

其中,gravitytileMode這兩個屬性比較有意思,我們著重來進行介紹。gravity 的默認值為fill——亦即在水平和垂直方向均進行縮放,使得圖片可以填充到整個 View 里面。

比如我們有一張如下的圖片:

car

為了比較好的展現clamp 鉗位模式,注意這張圖,我們在右邊緣和下邊緣用了黑白交替的邊線。我們的 XML 配置極其簡單,以<bitmap>作為根節點:

<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/car"
    android:tileMode="repeat"/>

當這個 BitmapDrawable 放入一個比它大的容器中時,tileMode 就起作用了:

  1. repeat模式:將重復貼該圖,直到填充完容器:
repeat
  1. clamp模式:鉗位模式,將沿用下邊、右邊邊緣的像素值分水平、垂直兩個方向擴展填充剩余位置:
clamp
  1. mirror模式:鏡像模式,將按水平、垂直鏡像重復來填充剩余位置:
mirror
  1. disabled:禁用任何填充方法,將使用整個位圖進行縮放填充。
disabled

我們接著來看 gravity 屬性,該屬性也比較容易理解:

  1. top:在頂部水平中心繪制;其他類如 left、right、bottom 和 top 類似;
top

當然,我們可以使用“|”來組合,達到特殊的效果,比如當 gravity 為bottom|right時,表現如下:

bottom|right
  1. center_horizontalcenter_vertical將在水平、垂直兩個方向上居中。當單獨使用 top/left/right/bottom 四個值時,默認帶了這兩個中的值:比如 top == top|center_horizontal

  2. fill_horizontalfill_vertical將在水平、垂直兩個方向上進行縮放填充,默認也是帶了center_horizontal或者center_vertical這兩個值的;

下面是“fill_vertical”的表現:

fill_vertical

下面是“fill_vertical|left”的表現:

fill_vertical|left
  1. clip_horizontalclip_vertical將在 Drawable 比容器大時,按水平、垂直方向進行裁剪,下面是 gravity 為“clip_vertical”的情況,可以看出,裁剪了小汽車的首尾:
clip_vertical

在實際的開發中,我們要活用這些 gravity 的值,可以通過“|”來獲取各種想要的效果。

Java代碼實現:

Resources res = getResources();
Bitmap bmp = BitmapFactory.decodeResource(res, R.drawable.adt_48);
BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bmp);
bitmapDrawable.setTileModeX(TileMode.MIRROR);
bitmapDrawable.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);

4、NinePatchDrawable

4.1、.9.png圖片資源

這是一種比較高端的 Drawable。其實就是九宮貼圖——這種 Drawable 契合了 Android 中的“.9.png”文件。這種圖片資源的特點在于:

  1. 在一張普通的 png 圖片四周,分別向外擴展了一個像素;
  2. 用這些擴展的像素,可以描邊,描邊用來規定可縮放區域內容padding區域
4.1.1、.9.png的擴展區域

比如我們現在有一張 .9.png 圖片如下:

pic

我們在四周看到了一像素的黑點,這些黑點分別在四周圍成四個邊線。四個圓角處都是透明的。那么,左、上兩條邊規定了當按鈕被縮放時的可縮放區域。比如下面紅色邊框圈出的矩形內的區域,就是可縮放區域,這個區域外的區域,在執行縮放時均保留原來的像素比例。

patch

比如一個按鈕各個角度拉伸,都可以保留圓角的圓潤,而不會發生鋸齒或者糊掉。我們的布局文件如下:

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    
    <Button
        android:layout_width="100dp"
        android:layout_height="40dp"
        android:background="@drawable/btn_normal"/>

    <Button
        android:layout_width="150dp"
        android:layout_height="80dp"
        android:layout_marginLeft="10dp"
        android:background="@drawable/btn_normal"/>
</LinearLayout>

當 btn_normal 是一個 .9.png 文件時,界面表現如下:

.9

但如果我們的 background 對應的圖片如果是一張非 .9.png 的原圖,那么,界面表現有些糟,明顯看出,圓角部分糊掉了,并且圓角看起來很詭異:

not .9
4.1.2、.9.png 的 padding 區域

這就是 .9.png 的妙處。這種格式圖片的右、下兩個邊緣的像素點,規定了padding區域,也就是說,內容的繪制時的 padding:

padding

上面的紅色邊框圈出的矩形區域規定了內容繪制的區域。比如我們把上面的圖片用作一個 Button 的 background 時,整個按鈕的文本將明顯偏上:

padding result

通過padding區域的控制,我們可以輕松實現一個按鈕按下后文字也相應下移幾個像素的點擊效果。

4.1.3、.9.png 的制作

制作簡單提一下,在 AndroidSDK 的安裝目錄中,tools 文件夾下有一個“draw9patch.bat”文件,啟動該 bat,就相應打開了 .9.png 的制作工具:

draw9patch

我們把一個簡單的png拖入這個窗口,就可以編輯了。用鼠標左鍵在邊緣點擊以點出像素點,用鼠標右鍵刪除像素點。在右邊可以實時預覽繪制效果:

draw9patch UI

4.2、NinePatchDrawable 的 XML 配置

這種 Drawable 的 XML 節點表述如下,以<nine-patch>作為根節點:

nine-patch
    |- src="@drawable/9_png_resid"
    |- dither="[true | false]"
    |

其中,dither 屬性,和之前 BitmapDrawable 中將的一樣,就是像素配置不同時,是否允許抖動。src 比較重要,這個值指向的必須是一個“.9.png”格式的圖片,否則,底層NinePatchDrawable.inflate方法在解析的時候,會拋出一個XmlPullParserException異常:

XmlPullParserException

我們可以看出,上圖中bitmap.getNinePatchChunk這個方法,獲取 9 宮的各項信息,如果從一個 Bitmap 對象中得不到這些信息,則表示這個圖片非“.9.png”格式的圖片,就拋出異常。
其實,“.9.png”的圖片,本質上是一張普通的 png 圖片。比如,我們有一張名為“btn_normal.9.png”的圖片,可以在代碼中這樣使用:

View imageContent = findViewById(R.id.xxx);

Resources res = getResources();
NinePatchDrawable normal = (NinePatchDrawable) res.getDrawable(R.drawable.btn_normal);
imageContent.setBackground(normal);

4.3、NinePatchDrawable 與 .9.png 圖片的映射

那么,Android 是怎樣把這張圖片映射為一個 NinePatchDrawable 的呢?原來,這張圖片開始被當作普通的 Bitmap,從 Resources.getDrawable 方法中可以看出端倪:

getDrawable

在 getDrawable 中調用了loadDrawable,在 loadDrawable 方法中有一個緩存策略,我們先不管,直接看加載資源的部分:

loadDrawable

可以看出,對 XML 配置類型的 Drawable,使用loadXmlResourceParse加載,然后使用Drawable.createFromXml這個靜態方法進行創建,得到 Drawable 對象。對于其他類型的 Drawable,先使用openNonAsset得到一個流對象,然后使用Drawable.createFromResourceStream這個靜態方法進行創建。Drawable.createFromXml 這個方法最終會調用Drawable.createFromXmlInner,這個方法我們前面 Drawable 簡介里面已經介紹過了。我們著重看 Drawable.createFromResourceStream 這個方法:

getNinePatchChunk

在這個方法中,我們先從流中解析得到一個 Bitmap 對象——這個對象本質上和其他所有類型的圖片資源沒任何區別。區別在于接下來調用的Bitmap.getNinePatchChunkNinePatch.isNinePatchChunk這兩個方法,通過這兩個方法的結合調用,可以判斷這個 Bitmap 是否是一個合格的“.9.png”圖片。接下來進入drawableFromBitmap

drawableFromBitmap

最后,根據九宮信息 np 這個參數是否為 null,來決定創建什么對象。可以看出,對“.9.png”格式的圖片,最終會創建一個 NinePatchDrawable 對象,對于其他普通的 png、jpg 等圖片,創建相應的 BitmapDrawable 對象。一切一目了然。

Java代碼實現:

這部分我們使用draw9patch工具很容易制作,一般不會在代碼中進行創建NinePatchDrawable對象,也不推薦在代碼中這樣做。

5、StateListDrawable

這個 Drawable 類型幾乎是我們開發中最常用的類型了,為什么呢?因為它是根據一系列的狀態來控制繪制表現的,這一系列狀態契合了我們界面控件的各個狀態。界面控件的狀態一般有:獲取焦點、失去焦點、普通狀態、按下狀態、可點擊狀態、不可點擊狀態、選中狀態、未選中狀態、勾選狀態、未被勾選狀態、激活狀態、未被激活狀態等等。

StateListDrawable<selector>作為根節點:

selector
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- state_pressed="[true | false]"
    |    |- state_focused="[true | false]"
    |    |- state_selected="[true | false]"
    |    |- state_hovered="[true | false]"
    |    |- state_checked="[true | false]"
    |    |- state_checkable="[true | false]"
    |    |- state_enabled="[true | false]"
    |    |- state_activated="[true | false]"
    |    |- state_window_focused="[true | false]"
    |

一個selector以多個item來組成,每個 item 由 0 個或者多個狀態和一個 drawable 來表示,當控件的狀態變化后,將根據控件當前的狀態,來進行匹配,匹配一個最適合當前狀態的 item,然后用這個 item 的 drawable 來進行繪制。
比如,我們一個普通按鈕的 selector 如下:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:drawable="@drawable/pressed_btn" />
    
    <item android:drawable="@drawable/normal_btn" />
</selector>

我們定義了一個按鈕的普通狀態和按下狀態的 Drawable,使用方法如下:

<Button
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:textColor="#e22"
    android:background="@drawable/flat_button_drawable"
    android:text="Flat Button" />

那么,在普通狀態和按下狀態中,界面表現分別如下:

普通狀態????

normal

按下狀態????

pressed

Cool!除了按下狀態的紅色有點刺眼外,看起來還不錯,是吧。其實,我們可以通過控件狀態,來控制普通態、按下態的按鈕文字顏色。我們新建一個 XML,放入res/color文件夾下,比如起名為 btn_text_color.xml:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:color="#fff"/>
    <item android:color="#e22"/>
</selector>

我們在Button中的配置如下,通過設置android:textColor來控制按鈕的文本顏色:

<Button
    android:layout_width="200dp"
    android:layout_height="60dp"
    android:textColor="@color/btn_text_color"
    android:background="@drawable/flat_button_drawable"
    android:text="Flat Button" />

現在,一個高大上的扁平化的按鈕效果出爐了:

普通狀態????

normal

按下狀態????

pressed

在實際操作中,我們可能要為多種狀態來進行設置,可以靈活運用:

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:state_pressed="true"
        android:state_pressed="true"
        android:state_selected="false"
        android:drawable="@drawable/pressed_btn"/>

    <item android:drawable="@drawable/normal_btn"/>
</selector>

這樣,第一個item將只匹配未被禁用且當前為按下狀態且未被選中狀態。其他狀態均使用第二個item。

當然,不踩幾個坑,怎么能做一名合格的開發者呢?

注意:

如果有不帶任何狀態的 item 的話,這個item一定要放在整個 item 列表的最下面。否則,所有的狀態均可優先匹配到這個 item,其他 item 將得不到匹配。因為匹配的時候是一個遍歷操作,如果遍歷找到和當前狀態符合的 Drawable,就直接返回。

Java代碼實現:

Resources res = getResources();
StateListDrawable stateListDrawable = new StateListDrawable();
stateListDrawable.addState(
    new int[] {android.R.attr.state_pressed},
    res.getDrawable(R.drawable.blue_drawable));

stateListDrawable.addState(
    new int[] {
        android.R.attr.state_pressed,
        android.R.attr.state_enabled},
    res.getDrawable(R.drawable.bmp_drawable));

stateListDrawable.addState(
    new int[] {},
    res.getDrawable(R.drawable.bkgnd_normal));

6、ClipDrawable

ClipDrawable允許我們對一個 Drawable 進行剪裁操作,在繪制的時候只繪制剪裁的部分。這里最關鍵的是Drawable.setLevel方法在起作用,在為一些控件比如進度條、音量控制條等設置 UI 效果的時候,一般會使用 ClipDrawable,否則,你的進度在界面上將得不到刷新。

ClipDrawable以<clip>作為根節點:

  clip
    |- drawable="@drawable/drawable_id"
    |- clipOrientation="[horizontal | vertical]"
    |- gravity="[ ... ]"
    |

clipOrientation決定了裁剪的方向,默認為horizontal——表示水平方向剪裁;而 gravity 的取值和之前介紹的類似,結合 clipOrientation 決定了剪裁發生的位置——默認為left,就是當 clipOrientation 為 horizontal 時,剪裁發生在 drawable 的右側。

最主要的繪制我們來看ClipDrawable.draw方法:

ClipDrawable.draw

根據 AndroidSDK 的規范,setLevel的 level 值在[0, 10000]這個區間內。可以看出,在繪制的時候,根據 level 值和 gravity 算出要剪裁的區域,然后在 Canvas 上執行 clipRect,從而達到剪裁效果。

XML的配置也很簡單:

<clip
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/normal_btn"
    android:clipOrientation="vertical"
    android:gravity="top" />

Java代碼實現:

Resources res = getResources();
NinePatchDrawable btnNormal = (NinePatchDrawable) res.getDrawable(R.drawable.btn_normal);
ClipDrawable clipDrawable = new ClipDrawable(
    btnNormal, Gravity.TOP, ClipDrawable.VERTICAL);
clipDrawable.setLevel(500);

我們后續結合LayerDrawable來看ClipDrawable在進度條等 UI 上的配置方式。

7、LayerDrawable

LayerDrawable可以將一組 Drawable 按 XML 中定義的順序層疊起來進行繪制,并可以設定每層 Drawable 的 id、位置等等。ProgressBar這個控件的背景切圖,可以通過 LayerDrawable 來進行配置。LayerDrawable 以<layer-list>作為根節點:

layer-list
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- id="@+id/xxx_id"
    |    |- top="dimension"
    |    |- left="dimension"
    |    |- right="dimension"
    |    |- bottom="dimension"
    |

每組 Drawable 由<item>節點進行配置,item 中 drawable 表示了這層 Drawale 引用的繪圖資源 ID,id屬性表示了這層 Drawable 的ID,top、left、right、bottom這四個屬性發布表示與各個方向的間距。比如一個簡單的 LayerDrawable 如下:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:drawable="@drawable/red_color"
        android:bottom="10dp"
        android:left="10dp"
        android:right="10dp"
        android:top="10dp"/>
    <item
        android:drawable="@drawable/green_color"
        android:bottom="20dp"
        android:left="20dp"
        android:right="20dp"
        android:top="20dp"/>
    <item
        android:drawable="@drawable/blue_color"
        android:bottom="30dp"
        android:left="30dp"
        android:right="30dp"
        android:top="30dp"/>
</layer-list>

那么,繪制出來的效果如下(其中,灰色那一層是Activity的背景):

繪制結果

接下來我們看看LayerDrawable在ProgressBar中的配置:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@android:id/background"
        android:drawable="@drawable/file_background"/>
    
    <item android:id="@android:id/progress">
        <clip android:drawable="@drawable/file_progress"/>
    </item>
    
    <item android:id="@android:id/secondaryProgress">
        <clip android:drawable="@drawable/file_cache_progress"/>
    </item>
</layer-list>

可以看出,我們在配置的時候,分別為每個 item 指定了 id——這些 id 對應表示了 ProgressBar 中每種進度狀態:background對應整個 ProgressBar 的背景,progress對應當前的進度背景,而secondaryProgress對應 secondaryProgress 的進度背景(一般我們用來做緩沖進度——和優酷視頻的緩沖類似)。另外我們看出,結合使用了 ClipDrawable——因為 ProgressBar 的實現中,正是結合Drawable.setLevel來進行刷新進度的,在前面講過,ClipDrawable 恰好在onDraw繪制中,對 Level 做了相應的處理:

setLevel

這里有一個方法:LayerDrawable.findDrawableByLayerId,這個方法可以獲取 id 對應的 Drawable。

Java代碼實現:

Resources res = getResources();
LayerDrawable layerDrawable = new LayerDrawable(
        new Drawable[] {
            res.getDrawable(R.drawable.red_color),
            res.getDrawable(R.drawable.green_color),
            res.getDrawable(R.drawable.blue_color)
        });

layerDrawable.setId(0, R.id.action_settings);
layerDrawable.setId(1, R.id.switchBtn);
layerDrawable.setLayerInset(0, 10, 10, 10, 10);
layerDrawable.setLayerInset(1, 20, 20, 20, 20);

8、AnimationDrawable

8.1、AnimationDrawable 的使用

借助AnimationDrawable,我們可以輕松實現基于一系列 Drawable 幀的動畫效果。AnimationDrawable 提供了一系列簡單易用的接口來幫助我們:

AnimationDrawable
   |- setOneShot : 設置動畫是否單次播放,默認為false,表示不循環
   |- start : 開始播放動畫,如果已經在播放中,則不起作用
   |- end : 結束播放
   |

一般我們在 XML 里面進行配置動畫,代碼中手工寫的方式不推薦。AnimationDrawable 以<animation-list>作為根節點:

animation-list
    |- oneshot="[true | false]"
    |- visible="[true | false]"
    |- item
    |    |- drawable="@drawable/drawable_id"
    |    |- duration="xms"
    |

animation-list 節點內的oneshot屬性表示該動畫是否只播放一次,當這個值為 false 的時候,表示循環播放——這是默認值。其他的一系列動畫效果,均由一組<item>節點來進行配置,item 中的duration表示這一幀和上一幀的時間間距,以 ms 為單位。比如我們有一個簡單的動畫配置如下:

<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">
    
    <item
        android:drawable="@drawable/red_color"
        android:duration="500"/>
    
    <item
        android:drawable="@drawable/green_color"
        android:duration="500"/>
    
    <item
        android:drawable="@drawable/blue_color"
        android:duration="500"/>
</animation-list>

我們可以將該動畫效果施加到一個 View 上:

View imageContent = findViewById(R.id.xxx);

AnimationDrawable drawable = (AnimationDrawable) res.getDrawable(R.drawable.animation_drawable);
imageContent.setBackground(drawable);
drawable.start();

這樣,我們在這個 View 上可以看到每隔 500ms 便變換一次顏色的動畫效果。當然,這只是一個 demo,利用 AnimationDrawable,我們可以做出更酷的動畫。

我們一般在 XML 里面配置 AnimationDrawable,通過 Resources.getDrawable 方法來獲取它。雖然我們不推薦在代碼里面手工創建 AnimationDrawable,但萬一哪天你需要它呢?

Java代碼實現:

Resources res = getResources();
AnimationDrawable animationDrawable = new AnimationDrawable();
animationDrawable.addFrame(res.getDrawable(R.drawable.red_color), 500);
animationDrawable.addFrame(res.getDrawable(R.drawable.green_color), 500);
animationDrawable.addFrame(res.getDrawable(R.drawable.blue_color), 500);

animationDrawable.setOneShot(false);
imageContent.setBackground(animationDrawable);
animationDrawable.start();

8.2、AnimationDrawable 的原理

我們只是把一個 AnimationDrawable 塞入了一個 View 的 background 中,那么這些動畫的變換,是怎么響應到 View 上的呢?原來,這一切都是Drawable.Callback這個回調在起作用:

Callback

我們通過 Drawable.setCallback 來設置一個Callback,這個 Callback 中有三個方法:

  • invalidateDrawable:重繪 Drawable;
  • scheduleDrawable:在 when 規定的 ms 后,執行 what 這個Runnable;(這里可以看出動畫的端倪了)
  • unscheduleDrawable:異步執行這個 what;用來結束動畫等。

View 類實現了 Drawable.Callback 這個接口,在我們調用View.setBackground方法為 View 設置背景的時候,會把 View 的 this 塞入 Drawable 中作為 Callback:

Drawable.Callback
setCallback

而在 AnimationDrawable 自己實現了Runnable這個接口,在run方法中,通過調用AnimationDrawable.nextFrame方法,提供了動畫幀的切換終止判斷等操作。

setFrame
scheduleSelf

在這里首先使用selectDrawable把對應幀的 Drawable 選為激活的,然后在scheduleSelf中,通過調用Drawable.Callback.scheduleDrawable這個 Callback 方法,可以達到動畫幀按時間間隔切換的效果。

9、其他 Drawable 及總結

基本上我們在工作中最常用的幾類 Drawable 如上所示。其他的一些 Drawable 有時也會用到,也很有趣。比如ShapeDrawableRotateDrawableScaleDrawable以及InsetDrawable。這些 Drawable 可以在工作中確實需要用到的時候去參考 SDK 進行學習和靈活運用,在這里簡單介紹下這幾種 Drawable 的作用和使用方法,以及一些效果截圖。

9.1、ShapeDrawable

通過在 XML 中配置 ShapeDrawable,我們可以輕松繪制矩形、線段、圓角矩形、漸變等圖形作為 background 而不需要切圖。ShapeDrawable 以<shape>作為根節點;需要熟悉子節點的有:cornersgradientpaddingsizesolidstroke等;比如下面是一個簡單的配置及效果展現:

<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <corners android:radius="5dp"/>

    <gradient
        android:angle="90"
        android:endColor="#ddd"
        android:startColor="#343434"
        android:type="linear"/>
    
    <stroke
        android:width="2dp"
        android:color="#00f"/>
</shape>

我們通過android:shape指定這個 shape 是一個矩形(rectangle),用子節點corners為矩形加上圓角,使之變成一個圓角矩形;再使用gradient子節點來施加一個漸變效果,漸變的類型用android:type指定為線性漸變(linear);最后再使用stroke子節點為整個圖形加上一個 2dp 寬的藍色外邊框。其效果圖如下:

shape
9.2、RotateDrawable

RotateDrawable可以結合當前 Drawable 的 level 值,進行旋轉。level 值每增加一,其旋轉角度旋轉(toDegrees – fromDegrees) / 10000。比如下圖是一張正常的圖片:

robot

我們通過一個 XML 進行旋轉,其中android:fromDegreesandroid:toDegrees確定了旋轉的起始角度和終止角度,android:pivotXandroid:pivotY確定了旋轉中心點的位置:

<rotate
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/android_robot"
    android:fromDegrees="0"
    android:toDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%"/>

我們在 UI 線程中控制這個 RotateDrawable 的 Level 值,可以獲得一個旋轉的動畫效果:

RotateDrawable 例子

我們截取了部分動畫效果的過程,如下:

RotateDrawable 示例運行截圖
9.3、ScaleDrawable

ScaleDrawable可以結合當前 Drawable 的 level 值,進行圖片的縮放,同樣結合HandlerTimer,我們可以得到一個簡單的縮放動畫。

9.4、InsetDrawable

InsetDrawable可以把一個 drawable 資源嵌入到其他的資源內部,并且在四周可以留下邊距。比如我們有時候需要一個左右各留白 15dp 的ListView的分隔線,我們可以用 InsetDrawable 來做。為什么不使用切圖的方式來留白呢——注意,我們這里要求是 15dp,而不是 15pixel,如果切圖的話,只能用像素單位留白,但這導致在不同的設備上可能用戶看到的留白的間距不統一

<inset
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/seperator_line"
    android:insetLeft="15dp"
    android:insetRight="15dp"/>

我們應用到一個 ListView 的分隔符上:

<ListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:divider="@drawable/inset_drawable"
    android:dividerHeight="1dp"/>

這樣,我們得到了一個首尾均留白 15dp 的分隔符,整個界面效果展現如下(灰色背景部分是整個 ListView 的輪廓):

ListView 結果

當然,還有其他諸如TransitionDrawableLevelListDrawableGradientDrawablePictureDrawablePaintDrawable沒有詳細介紹,但這幾種一般不是很常用。經過前面一些 Drawable 的簡介,即時我們在工作中需要用到這幾類 Drawable,也可以輕松通過查看文檔等方式來學習和使用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容