Android 進階——高級UI必知必會之常用的屏幕適配完全攻略詳解(七)

引言

本系列高級UI相關文章中,已經將Android的2D繪製基礎已經大致地進行了總結,相信都已經掌握了2D繪製的相關知識,這一篇就總結下關於常見屏幕適配的相關原理,需要說明的是:可以說目前並沒有絕對完美的屏幕適配方案,即使在大廠裏所用的也不一定是完美的。相關係列文件鏈接如下:

一、屏幕適配概述

Android 之所以得以發展壯大成爲全球第一的移動手機操作系統,得益於它的開源性,前期由Google公司和開放手機聯盟(Open Handset Alliance)領導及開發,後面越來越多的ODM、OEM、ROM提供商加入到Android陣營,在帶給Android巨大的活力的同時,也帶來了Android 最嚴重的的問題之一——“碎片化”,表現在設備碎片化、品牌碎片化、系統碎片化、傳感器碎片化和屏幕碎片化等多個方面,而作爲Android開發者我們首先要應對的就是屏幕碎片化的棘手問題。

二、屏幕相關術語

Android 系統爲了適配更多的設備,更多的不同的屏幕,除了物理存在的相關術語之外,還抽象了一些術語來通過一定的邏輯關係與物理存在的術語相聯繫。
在這裏插入圖片描述

1、屏幕尺寸

屏幕尺寸指的是屏幕的對角線的長度(單位是:英寸1英寸=2.54釐米)。

2、屏幕分辨率

屏幕分辨率是用屏幕縱向的像素點乘以橫向的像素點總數(單位是:px1px=1個像素點),比如1920*1080分辨率代表的是縱向上有1920個像素點,而橫向上有1080個像素點。

3、屏幕像素密度

屏幕像素密度每英寸上的像素點數(單位是:dpi,即dot per inch),屏幕像素密度與屏幕分辨率屏幕尺寸相關,可以通過以下公式計算:
在這裏插入圖片描述

4、dp、dip 、dpi、sp和px

  • dp——密度獨立像素是Android 特有的抽象出來的概念,與屏幕分辨率無關,dip 同dp一個意思,目前廢棄了,一般都寫dp。Android內部在識別圖像像素時以160dpi爲基準(1dip=1px1dp=1px)比如說在下列兩臺設備上使用DP進行操作時:
分辨率 屏幕像素密度 說明
480 * 320 160dpi 那麼這臺機器上的1dp會被翻譯成1px
800 * 480 240dpi 而這臺機器上的1dp會被翻譯成1.5px

因爲dp是由Android給予的基礎標準按比例進行翻譯的,這也是爲什麼我們用dp 能解決一部分適配的原因。

  • dpi——屏幕像素密度的單位。
  • sp——即Scale-IndependentPixels,可根據文字大小首選項自動進行縮放,Google推薦我們使用12sp以上的大小,通常可以使用12sp,14sp,18sp,22sp,爲避免精度損失,建議最好不要使用奇數和小數。
  • px——就是我們常說的像素,dp轉爲px的公式: px = dp * (dpi / 160)

Android中無論使用的是dp還是sp,最終都會轉爲px進行運算

假設在某一應用中,用戶的手指至少移動 16 像素之後,系統會識別出滾動或滑動手勢,那麼在基準屏幕上,用戶的手指必須至少移動 16 pixels / 160 dpi,相當於 1 英寸的 1/10(2.5 毫米),相應手勢才能被識別;而在配備高密度顯示屏 (240dpi) 的設備上,用戶的手指必須至少移動 16 pixels / 240 dpi,相當於 1 英寸的 1/15(1.7 毫米),此距離短得多,因此用戶會感覺應用在該設備上更靈敏。要解決此問題,手勢閾值必須在代碼中以 dp 表示,然後再轉換爲實際像素

