1. 程式人生 > >AndroidAutoSize開源庫螢幕適配分析

AndroidAutoSize開源庫螢幕適配分析

1、AndroidAutoSize實戰

1.1 AndroidAutoSize簡介

  • 基於今日頭條螢幕適配方案的一個開源庫(Star: 4329);
  • 通過修改Application/Activity等的DisplayMetrics中核心資料,使得在不同解析度手機上對應的dp相等而達到每個顯示的View佔用螢幕的比例相同。

1.2 程式碼實現

1.2.1 依賴

compile 'me.jessyan:autosize:1.0.1'

1.2.2 manifest配置

<manifest>
    <application>            
        <meta-data
            android:name="design_width_in_dp"
            android:value="360"/>
        <meta-data
            android:name="design_height_in_dp"
            android:value="640"/>           
     </application>           
</manifest>

1.2.3 Activity支援

  • 預設情況下已經支援適配(寬度)

image

  • 取消適配

取消適配只需要我們的Activity實現CancelAdapt即可,具體如下:

public class TestActivity extends Activity implements CancelAdapt{

}

效果圖如下:

image

取消適配後可以看到寬度360dp 已經不能佔滿整個寬屏。

  • 自定義寬/高(dp)

如果我們的Activity不能使用Manifest中配置的寬/高,需要個性化配置。Activity只需要實現CustomAdapt 介面並實現isBaseOnWidth和getSizeInDp兩個函式即可,具體程式碼如下:

public class TestActivity extends Activity implements CustomAdapt {

    @Override
    public boolean isBaseOnWidth() {
        // true: 以寬度等比例縮放     false: 以高度等比例縮放
        return true;
    }

    @Override
    public float getSizeInDp() {
        //  對應的寬/高  
        return 540;
    }
}

效果圖如下:

image

通過自定義總寬度值,可以看到寬度180dp只佔有整個螢幕寬度的1/3。

1.2.4 Fragment支援

(略)

1.3 不同解析度螢幕效果對比

  • 裝置介紹
螢幕解析度 dpi 螢幕寬度dp
手機 A 720x1280 160 720(px=dp*(dpi/160))
手機 B 720x1280 320 360(px=dp*(dpi/160))
  • 適配前效果對比(左:手機A, 右:手機B)

image

可以發現,僅僅通過dp來適配,不同的裝置顯示的差異非常大。

  • 適配後效果對比(左:手機A, 右:手機B)

為了與上圖有一個比較,此處將Manifest中meta-data的design_width_in_dp設定為400。

image

適配過後,不同解析度的裝置顯示非常相似。

2、AndroidAutoSize原理分析

2.1 基本概念

2.1.1 一些重要的單位

名稱 簡介
px pixels(畫素),螢幕上的實際畫素點,無論控制元件或文字最終都會轉化為px單位來顯示其大小。
dp 與dip雷同,指的是裝置獨立畫素,在不同解析度和尺寸的手機上代表了不同的真實畫素,計算公式:px = dp(dpi/160)
dpi 畫素密度,指的是在系統軟體上指定的單位尺寸的畫素數量,它往往是寫在系統出廠配置檔案的一個固定值。
sp 全稱scaled pixels,放大畫素的縮寫,專門用於處理字型的大小。它不僅與螢幕dpi有關,還與系統的預設字型大小有關。

2.1.2 單位轉換中涉及到的兩個重要類

  • DisplayMetrics.java
public class DisplayMetrics {

    public static final int DENSITY_MEDIUM = 160;
    public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
    public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;
    public static int DENSITY_DEVICE = getDeviceDensity();

    public int widthPixels;             // 螢幕畫素(寬)
    public int heightPixels;           // 螢幕畫素(高)
    public float density;                  // 螢幕密度, density = dpi/160, dp與px之間的轉化就是用此引數
    public int densityDpi;              // dpi
    public float scaledDensity;    // 字型大小轉換會用到此引數 px = sp*scaledDensity
    public float xdpi;
    public float ydpi;
    ......

    public void setToDefaults() {
          widthPixels = 0;
          heightPixels = 0;
          density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;
          densityDpi =  DENSITY_DEVICE;
          scaledDensity = density;
          xdpi = DENSITY_DEVICE;
          ydpi = DENSITY_DEVICE;
          ......
    }

}

