1. 程式人生 > >安卓專案實戰之與UI那點事:圖片適配你必須要了解的知識點

安卓專案實戰之與UI那點事:圖片適配你必須要了解的知識點

1,mipmap和drawable的區別

在Android4.2以上的版本中,提供了對mipmaps的支援,如果你用Andorid Studio開發Android程式會發現Android Studio自動幫你建立了幾個mipmaps資料夾,很多人每次新建一個工程的時候,總是先把mipmap刪掉,新建幾個不同dpi的drawable資料夾,才開始幹別的。究竟mipmap和drawable有什麼區別呢?對此疑問進行一次總結如下:

首先我們先來糾正一個錯誤:mipmap資料夾是用來替代drawable資料夾的?
經多方查詢我們發現官網壓根沒說用mipmap資料夾替代drawable資料夾這樣的話,而且根據官方對於mipmap資料夾的介紹我們可以得出以下結論:
就目前而言,mipmap資料夾僅僅是用來存放應用的啟動圖示和與縮放動畫相關的圖片的

,AndroidStudio新建專案的ic_launcher.png都是預設放在mipmap資料夾下的,這樣無論何種螢幕解析度,系統都會選擇最適合的解析度的icon顯示在主屏上。
而其他的圖片資源等,還是按照以前方式,放在drawable資料夾下,如點陣圖檔案(PNG、JPEG、GIF),還有點九圖片和XML檔案(shape和selector等)。
擴充套件:
mipmap指的是一種紋理對映技術,是目前解決紋理解析度與視點距離關係的最有效途徑,mipmap是Android系統為了解決不同解析度顯示高清圖示而採用的一個技術,來使圖示保證清晰且適配各種螢幕。而mipmap也可以用在需要在動畫中被縮放的圖片,關於mipmap紋理對映技術的具體實現細節如果感興趣請讀者自行去查詢資料瞭解。

2,mipmap應用啟動圖示適配

講過上面的瞭解我們知道mipmap資料夾只是用來放置應用程式的啟動圖示icon的,僅此而已,並且系統對於mipmaps的支援也是從Android4.2以上版本才開始的,將icon放置在mipmap資料夾還可以讓我們程式的launcher圖示自動擁有跨裝置密度展示的能力,比如說一臺螢幕密度是xxhdpi的裝置可以自動載入mipmap-xxxhdpi下的icon來作為應用程式的launcher圖示,這樣圖示看上去就會更加細膩。
對於每種密度下的icon應該設計成什麼尺寸其實Android也是給出了最佳建議,icon的尺寸最好不要隨意設計,因為過低的解析度會造成圖示模糊,而過高的解析度只會徒增APK大小。建議尺寸如下表所示:
在這裡插入圖片描述


然後我們引用mipmap的方式和之前引用drawable的方式是完全一致的,在資源中就使用@mipmap/res_id,在程式碼就使用R.mipmap.res_id即可。
建議:我們至少需要提供一個xxxhdpi型別的啟動圖示,因為Android會幫你自動縮小圖示到對應的別的解析度上(放大是會變模糊的),這樣子可以節省些apk size。

3,drawable圖片適配(重點)

在Android專案當中,drawable資料夾都是用來放置圖片資源的,不管是jpg、png、還是9.png,都可以放在這裡。除此之外,還有像selector這樣的xml檔案也是可以放在drawable資料夾下面的。
一般根據裝置dpi(dpi是指每英寸的畫素)的不同我們需要建立不同的drawable資料夾,如下圖:
在這裡插入圖片描述
可以看到dpi可大致分為mdpi,hdpi,xhdpi,xxhdpi,xxxdpi資料夾,按照安卓官方的適配建議需要每個資料夾中都放置相對應的圖片,這樣一來一個圖片就會有多個,有的人可能認為這樣會增大工作量,只使用一套圖放置一個資料夾,這樣減輕UI人員和開發人員的工作,但是這樣會造成另一個問題,就是會造成記憶體問題,接下來我們具體來看下是如何造成記憶體問題的:
首先我準備了一張270*480畫素的圖片:
在這裡插入圖片描述
將圖片命名為android_logo.png,然後把它放在drawable-xxhdpi資料夾下面。為什麼要放在這個資料夾下呢?是因為我的手機螢幕的密度就是xxhdpi的。那麼怎麼才能知道自己手機螢幕的密度呢?你可以使用如下方法先獲取到螢幕的dpi值:

