Drawable在android當中非常常見,每天都會和它打交道,但很多人卻很少知道Drawable有哪些常用的方法,甚至有時候在面試的時候問起也是一臉茫然。究其原因可能是Drawable真的是封裝得太好了,以至于基本不用關心它的實現。
Drawable顧名思義,一個可以Draw的東西,這是一種高度抽象的對象,至于里面怎么Draw完全任意實現。Android上面已經封裝好了很多Drawable,比如ColorDrawable,ShapeDrawable,BitmapDrawable等等,真的不用關心里面實現就能完成大部分開發。
但往往這種定性思維是可怕的,當你不了解里面的實現你就可能寫出很丑陋的代碼,比如當你拿到一個Bitmap,想要先縮放在設到一個ImageView的時候,你可能會這樣寫:
新手上來可能就對圖片進行縮放,這里演示代碼還好,有時候我們要做一些圖像合成,比如在圖片上面加個角標,那在內存里面(特別很可能是在UI線程)先合成一個Bitmap,再設給ImageView,這個合成過程完全是徒勞的,既費時間又費空間。這個例子體現出來對Drawable的理解不夠深刻,刻板機械的使用導致寫出來的代碼效率不高。
所以請牢記Drawable可以實現繪制任何東西,畢竟重寫的draw方法都給你canvas了,還有什么不能畫。在平時的開發過程當中要把自己從Bitmap的思維方式轉向到Drawable上面來。我們再來看個例子:
這個效果是一張圖標上面左邊有個角標,右邊有個圖標加文字,同時頂部有一段漸變陰影。如果不加思索你可能用一個RelativeLayout,頂上先放一個ImageView來設陰影(圖上不明顯),左上角再放一個ImageView,右上角放一個TextView,使用DrawableLeft設一個圖標。
一樣是完成需求,你可以把這所有的這么多View都轉換成一個自定義Drawable(參考上面示例代碼),這樣一來View的數量減少了,View的層級也減少了,我們知道View的層級和數量直接影響measure,layout時間,加上view的infalte時間是拖慢頁面的重要因素,因此頁面響應速度就會加快。這是一種減少過度繪制,提高頁面幀率的非常有效而且簡單的方式。
這個時候有同學可能就會問了,那是不是遇到這種問題我們都不用View了,把這些View都去掉換成一個Drawable呢?這是一個粒度的問題。事實上系統常見的View比如TextView里面的繪制特別復雜,但你用它它就幫你實現好了,相反你要自己來draw則要考慮各種問題。因此到底優化到什么程度是根據實際情況來決定的。一般而言圖片上面疊加圖片的推薦使用換成一個Drawable來自己畫,比如加陰影,角標等。如果遇到文字很多,文字的部分建議還是用TextView來實現。
還有種情況在ListView里面有時候某一行樣式稍微有些區別,正常情況我們可能需要增加Type來實現,現在也可以嘗試將這個微小差別轉換成Drawable,這樣很有可能簡化很多開發工作,把差異都封裝在這個自定義drawable里面了。
Drawable系統有很多子類可以簡化很多開發。
LayerDrawable 含多個drawable的drawable,他可以實現不用自定義drawable就可以依次畫出多個drawable,比如這個例子在原始圖片下面墊一個紅色背景。fresco就是用這個來實現默認占位圖片,圖片加載出來動畫的。
InsetDrawable 有時候我們希望點擊區域不只是圖片本身的大小,那這個Drawable就派上用場了,他可以設置外面一圈空白區域大小來調整Drawable實際大小,讓點擊更輕松。
DrawableWrapper 是個非常有用的抽象類,借助他可以非常方便的對已有的drawable進行包裝,做一些其他事情,比如還是給圖片畫個角標,可以這樣寫:
VectorDrawable 開發者的福音,再也不用小圖標也切好幾套了
ClipDrawable 可以做出圖像被裁剪的效果,結合定時器可以做出動畫展開的感覺
更多Drawable系統實現請參考官方文檔
接下來重點來看看Drawable里面有哪些常見的方法
draw 這個方法就不繼續說了,參數為canvas,可以用來繪制任何想要的東西
setBounds 給drawable設置區域,調用draw方法的時候,就會繪制在指定區域,在draw之前一定要調用這個方法來設置位置。
setFilterBitmap 這個子類不是必須實現的,在BitmapDrawable里面對bitmap縮放旋轉時進行雙線性采樣,看起來更平滑。
setCallback
這個函數非常重要,以ImageView為例,給它設置了Drawable之后,就會調用drawable的setCallback方法,將View自己傳進來,保存到drawable的mCallback里面。在調用下面會說到的invalidateSelf函數時,就會調用mCallback的invalidateDrawable方法,這個mCallback也就是view自己,最終會調用view的invalidate進行重繪。這樣drawable的請求重繪就和view關聯起來。
invalidateSelf 上面講過了
scheduleSelf 其實是postDelay實現下一次執行自己,用于動畫
setAlpha 設置透明度,這個在有時候實現按鈕點擊上很有幫助,比如不可點是5%透明度,正常是70%透明度,按下是100%透明度,就可以這樣實現selector:
getConstantState
上面例子有使用這個函數,每個drawable里面都有一個ConstantState的東西,他保存了這個Drawable的各種屬性,比如ColorState,BitmapState等。ConstantState有個非常重要的函數叫newDrawable,他可以從現有drawable對象很方便生成一個新對象,注意只是對象是另外一個新的,ConstantState則是直接引用的,也就是說兩個drawable對象里面引用了同一個ConstantState。
mutate
這個方法要是不了解就談不上進階drawable。剛才講到newDrawable之后ConstantState是同一個,但這樣會有問題,兩個人用同一個會導致有些屬性無法單獨修改,這個時候就需要用到mutate方法,他的作用就是在復制一份ConstantState,讓newDrawable出來的對象也擁有自己的ConstantState,這樣他們之間就不會互相干擾。這里需要注意的時候對于BitmapDrawable雖然mutate了,ConstantState也是各自有一個,但ConstantState里面的bitmap還是只有一份,這個應該是沒有任何異議的(否則太傻了),mutate可以理解為深拷貝。這里引用著名的圖來說明mutate到底解決什么問題:
setTintList/setTintMode
ConstantState里面哪些屬性可能需要單獨修改而使用mutate方法呢?一個非常重要的就是TintList和TintMode,翻譯成叫著色,也就是我們可以在原來Drawable的基礎上面在畫一層顏色,兩層顏色疊加起來。如果先不進行mutate,ConstantState是同一個,修改了其中一個drawable的tint,另外一個也會跟著被修改。兩層顏色疊加方式則有很多種。通俗來講就是達到各種效果,比如給圖標換個顏色:
這個API很好用,以往我們做個按下效果可能需要切2張圖,現在結合上面講的mutate等方法,很方便的就能給圖標換個顏色來實現按下效果(注意這個方法高版本才有,低版本上面可以借用support包來實現類似效果)。比如:
setColorFilter setTintList/setTintMode是對setColorFilter的封裝,最終起作用的其實是后者。
onStateChange 我們的selector(StateListDrawable)在狀態變化的時候就會回調到drawable的這個方法來實現切換
isStatefu 是否穩定,是一種優化處理,如果返回false表示這個drawable不跟隨狀態變化而改變,則可以減少一些狀態計數和更新
setLevel 這個函數很有意思,在一些有過程的Drawable實現里面可以定位到具體某一幀,比如RotateDrawable:
Drawable其實是Android系統里面實現很不錯的一個模塊,而且代碼又不難懂,深入學習Drawable還是可以發現很多值得細細品味的東西,希望大家用好Drawable~