這裡面有幾個重要的引數: density、densityDpi、scaledDensity,AndroidAutoSize的原理主要就是修改這幾個引數值來實現螢幕的適配。下面還有一個系統提供的單位轉化API,系統內部基本上都是呼叫此API來實現單位轉化。

  • TypedValue.java
public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics){
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
}

根據unit,將value轉化為px值。

2.2 實現原理

通過上面的單位介紹及之間的轉換,我們可以得到如下結論:

  • p x= dp(dpi/160), density = dpi/160 => px = dp*density => dp = px/density;

明白上面這個結論,下面我們來討論為什麼我們日常對控制元件設定的寬/高為某一dp時,無法做到各個手機螢幕的適配。

  • 裝置 A , 螢幕寬度為 720px, dpi為160,則螢幕總dp為 720/(160/160) = 720 dp
  • 裝置 B , 螢幕寬度為 720px, dpi為320,則螢幕總dp為 720/(320/160) = 360 dp

可以看到螢幕的總 dp 寬度在不同的裝置上是會變化的,但是我們在佈局中填寫的 dp 值卻是固定不變的,這就導致我們設定的固定寬度在不同的裝置上顯示的比例不一樣。 例如我們佈局中有一個View設定固定寬度為180dp,在裝置A中會佔螢幕寬度的1/4,但是在裝置B中只會佔螢幕寬度的1/2,這種差別是十分巨大的。

這時我們要想完美適配,那就必須保證這個 View 在任何解析度的螢幕上,與螢幕的比例都是相同的。

要做到在任何解析度的螢幕上顯示比例相同,我們該怎麼做呢?

  • 方案一: 動態改變每個View的dp值

由於每種裝置的寬度dp值是不同的,為使得View能夠在不同裝置上顯示的比例一致,可以通過程式碼計算動態的設定每個View的dp值,這種方式顯然是不合適的,工作量太大,太複雜。

  • 方案二: 每個View設定固定的dp值,通過修改density 值而達到每種解析度手機的寬度dp值相同

由公式:dp = px/density 可知,由於px是螢幕解析度,這個值有硬體確定,我們是無法改變的,那麼我們可以通過修改density 的值使得不同解析度的手機寬度dp值是相同的,這樣當我們對View設定為某一特定的dp寬度時,佔總寬度的dp比例是相同的,這樣也就達到佔螢幕的比例相同。

那麼問題來了,我們如何確定density 的值呢?

由dp = px/density => 螢幕的總 px 寬度 / density = 螢幕的總 dp 寬度

螢幕的總 px 寬度 / density = 螢幕的總 dp 寬度 => 當前裝置螢幕總寬度(單位為畫素)/ 設計圖總寬度(單位為 dp) = density

如果我們將一套設計圖的總寬度(dp)作為最終手機螢幕的中寬度(dp), 從而達到修改density的目的,同時又可以保證最終不同解析度手機的螢幕總寬度是相同的,這也就完成了適配。

AndroidAutoSize/今日頭條 就是基於方案二的原理來實現螢幕適配的。

2.3 方案可行性

假設設計圖中寬度為300dp,一個View在設計圖上的尺寸為 100dp * 100dp,那麼這個View的寬度佔整個設計圖寬度的33.3%,那麼接下來我們來驗證下通過方案二的適配方案,這個View能否做到不同解析度的裝置還能保持與設計圖中一致的比例。

  • 驗證裝置 1

螢幕總寬度為 1080 px,根據上面公式求出density, 1080/300 = 3.6(density),那麼這個尺寸為 100dp * 100dp的View,系統最後會將都換算成px,也就是 px=100*3.6=360(px), 然後 360/1080=0.333=33.3%,與設計圖上的比例一致。

問題一

某些裝置總寬度為1080px,但裝置的dpi不同,是否對該方案適配產生影響?

答案當然是不會的,其實這個方案根本沒有根據 裝置提供的dpi 求出 density,是根據自己的公式求出的 density,所以這對該方案是沒有影響的。

  • 驗證裝置 2-