float xdpi = getResources().getDisplayMetrics().xdpi;
float ydpi = getResources().getDisplayMetrics().ydpi;

其中xdpi代表螢幕寬度的dpi值,ydpi代表螢幕高度的dpi值,通常這兩個值都是近乎相等或者極其接近的,在我的手機上這兩個值都約等於403。那麼403又代表著什麼意思呢?我們直接參考下面這個表格就知道了:
在這裡插入圖片描述
從表中可以看出,403dpi是處於320dpi到480dpi之間的,因此屬於xxhdpi的範圍。
圖片放好了之後,下面我在佈局檔案中引用這張圖片,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
   >

    <ImageView
        android: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,這樣圖片有多大,我們的控制元件就會有多大。
現在執行一下程式,效果如下所示:
在這裡插入圖片描述
由於我的手機解析度是10801920畫素的,而這張圖片的解析度是270480畫素的,剛好是手機解析度的四分之一,因此從上圖中也可以看出,android_logo圖片的寬和高大概都佔據了螢幕寬高的四分之一左右,大小基本是比較精準的。
關於手機螢幕的解析度資訊我們可以通過在Activity中呼叫如下程式碼來獲取:

DisplayMetrics dm = new DisplayMetrics();
this.getWindowManager().getDefaultDisplay().getMetrics(dm);
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;

接著我們嘗試做點改變,將android_logo.png這張圖移動到drawable-xhdpi資料夾下,注意不是複製一份到drawable-xhdpi資料夾下,而是將圖片移動到drawable-xhdpi資料夾下,然後重新執行一下程式,效果如下圖所示:
在這裡插入圖片描述
嗯?怎麼感覺圖片好像變大了一點,是錯覺嗎?
那麼我們再將這張圖移動到drawable-mdpi資料夾下試試,重新執行程式,效果如下圖所示:
在這裡插入圖片描述
這次肯定不是錯覺了,這實在是太明顯了,圖片被放大了!
那麼為什麼好端端的一張圖片會被自動放大呢?而且這放大的比例是不是有點太過份了。其實不然,Android所做的這些縮放操作都是有它嚴格的規定和演算法的。可能有不少做了很多年Android的朋友都沒去留意過這些縮放的規則,因為這些細節太微小了,那麼本篇的微技巧探索裡面,我們就來把這些細節理理清楚。

首先解釋一下圖片為什麼會被放大,當我們使用資源id來去引用一張圖片時,Android會使用一些規則來去幫我們匹配最適合的圖片。具體的查詢規則如下:

  1. 先查詢和螢幕密度最匹配的資料夾。比如上例中我的手機螢幕密度是xxhdpi,那麼系統會優先去drawable-xxhdpi資料夾下去查詢,如果有的話就使用,此時圖片是不會被放大或者縮小的。
  2. 如果在最匹配的目錄沒有找到對應圖片,就會向更高密度的目錄查詢,直到沒有更高密度的目錄。上例中更高密度的目錄就是drawable-xxxhdpi檔案夾了,然後發現這裡也沒有android_logo這張圖,接下來會嘗試再找更高密度的資料夾,發現沒有更高密度的了。
  3. 如果一直往高密度目錄均沒有查詢,Android就會查詢drawable-nodpi目錄。drawable-nodpi目錄中的資源適用於所有密度的裝置,不管當前螢幕的密度如何,系統都不會縮放此目錄中的資源。因此,對於永遠不希望系統縮放的資源,最簡單的方法就是放在此目錄中;同時,放在該目錄中的資源最好不要再放到其他drawable目錄下了,避免得到非預期的效果。
  4. 如果在drawable-nodpi目錄也沒有查詢到,系統就會向比最匹配目錄密度低的目錄依次查詢,直到沒有更低密度的目錄。例如,上例中最匹配目錄是xxhdpi,更高密度的目錄和nodpi目錄查詢不到後,就會依次查詢drawable-xhdpi -> drawable-hdpi -> drawable-mdpi -> drawable-ldpi。

