1. 程式人生 > >Android drawable微技巧,你所不知道的drawable的那些細節

Android drawable微技巧,你所不知道的drawable的那些細節

好像有挺久時間沒更新部落格了,最近我為了準備下一個系列的部落格,
也是花了很長的時間研讀原始碼。很遺憾的是,下一個系列的部落格我可能還要再過一段時間才能寫出來,那麼為了不至於讓大家等太久,今天就給大家更新一篇單篇的文章,講一講android drawable方面的微技巧。

話說微技巧這個詞也是我自己發明的,因為drawable這個東西相信大家天天都在使用,每個人都再熟悉不過了,之所以叫微技巧就是對於這個我們再熟悉不過的技術,可能還有一些你所不知道的細節,那今天我們就來一起探究一下這些微小的細節吧。

大家都知道,在Android專案當中,drawable資料夾都是用來放置圖片資源的,不管是jpg、png、還是9.png,都可以放在這裡。除此之外,還有像selector這樣的xml檔案也是可以放在drawable資料夾下面的。
但是如果你現在使用Android Studio來新建一個專案,你會發現有如下的目錄結構:


嗯?怎麼會有這麼多mipmap開頭的資料夾,而且它們的命名規則和drawable資料夾很相似,也是hdpi、mdpi、xhdpi等等,並且裡面還真是放的圖片,難道Android專案中放置圖片的位置已經改了?

對於剛剛從Eclipse轉向Android Studio的開發者們可能會對mipmap資料夾感到陌生,其實不用擔心,我們平時的程式設計習慣並不需要發生任何改變,因為mipmap資料夾只是用來放置應用程式的icon的,僅此而已。那麼在此之前,我們都是把應用程式的icon圖示和普通的圖片資源一起放到drawable資料夾下的,這樣看上去就會比較雜亂,有的時候想從一堆的圖片資源裡面找icon半天也找不到,而檔案一多也就容易出現漏放的情況,但恰恰Android是極度建議我們在每一種解析度的資料夾下面都放一個相應尺寸的icon的,因此將它們獨立出來專門放到mimap資料夾當中就很好地解決了這個問題。

另外,將icon放置在mipmap資料夾還可以讓我們程式的launcher圖示自動擁有跨裝置密度展示的能力,比如說一臺螢幕密度是xxhdpi的裝置可以自動載入mipmap-xxxhdpi下的icon來作為應用程式的launcher圖示,這樣圖示看上去就會更加細膩。

除此之外,對於每種密度下的icon應該設計成什麼尺寸其實Android也是給出了最佳建議,icon的尺寸最好不要隨意設計,因為過低的解析度會造成圖示模糊,而過高的解析度只會徒增APK大小。建議尺寸如下表所示:

密度 建議尺寸
mipmap-mdpi 48 * 48
mipmap-hdpi 72 * 72
mipmap-xhdpi 96 * 96
mipmap-xxhdpi 144 * 144
mipmap-xxxhdpi 192 * 192

然後我們引用mipmap的方式和之前引用drawable的方式是完全一致的,在資源中就使用@mipmap/res_id,在程式碼就使用R.mipmap.res_id。比如AndroidManifest.xml中就是這樣引用ic_launcher圖示的:

Java
12345678910111213 <applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity></application>

好的,關於mimap的內容就講這麼多,它並不是本篇文章的重點,接下來我們來真真正正看一些drawable的微技巧。

首先我準備了一張270*480畫素的圖片:


將圖片命名為android_logo.png,然後把它放在drawable-xxhdpi資料夾下面。為什麼要放在這個資料夾下呢?是因為我的手機螢幕的密度就是xxhdpi的。那麼怎麼才能知道自己手機螢幕的密度呢?你可以使用如下方法先獲取到螢幕的dpi值:

Java
12 floatxdpi=getResources().getDisplayMetrics().xdpi;floatydpi=getResources().getDisplayMetrics().ydpi;

其中xdpi代表螢幕寬度的dpi值,ydpi代表螢幕高度的dpi值,通常這兩個值都是近乎相等或者極其接近的,在我的手機上這兩個值都約等於403。那麼403又代表著什麼意思呢?我們直接參考下面這個表格就知道了:

dpi範圍 密度
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

從表中可以看出,403dpi是處於320dpi到480dpi之間的,因此屬於xxhdpi的範圍。
圖片放好了之後,下面我在佈局檔案中引用這張圖片,如下所示:

Java
123456789101112131415 <?xml version="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/image"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/android_logo"/></LinearLayout>

在ImageView控制元件中指定載入android_logo這張圖,並把ImageView控制元件的寬高都設定成wrap_content,這樣圖片有多大,我們的控制元件就會有多大。
現在執行一下程式,效果如下所示:


由於我的手機解析度是1080*1920畫素的,而這張圖片的解析度是270*480畫素的,剛好是手機解析度的四分之一,因此從上圖中也可以看出,android_logo圖片的寬和高大概都佔據了螢幕寬高的四分之一左右,大小基本是比較精準的。
到目前為止一切都挺順利的,不是嗎?下面我們嘗試做點改變,將android_logo.png這張圖移動到drawable-xhdpi資料夾下,注意不是複製一份到drawable-xhdpi資料夾下,而是將圖片移動到drawable-xhdpi資料夾下,然後重新執行一下程式,效果如下圖所示:


嗯?怎麼感覺圖片好像變大了一點,是錯覺嗎?
那麼我們再將這張圖移動到drawable-mdpi資料夾下試試,重新執行程式,效果如下圖所示:


這次肯定不是錯覺了,這實在是太明顯了,圖片被放大了!
那麼為什麼好端端的一張圖片會被自動放大呢?而且這放大的比例是不是有點太過份了。其實不然,Android所做的這些縮放操作都是有它嚴格的規定和演算法的。可能有不少做了很多年Android的朋友都沒去留意過這些縮放的規則,因為這些細節太微小了,那麼本篇的微技巧探索裡面,我們就來把這些細節理理清楚。

首先解釋一下圖片為什麼會被放大,當我們使用資源id來去引用一張圖片時,Android會使用一些規則來去幫我們匹配最適合的圖片。什麼叫最適合的圖片?比如我的手機螢幕密度是xxhdpi,那麼drawable-xxhdpi資料夾下的圖片就是最適合的圖片。因此,當我引用android_logo這張圖時,如果drawable-xxhdpi資料夾下有這張圖就會優先被使用,在這種情況下,圖片是不會被縮放的。但是,如果drawable-xxhdpi資料夾下沒有這張圖時, 系統就會自動去其它資料夾下找這張圖了,優先會去更高密度的資料夾下找這張圖片,我們當前的場景就是drawable-xxxhdpi資料夾,然後發現這裡也沒有android_logo這張圖,接下來會嘗試再找更高密度的資料夾,發現沒有更高密度的了,這個時候會去drawable-nodpi資料夾找這張圖,發現也沒有,那麼就會去更低密度的資料夾下面找,依次是drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

總體匹配規則就是這樣,那麼比如說現在終於在drawable-mdpi資料夾下面找到android_logo這張圖了,但是系統會認為你這張圖是專門為低密度的裝置所設計的,如果直接將這張圖在當前的高密度裝置上使用就有可能會出現畫素過低的情況,於是系統自動幫我們做了這樣一個放大操作。

那麼同樣的道理,如果系統是在drawable-xxxhdpi資料夾下面找到這張圖的話,它會認為這張圖是為更高密度的裝置所設計的,如果直接將這張圖在當前裝置上使用就有可能會出現畫素過高的情況,於是會自動幫我們做一個縮小的操作。所以,我們可以嘗試將android_logo這張圖移動到drawable-xxxhdpi資料夾下面將會得到這樣的結果:


可以看到,現在圖片的寬和高都達到不手機螢幕的四分之一,說明圖片確實是被縮小了。
另外,剛才在介紹規則的時候提到了一個drawable-nodpi資料夾,這個資料夾是一個密度無關的資料夾,放在這裡的圖片系統就不會對它進行自動縮放,原圖片是多大就會實際展示多大。但是要注意一個載入的順序,drawable-nodpi資料夾是在匹配密度資料夾和更高密度資料夾都找不到的情況下才會去這裡查詢圖片的,因此放在drawable-nodpi資料夾裡的圖片通常情況下不建議再放到別的資料夾裡面。

圖片被放大的原因現在我們已經搞清楚了,那麼接下來還有一個問題,就是放大的倍數是怎麼確定的呢?很遺憾,我沒有找到相關的文件記載,但是我自己總結出了一個規律,這裡跟大家分享一下。

還是看一下剛才的 dpi範圍-密度 表格:

dpi範圍 密度
0dpi ~ 120dpi ldpi
120dpi ~ 160dpi mdpi
160dpi ~ 240dpi hdpi
240dpi ~ 320dpi xhdpi
320dpi ~ 480dpi xxhdpi
480dpi ~ 640dpi xxxhdpi

可以看到,每一種密度的dpi範圍都有一個最大值,這個最大值之間的比例就是圖片會被系統自動放大的比例。
口說無憑,下面我們來通過例項驗證一下,修改佈局檔案中的程式碼,如下所示:

Java
123456789101112131415161718192021 <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ImageViewandroid:id="@+id/image"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/android_logo"/><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:text="獲取圖片寬高"android:onClick="buttonClick"/></LinearLayout>

可以看到,我們添加了一個按鈕,並給按鈕註冊了一個點選事件。然後在MainActivity中處理這個點選事件:

Java
1234567891011121314151617 publicclassMainActivityextendsAppCompatActivity{ImageView imageView;@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);imageView=(ImageView)findViewById(R.id.image);}publicvoidbuttonClick(View view){Toast.makeText(this,"圖片寬度:"+imageView.getWidth(),Toast.LENGTH_SHORT).show();Toast.makeText(this,"圖片高度:"+imageView.getHeight(),Toast.LENGTH_SHORT).show();}}

這裡在點選事件中分別獲取圖片的寬和高並使用Toast提示出來。程式碼修改這麼多就可以了,然後將圖片移動到drawable-mdpi資料夾下。
下面我們來開始分析,mdpi密度的最高dpi值是160,而xxhdpi密度的最高dpi值是480,因此是一個3倍的關係,那麼我們就可以猜測,放到drawable-mdpi資料夾下的圖片在xxhdpi密度的裝置上顯示會被放大3倍。對應到android_logo這張圖,原始畫素是270*480,放大3倍之後就應該是810*1440畫素。下面執行程式,效果如下圖所示:


驗證通過。我們再來試驗一次,將圖片移動到drawable-xxxhdpi目錄下。xxxhdpi密度的最高dpi值是640,480是它的0.75倍,那麼我們就可以猜測,放到drawable-xxxdpi資料夾下的圖片在xxhdpi密度的裝置上顯示會被縮小至0.75倍。270*480的0.75倍應該是202.5*360,由於畫素不支援小數點,那麼四捨五入就應該是203*360畫素。重新執行程式,效果如下圖所示:


再次驗證通過。如果你有興趣的話可以使用其它幾種dpi的drawable資料夾來試一試,應該都是適配這套縮放規則的。這樣我們就把圖片為什麼會被縮放,以及具體的縮放倍數都搞明白了,drawable相關的細節你已經探究的非常細微了。

不過本篇文章到這裡還沒結束,下面我準備講一講我們在實際開發當中會遇到的場景。根據Android的開發建議,我們在準備圖片資源時儘量應該給每種密度的裝置都準備一套,這樣程式的適配性就可以達到最好。但實際情況是,公司的UI們通常就只會給一套圖片資源,想讓他們針對每種密度的裝置都設計一套圖片資源,並且還是按照我們上面講的縮放比例規則來設計,就有點想得太開心了。沒錯,這個就是現實情況,那麼在這種情況下,我們應該將僅有的這一套圖片資源放在哪個密度的資料夾下呢?

可以這樣來分析,根據我們剛才所學的內容,如果將一張圖片放在低密度資料夾下,那麼在高密度裝置上顯示圖片時就會被自動放大,而如果將一張圖片放在高密度資料夾下,那麼在低密度裝置上顯示圖片時就會被自動縮小。那我們可以通過成本的方式來評估一下,一張原圖片被縮小了之後顯示其實並沒有什麼副作用,但是一張原圖片被放大了之後顯示就意味著要佔用更多的記憶體了。因為圖片被放大了,畫素點也就變多了,而每個畫素點都是要佔用記憶體的。

我們仍然可以通過例子來直觀地體會一下,首先將android_logo.png圖片移動到drawable-xxhdpi目錄下,執行程式後我們通過Android Monitor來觀察程式記憶體使用情況:


可以看到,程式所佔用的記憶體大概穩定在19.45M左右。然後將android_logo.png圖片移動到drawable-mdpi目錄下,重新執行程式,結果如下圖所示:


現在漲到23.40M了,佔用記憶體明顯增加了。如果你將圖片移動到drawable-ldpi目錄下,你會發現佔用記憶體會更高。
通過這個例子同時也驗證了一個問題,我相信有不少比較有經驗的Android程式設計師可能都遇到過這個情況,就是當你的專案變得越來越大,有的時候載入一張drawable-hdpi下的圖片,程式就直接OOM崩掉了,但如果將這張圖放到drawable-xhdpi或drawable-xxhdpi下就不會崩掉,其實就是這個道理。

那麼經過上面一系列的分析,答案自然也就出來了,圖片資源應該儘量放在高密度資料夾下,這樣可以節省圖片的記憶體開支,而UI在設計圖片的時候也應該儘量面向高密度螢幕的裝置來進行設計。就目前來講,最佳放置圖片資源的資料夾就是drawable-xxhdpi。那麼有的朋友可能會問了,不是還有更高密度的drawable-xxxhdpi嗎?幹嗎不放在這裡?這是因為,市面上480dpi到640dpi的裝置實在是太少了,如果針對這種級別的螢幕密度來設計圖片,圖片在不縮放的情況下本身就已經很大了,基本也起不到節省記憶體開支的作用了。

好的,關於drawable微技巧方面的探索我們就講到這裡,本篇文章中也是集合了不少我平時的工作經驗總結,以及通過做試驗所得出的一些結論,相信還是可以給大家帶來不少幫助的。後面我會抓緊時間繼續準備新系列的內容,敬請期待。