// The gesture threshold expressed in dp
private static final float GESTURE_THRESHOLD_DP = 16.0f;
// Get the screen's density scale
final float scale = getResources().getDisplayMetrics().density;
// Convert the dps to pixels, based on density scale
mGestureThreshold = (int) (GESTURE_THRESHOLD_DP * scale + 0.5f);
// Use mGestureThreshold as a distance in pixels...

**DisplayMetrics.density 字段根據當前像素密度指定要將 dp 單位轉換爲像素而必須使用的縮放係數。**對於中密度屏幕,DisplayMetrics.density 等於 1.0;對於高密度屏幕,它等於 1.5;對於超高密度屏幕,它等於 2.0;對於低密度屏幕,它等於 0.75

5、mdpi、hdpi、xhdpi、xxhdpi和xxxhdpi

名稱 像素密度範圍 圖片大小
mdpi (基準1.0x) 120dp~160dp 48×48px
hdpi (1.5x) 160dp~240dp 72×72px
xhdpi (2.0x) 240dp~320dp 96×96px
xxhdpi (3.0) 320dp~480dp 144×144px
xxxhdpi (4.0x) 480dp~640dp 192×192px

在Google文檔中說明了 mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi=2:3:4:6:8 的尺寸比例並進行縮放。例如一個圖標的大小爲48×48dp時在mdpi上圖片的實際大小爲48×48px;而在hdpi像素密度上,實際尺寸爲mdpi上的1.5倍,則圖片的實際大小爲72×72px,所以需要把正確分辨率的圖片放到對應的文件夾下,系統將根據運行應用的設備的像素密度自動選取正確的文件

如果你不希望那些資源在運行時不被縮放,則應將這些資源放在帶有 nodpi 配置限定符的目錄中,帶有此限定符的資源被視爲與密度無關,系統將不會對它們進行縮放。

三、屏幕適配策略

Android SDK自身就提供了基礎的適配能力,當然也是最優的適配方案,在Framework層自動進行適配:

  • 在配置佈局XML文件時,避免配置固定的尺寸,優先使用wrap_contentmatch_parent
  • 在LinearLayout中使用weight進行配置,不過呢,不建議嵌套LinearLayout裏使用weight(因爲在嵌套的 LinearLayout 中使用權重時,系統會執行多遍佈局以確定每個視圖的尺寸,從而降低界面性能)。
  • 靈活使用相對佈局和約束佈局,因爲凡是 LinearLayout 所能構建的佈局,ConstraintLayout 幾乎都能構建,而不會影響性能,因此您應該嘗試將佈局轉換爲 ConstraintLayout。然後,您可以使用約束鏈定義加權佈局。
  • 對於不同圖片資源採用.9和SVG替代,靈活實現縮放。

不過呢以上是優先考慮的原則,但是並不能完全適配所有的業務UI,所以引出以下的策略。

使用 ConstraintLayout 時,不宜使用 match_parent,而是應將尺寸設爲 0dp 以啓用一種稱爲“匹配約束”的特殊行爲

1、限定符適配

所謂的限定符就是Android在進行資源加載的時候會自動獲取屏幕的相關信息,進而對相應的文件夾(通過對應的名字)進行識別,而這些特殊名字就是我們的限定符,限定符可以分爲四類:

  • 根據屏幕尺寸,小屏幕small 、基準屏幕normal 、大屏幕large(大於7inch) 和 超大屏幕xlarge
  • 根據屏幕密度
屏幕密度 說明
ldpi <=120dpi
mdpi <= 160dpi
hdpi <= 240dpi
xhdpi <= 320dpi
xxhdpi <= 480dpi
xxhdpi <= 640dpi(只用來存放icon)
nodpi 與屏幕密度無關的資源.系統不會針對屏幕密度對其中資源進行壓縮或者拉伸
tvdpi 介於mdpi與hdpi之間,特定針對213dpi,專門爲電視準備的,手機應用開發不需要關心這個密度值.
  • 根據屏幕方向,橫屏land,豎屏port

  • 根據屏幕寬高比,比標準屏幕寬高比明顯的高或者寬的這樣屏幕long 、和標準屏幕配置一樣的屏幕寬高比notlong