總體匹配規則就是這樣,比如說此時在螢幕密度為xxhdpi的裝置上,系統優先去drawable-xxhdpi資料夾下去查詢並沒有找到,那麼繼續往更高密度找也沒有找到,最後終於在drawable-mdpi資料夾下面找到android_logo這張圖了,但是系統會認為你這張圖是專門為低密度的裝置所設計的,如果直接將這張圖在當前的高密度裝置上使用就有可能會出現畫素過低的情況,於是系統自動幫我們做了這樣一個放大操作,圖片放大則畫素增加,必然會引起記憶體佔用量增加。
那麼同樣的道理,如果系統是在drawable-xxxhdpi資料夾下面找到這張圖的話,它會認為這張圖是為更高密度的裝置所設計的,如果直接將這張圖在當前裝置上使用就有可能會出現畫素過高的情況,於是會自動幫我們做一個縮小的操作,圖片縮小則畫素減少,記憶體佔用量就會降低。
所以,我們可以嘗試將android_logo這張圖移動到drawable-xxxhdpi資料夾下面將會得到這樣的結果:
在這裡插入圖片描述
可以看到,現在圖片的寬和高都達到不手機螢幕的四分之一,說明圖片確實是被縮小了。
接下來我們來看下在實際開發當中會遇到的場景:根據Android的開發建議,我們在準備圖片資源時儘量應該給每種密度的裝置都準備一套,這樣程式的適配性就可以達到最好,但也存在問題,一是這種方式會增大安裝包的大小;二是很多公司UI在出圖時只會出一套。那麼在這種情況下,我們應該將僅有的這一套圖片資源放在哪個密度的資料夾下呢?
可以這樣來分析,根據我們剛才所學的內容,如果將一張圖片放在低密度資料夾下,那麼在高密度裝置上顯示圖片時就會被自動放大,而如果將一張圖片放在高密度資料夾下,那麼在低密度裝置上顯示圖片時就會被自動縮小。那我們可以通過成本的方式來評估一下,一張原圖片被縮小了之後顯示其實並沒有什麼副作用,但是一張原圖片被放大了之後顯示就意味著要佔用更多的記憶體了。因為圖片被放大了,畫素點也就變多了,而每個畫素點都是要佔用記憶體的。
記憶體的使用量可通過Android Monitor來檢視,首先將android_logo.png圖片移動到drawable-xxhdpi目錄下,執行程式後我們通過Android Monitor來觀察程式記憶體使用情況: (裝置和目錄級別相一致的最優情況)
在這裡插入圖片描述
可以看到,程式所佔用的記憶體大概穩定在19.45M左右。然後將android_logo.png圖片移動到drawable-mdpi目錄下,重新執行程式,結果如下圖所示: (低密度資料夾下圖片在高密度裝置上顯示時會被放大)
在這裡插入圖片描述
現在漲到23.40M了,佔用記憶體明顯增加了,可以看到,僅僅一張圖片的記憶體佔用差別就已經在MB級別了。如果你將圖片移動到drawable-ldpi目錄下,你會發現佔用記憶體會更高。圖片放大的記憶體成本將是不得不考慮的一個重要因素了。

那麼經過上面一系列的分析,答案自然也就出來了,圖片資源應該儘量放在高密度資料夾下,這樣可以節省圖片的記憶體開支,而UI在設計圖片的時候也應該儘量面向高密度螢幕的裝置來進行設計。由於目前的Android智慧手機的螢幕基本都在1080p了,螢幕的dpi多數都處於320~480,為了更好地適配,同時為了節省記憶體成本,建議將切圖放置在drawable-xxhdpi目錄,同時建議UI針對該密度的裝置設計切圖。那麼有的朋友可能會問了,不是還有更高密度的drawable-xxxhdpi嗎?幹嗎不放在這裡?這是因為,市面上480dpi到640dpi的裝置實在是太少了,如果針對這種級別的螢幕密度來設計圖片,圖片在不縮放的情況下本身就已經很大了,基本也起不到節省記憶體開支的作用了。