剛才我們驗證的是寬度為1080px的裝置,現在我們用另外一種解析度的裝置720px來驗證。

螢幕總寬度為 720 px,根據上面公式求出density, 720 /300 = 2.4(density),那麼這個尺寸為 100dp * 100dp的View,系統最後會將都換算成px,也就是 px=100*2.4=24.(px), 然後 240/720=0.333=33.3%,與設計圖上的比例一致。

通過計算,該方案是可行的。

2.4 優缺點

  • 優點
  1. 使用成本非常低,操作非常簡單,使用該方案後在頁面佈局時不需要額外的程式碼和操作,這點可以說完虐其他螢幕適配方案

  2. 侵入性非常低,該方案和專案完全解耦,在專案佈局時不會依賴哪怕一行該方案的程式碼,而且使用的還是 Android 官方的 API,意味著當你遇到什麼問題無法解決,想切換為其他螢幕適配方案時,基本不需要更改之前的程式碼,整個切換過程幾乎在瞬間完成,會少很多麻煩,節約很多時間,試錯成本接近於 0

  3. 可適配三方庫的控制元件和系統的控制元件(不止是是 Activity 和 Fragment,Dialog、Toast 等所有系統控制元件都可以適配),由於修改的 density 在整個專案中是全域性的,所以只要一次修改,專案中的所有地方都會受益

  4. 不會有任何效能的損耗。

  • 缺點
  1. 只需要修改一次 density,專案中的所有地方都會自動適配,這個看似解放了雙手,減少了很多操作,但是實際上反應了一個缺點,那就是隻能一刀切的將整個專案進行適配,但適配範圍是不可控的。

  2. 這個方案依賴於設計圖尺寸,但是專案中的系統控制元件、三方庫控制元件、等非我們專案自身設計的控制元件,它們的設計圖尺寸並不會和我們專案自身的設計圖尺寸一樣。

其實 AndroidAutoSize開源庫已經很大程度上解決了如上兩個缺點,如前面已經給出Activity的用法,適配粒度可以達到Activity。

3、一些思考

3.1 其他方案對比

  • 傳統的dp、layout_weight等做簡單的適配

裝置的dpi值並不是任意指定的,它是通過 sqrt(screenWpx2 + screenHpx2) / 螢幕尺寸 計算出的結果(上面模擬器引數是我特意設定,為了很明顯的演示所需) , 因此在大多數裝置上對View的寬/高設以dp為單位進行設定值,差別並不是十分大,當然這只是大多數裝置,因此要適配每種裝置還是很難做到的。

  • smallestWidth 限定符螢幕適配方案

開發者先在專案中根據主流螢幕的 最小寬度 (smallestWidth) 生成一系列 values-swdp 資料夾 (含有 dimens.xml 檔案),當把專案執行到裝置上時,系統會根據當前裝置螢幕的 最小寬度 (smallestWidth) 去匹配對應的 values-swdp 資料夾,而對應的 values-swdp 資料夾中的 dimens.xml 文字中的值,又是根據當前裝置螢幕的 最小寬度 (smallestWidth) 而定製的,所以一定能適配當前裝置。

├── src/main
│   ├── res
│   ├── ├──values
│   ├── ├──values-sw320dp
│   ├── ├──values-sw360dp
│   ├── ├──values-sw400dp
│   ├── ├──values-sw411dp
│   ├── ├──values-sw480dp
│   ├── ├──...
│   ├── ├──values-sw600dp
│   ├── ├──values-sw640dp
  • 優點
  1. 非常穩定,極低概率出現意外
  2. 不會有任何效能的損耗
  3. 適配範圍可自由控制,不會影響其他三方庫
  • 缺點
  1. 在佈局中引用 dimens 的方式,日常維護修改時較麻煩
  2. 侵入性高,如果專案想切換為其他螢幕適配方案,因為每個 Layout 檔案中都存在有大量 dimens 的引用,這時修改起來工作量非常巨大,切換成本非常高昂
  3. 無法覆蓋全部機型,想覆蓋更多機型的做法就是生成更多的資原始檔。
  4. 如果想使用 sp,也需要生成一系列的 dimens,導致再次增加 App 的體積

參考文獻