限定符指定的尺寸並不是實際屏幕尺寸,而是 Activity 的窗口可用的寬度或高度(以 dp 爲單位)。Android 系統可能會將部分屏幕用於系統界面(如屏幕底部的系統欄或頂部的狀態欄),所以此部分屏幕不可供佈局使用。如果App在多窗口模式下使用,則它只能使用該窗口的尺寸,且該窗口進行大小調整時就會使用新窗口尺寸並觸發配置更改,以便系統可以選擇適當的佈局文件。因此,在聲明尺寸時,您應具體說明 Activity 需要的尺寸。在聲明爲佈局提供多少空間時,系統會考慮系統界面使用的所有空間。

1.1、屏幕分辨率適配(不推薦)

屏幕分辨率限定符適配(以px爲單位)需要在 res 文件夾下創建各種屏幕分辨率對應的 values-xxx 文件夾:
在這裏插入圖片描述
把所有的需要適配的設備的分辨率都創建對應的資源文件夾,然後選擇一個基準分辨率,例如基準分辨率爲 1280x720,先將寬度等分成 720 份,則對應取值爲 1px~720px,再將高度分成 1280 份,取值爲 1px~1280px,生成各種分辨率對應的 dimens.xml 文件。如下分別爲分辨率 1280x720 與 1920x1080 所對應的橫向dimens.xml 文件(values-xx/lay_x.xml):
在這裏插入圖片描述
假設設計圖上的一個控件的寬度爲 720px,那麼佈局中配置android:layout_width="@dimen/x720" ,當運行時系統會根據設備的分辨率去尋找對應的 dimens.xml 文件。在分辨率爲 1280x720 的設備上時就會去找 values-1280x720 文件夾下的 lay_x.xml 文件,而運行在分辨率爲 1920x1080 的設備上,系統就會去找 values-1920x1080 文件夾下的 lay_x.xml 文件,從而完成適配,本質上就是
多套dimens資源適配
,不過呢這是相當古老的策略,基本已經淘汰了。

1.2、屏幕尺寸限定符適配 (過時)

當我們一個App中的同一頁面在平板和手機時,佈局和尺寸都不一樣就可以使用屏幕尺寸來進行適配,本質上也和屏幕分辨率適配差不多,也是創建多套res資源進行適配,除了創建dimens資源外還可能創建多套layout資源,在屏幕尺寸大於7英寸的設備上就會加載res/layout-large/下的佈局文件,而在小於7英寸的設備上就會加載res/layout/下的,僅適用於Android3.2之前,在此版本之後爲了更精確地區分屏幕尺寸大小,Google推出了最小寬度限定符smallestWidth 。

1.3、smallestWidth 限定符適配(主流)

最小“寬度”限定符 smallestWidth 限定符(以dp和sp爲單位)的使用和屏幕尺寸基本一致,只是使用了具體的“寬度”(以dp爲單位)進行限定,程序運行在最小寬度爲 360dp 的設備上,系統會自動找到對應的 values-sw360dp 文件夾下的 dimens.xml 文件(比如sw360dp代表的是最小“寬度”爲360dp),

此處“最小寬度”是不區分屏幕方向的,即無論是寬度還是高度,哪一邊小就認爲哪一邊是“最小寬度”,它代表的是屏幕兩側的最小尺寸。

如下分別爲最小寬度爲 360dp 與最小寬度爲 640dp 所對應的 dimens.xml 文件:
在這裏插入圖片描述