4,UI切圖px標註轉dp

一款優秀app的產生,往往需要有一套精美華麗的UI設計圖,誠然,UI僅僅只是個開始,有追求極致的前端工程師開發軟體時儘可能地去貼近UI的設計才是重中之重。

我們知道,Android的尺寸單位一般採用dp或者sp,然而有時候我們遇到的UI設計圖給的尺寸標註卻是px的,這顯然是給iOS畫的UI。安卓裝置的多樣性決定了我們絕對不能將控制元件的尺寸大小直接設定為UI圖上的px值。那該如何解決呢?憤憤不平地去找UI工程師出一套安卓的標註?條件允許的話你當然可以這樣幹,但其實我們還有另外一種快準不知道狠不狠的解決方案:px轉dp。

我們知道px轉dp的公式為:dp = px/density
上面density指是裝置密度,有了裝置密度,我們才可以將px轉為dp。而Android系統也為我們提供了獲取裝置密度的方法:

context.getResources().getDisplayMetrics().density;

獲取到了我們測試手機的裝置密度之後,然後將UI圖上標註的px去除以desity?
當然不是!!!,density值是獲取了,但是請問UI圖上的px值是按照你的手機來標註的嗎?也就是說,我們必須要獲取UI圖在設計時基於的裝置的裝置密度(density)。

裝置密度公式:density = PPI/160。
PPI是畫素密度,公式:PPI = √(長度畫素數² + 寬度畫素數²) / 螢幕尺寸
上面PPI的公式不難理解,就是指每英寸螢幕有多少個畫素點。比如iPhone6的PPI是326,1英寸螢幕有326個畫素點。至於裝置密度這個公式,PPI除以160,為什麼是160而不是別的,這個不用太過於糾結。160是谷歌推薦的數值,這樣轉換為hdpi、xhdpi等的數值就比較妥當。

根據上面的公式我們就有了如下計算裝置密度density的方法:

int width = 750;//螢幕寬度
int height = 1334;//螢幕高度
float screenInch = 4.7f;//螢幕尺寸
//裝置密度公式
float density = (float) Math.sqrt(width * width + height * height) / screenInch / 160;

注意上面三個變數值是UI切圖時所基於裝置的取值。問UI工程師,問ta是以哪個尺寸為基準進行畫圖的。也有個神器叫PxCook能識別出UI圖的裝置型號基準,然後通過裝置型號搜尋出該裝置是幾寸屏。

一般在Android中px與dp的關係:

dp可以保證在不同螢幕畫素密度的裝置上顯示相同的效果,而ui設計師給你的設計圖是以px為單位的,Android開發則是使用dp作為單位的,那麼我們需要進行轉換:
在這裡插入圖片描述
在Android中,規定以160dpi(即螢幕解析度為320x480)為基準:1dp=1px

例如我之前所在公司的UI切的圖都是基於1280*720解析度的裝置切的圖,所以對於他在圖上所標出的px我都是直接除以2得到dp值然後來使用的。

dp,sp與px之間的轉換工具類:

/**
 * dp,sp 和 px 轉換的輔助類
 */
public class DisplayUtil {

    /**
     * 將px值轉換為dip或dp值,保證尺寸大小不變
     * DisplayMetrics類中屬性density
     */
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }

    /**
     * 將dip或dp值轉換為px值,保證尺寸大小不變
     * DisplayMetrics類中屬性density
     */
    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    /**
     * 將px值轉換為sp值,保證文字大小不變
     * DisplayMetrics類中屬性scaledDensity
     */
    public static int px2sp(Context context, float pxValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (pxValue / fontScale + 0.5f);
    }

    /**
     * 將sp值轉換為px值,保證文字大小不變
     * DisplayMetrics類中屬性scaledDensity
     */
    public static int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}