雖然與屏幕分辯限定符適配意義都需要多套 dimens.xml 文件, 那麼smallestWidth 限定符適配的優勢在哪呢?首先屏幕分辨率限定符適配是根據屏幕分辨率的,Android 設備分辨率一大堆,而且還要考慮虛擬鍵盤和狀態欄,這樣就需要大量的 dimens.xml 文件。而無論手機屏幕分辨率和像素密度幾何,90% 的手機的最小“寬度”都爲 360dp平板的最小“寬度”爲580dp),所以採用 smallestWidth 限定符適配只需要少量dimens.xml 文件即可,再者屏幕分辨率限定符適配需要設備分辨率與 values-xx 文件夾完全匹配才能達到適配,而 smallestWidth 限定符適配在尋找 dimens.xml 文件時是從大往小找,例如設備的最小寬度爲 360dp,就會先去找 values-360dp,若找不到則會向下去找最接近的,若還沒有則使用默認的 values 下的 demens.xml 文件,所以即使沒有完全匹配也能達到不錯的適配效果。谷歌說明了不同屏幕 dp 寬度與不同屏幕尺寸和方向的一般對應關係:
在這裏插入圖片描述

獲取設備最小“寬度”代碼爲:

DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int heightPixels = ScreenUtils.getScreenHeight(this);
int widthPixels = ScreenUtils.getScreenWidth(this);
float density = dm.density;
float heightDP = heightPixels / density;
float widthDP = widthPixels / density;
float smallestWidthDP;
if(widthDP < heightDP) {
    smallestWidthDP = widthDP;
}else {
    smallestWidthDP = heightDP;
}

以上的這些值可以通過ScreenMatch 插件自動生成dimens裏的值:

  • 首先在res目錄下右鍵,然後選擇ScreenMatch,點擊OK之後,就會在Project目錄下生成默認的values/dimens.xml文件裏的值(不過名稱是screenMatch_example_dimens.xml)
    在這裏插入圖片描述
    和用於配置ScreenMatch插件的screenMatch.properties文件
    在這裏插入圖片描述
  • 把screenMatch_example_dimens.xml文件裏的值複製到values下的dimens.xml
  • 根據需求修改默認的配置文件(默認是以360dp作爲基線)
  • 再選中dimens.xml文件右鍵ScreenMatch,點擊OK。

系統在進行資源加載的時候,會根據自身機型的特性不同,去加載不同文件夾底下的資源,sw360dp大多數情況下都可以兼容所有手機的分辨率。

sw360dp---->大多數手機設備
sw720dp---->10寸平板
sw560dp---->8寸平板
sw480dp---->7寸平板

1.4、屏幕方向限定符適配

如果我們要求給橫屏、豎屏顯示的佈局不一樣,就可以使用屏幕方向限定符來實現,另外屏幕方向限定符適配還可以搭配最小“寬度”限定符來使用,其他的思想也大同小異,比如res/values-sw360dp-port/activity_main.xml表示

1.5、可用寬度限定符適配

可用寬度限定符適配是根據當前可用的寬度或高度來進行適配的,而不是根據屏幕的最小寬度來加載佈局。比如說App有一個界面佈局,需求是在屏幕寬度至少爲 600dp 時才使用該佈局,就可以使用可用寬度限定符進行適配,原理大同小異,創建對應的資源例如res/layout-w600dp/main_activity.xml

以上所有的限定符策略,核心思想都是一樣的,就是多套資源進行適配,當然也可以在清單文件中進行聲明所支持的屏幕分辨率。

2、佈局別名適配

所謂佈局別名適配就是把不同名字的佈局XML文件,通過在values聲明,使之產生聯繫代表同一個文件,實現起來很簡單,假如App一個界面在手機(res/layout/main.xml )和平板(res/layout/main_pad.xml )上需要分別加載不同的佈局,使用佈局別名適配方案的話需要以下步驟:

2.1、首先,在res/layout/下創建兩種佈局文件res/layout/main.xml和res/layout/main_pad.xml

2.2、然後分別建立res/values/layout.xml、res/values-large/layout.xml、res/values-sw600dp/layout.xml(Android3.2之後的平板佈局)三個文件。

  • res/values/layout.xml
<resources>
    <item name="main" type="layout">@layout/main</item>
</resources>
  • res/values-large/layout.xml

當App運行在Android3.2之前的平板上時,會自動加載該values下對應的文件引用

<resources>
    <item name="main" type="layout">@layout/main_pad</item>
</resources>
  • res/values-sw600dp/layout.xml
    當App運行在Android3.2之後的平板上時,會自動加載該values下對應的文件引用
<resources>
    <item name="main" type="layout">@layout/main_pad</item>
</resources>

以上三個values文件皆聲明瞭“main”別名佈局。

  • 使用別名

經過再values聲明瞭之後就有了main爲別名的佈局,在Activity中setContentView(R.layout.main)即可,App在運行時就自動會檢測手機的屏幕大小,如果是平板設備就會加載res/layout/main_pad.xml,如果是手機設備,就會加載res/layout/main.xml 。

3、清單文件裏配置支持的屏幕

App自然是使其適合所有屏幕尺寸和密度的設備上運行最好,但如果有特殊要求,可能不希望App在某些特殊屏幕上運行,就可以在manifest清單文件上聲明。

3.1、配置屏幕的最大長寬比

用戶可以在分屏和自由窗口模式下啓動可調整大小的 Activity並通過拖動其邊或角來更改該 Activity 的大小,多窗口模式適用於在 Android 7.0或更高版本中運行的所有應用並應用默認可調整大小。 當然還可以爲整個應用或特定 Activity 明確設置 android:resizeableActivity=true 屬性(當android:resizeableActivity =false時就不支持多窗口模式)。 Android 8.0和更高版本設置最大長寬比可以在清單文件中 < activity > 標記中配置 android:MaxAspectRatio 來配置最大比例:

<!-- Render on full screen up to screen aspect ratio of 2.4 -->
<!-- Use a letterbox on screens larger than 2.4 -->
<activity android:maxAspectRatio="2.4">
 ...
</activity>

在Android 7.1 及更低版本,請在 < application > 元素中添加一個 名爲 android.max_aspect 的 < meta-data > 元素:

<!-- Render on full screen up to screen aspect ratio of 2.4 -->
<!-- Use a letterbox on screens larger than 2.4 -->
<meta-data android:name="android.max_aspect" android:value="2.4" />

要想最大長寬比生效,Activity 的android:resizeableActivity必須爲false

3.2、配置支持平板或電視

在清單文件裏使用 < supports-screens > 節點配置支持的屏幕尺寸。

<manifest ... >
    <supports-screens android:smallScreens="false"
                      android:normalScreens="false"
                      android:largeScreens="true"
                      android:xlargeScreens="true"/>
    ...
</manifest>

清單文件裏雖然支持 < compatible-screens > 配置App支持的確切特定的屏幕尺寸和密度,< compatible-screens> 元素必須包含一個或多個 < screen> 元素且每個 < screen > 元素都使用 android:screenSize 和 android:screenDensity 屬性指定與您的應用兼容的屏幕配置 。 每個 < screen > 元素都必須同時包含這兩個屬性才能指定一個屏幕配置 ,缺少任一屬性都會導致該元素無效。

<manifest ... >
    <compatible-screens>
        <!-- all small size screens -->
        <screen android:screenSize="small" android:screenDensity="ldpi" />
        <screen android:screenSize="small" android:screenDensity="mdpi" />
        <screen android:screenSize="small" android:screenDensity="hdpi" />
        <screen android:screenSize="small" android:screenDensity="xhdpi" />
        <!-- all normal size screens -->
        <screen android:screenSize="normal" android:screenDensity="ldpi" />
        <screen android:screenSize="normal" android:screenDensity="mdpi" />
        <screen android:screenSize="normal" android:screenDensity="hdpi" />
        <screen android:screenSize="normal" android:screenDensity="xhdpi" />
    </compatible-screens>
    ...
    <application ... >
        ...
    <application>
</manifest>

4、運行時動態代碼適配

核心思想很簡單,選取一個分辨率作爲基準,計算縮放比例,然後繼承ViewGroup重寫onMeasure方法,在佈局裏替換Android系統的原生ViewGroup,用自己寫的ViewGroup包裹控件,並且在onMeasure方法裏重新設置尺寸,由於篇幅問題,請參見下篇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章