支援不同的螢幕大小

Android 裝置有多種外形與大小。因此,您的應用程式版面配置必須能夠快速回應並自動調整。相較於以特定螢幕大小和長寬比為依據的靜態維度來定義版面配置,您應將應用程式設計成能夠妥善配合不同的螢幕大小與方向。

透過盡可能支援多款螢幕,您就可以只使用一個 APK 或 AAB,讓更多不同裝置的使用者都能使用應用程式。此外,為不同螢幕大小設計應用程式可確保應用程式能夠處理裝置上的視窗設定變更,例如使用者存取多視窗模式;或者您的應用程式在折疊式裝置上執行,而螢幕大小和長寬比會在應用程式執行過程中變更。

不過,支援不同的螢幕大小和方向,不代表您的應用程式一定與所有 Android 板型規格相容。您必須採取額外步驟,才能支援 Wear OS、TV、Auto 和 Chrome OS

如要瞭解如何為不同螢幕建構 UI,請參閱質感設計:瞭解版面配置

視窗大小類別

視窗大小類別是一組固定的可視區域中斷點,可用來設計、開發及測試可調整大小的應用程式版面配置。這些類別經過專門挑選,目的是要讓您在最佳化應用程式時兼顧版面配置的簡單與靈活,以滿足獨特的情境需求。

視窗大小類別將應用程式可用的原始視窗大小分割為更易於管理且更有意義的值區。值區共有三種:精簡、中等和展開。可用的寬度和高度會單獨進行分區,因此應用程式在任何時間點都有兩個相關的大小類別:寬度視窗大小類別和高度視窗大小類別。

雖然視窗大小類別會同時指定寬度和高度,但由於垂直捲動是較普遍的操作,可用寬度往往比可用高度更加重要。因此,寬度視窗大小類別與應用程式 UI 的關聯性可能會更強。

圖 1. 以寬度為基礎的視窗大小類別圖示。
圖 2. 以高度為基礎的視窗大小類別圖示。

如上圖所示,這些中斷點可讓您繼續思考裝置與設定的版面配置。每個大小類別中斷點都代表了典型裝置情境的大部分案例,當您考慮設計以中斷點為基礎的版面配置時,這個參考框架便可以派上用場。

大小類別 中斷點 裝置佔比
精簡寬度 < 600dp 99.96% 直向模式的手機
中等寬度 600dp+ 93.73% 直向模式的平板電腦

直向模式的展開大型內部螢幕

展開寬度 840dp+ 97.22% 橫向模式的平板電腦

橫向模式的展開大型內部螢幕

精簡高度 < 480dp 99.78% 橫向模式的手機
中等高度 480dp+ 96.56% 橫向模式的平板電腦

97.59% 直向模式的手機

展開高度 900dp+ 94.25% 直向模式的平板電腦

雖然透過實體裝置來顯示大小類別很有用,但視窗大小類別並非明確取決於螢幕的實際大小。換句話說,視窗大小類別並不代表 isTablet 邏輯,而是取決於應用程式可用的視窗大小。

這會造成兩種重要影響:

  • 實體裝置不能保證特定的視窗大小類別。有許多原因會導致應用程式的可用螢幕空間與裝置的實體螢幕大小不同。在行動裝置上,分割畫面模式可將畫面分割給多個應用程式。在 Chrome OS 中,Android 應用程式能用可自由調整大小的任意形式視窗顯示。折疊式裝置可以有多個實體螢幕,而且折疊裝置時會變更顯示的螢幕。

  • 視窗大小類別可能在應用程式的生命週期發生變化。當應用程式處於開啟狀態時,裝置旋轉、多工處理和折疊功能都可能變更可用的螢幕空間。因此,視窗大小類別是動態的,而應用程式的 UI 應隨之調整。

請使用視窗大小中斷點決定概略的應用程式版面配置,例如決定使用特定的標準版面配置,以善用額外的螢幕空間。中斷點也與質感設計回應式版面配置格線中的版面配置中斷點一一對應。

如要計算視窗大小類別,應用程式應查詢 Jetpack WindowManager 資料庫提供的現有適用視窗指標。

檢視畫面

enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ...

        // Replace with a known container that you can safely add a View
        // to where it won't affect the layout and the view won't be
        // replaced.
        val container: ViewGroup = binding.container

        // Add a utility view to the container to hook into
        // View.onConfigurationChanged.
        // This is required for all activities, even those that don't
        // handle configuration changes.
        // We also can't use Activity.onConfigurationChanged, since there
        // are situations where that won't be called when the configuration
        // changes.
        // View.onConfigurationChanged is called in those scenarios.
        container.addView(object : View(this) {
            override fun onConfigurationChanged(newConfig: Configuration?) {
                super.onConfigurationChanged(newConfig)
                computeWindowSizeClasses()
            }
        })

        computeWindowSizeClasses()
    }

    private fun computeWindowSizeClasses() {
        val metrics = WindowMetricsCalculator.getOrCreate()
            .computeCurrentWindowMetrics(this)

        val widthDp = metrics.bounds.width() /
            resources.displayMetrics.density
        val widthWindowSizeClass = when {
            widthDp < 600f -> WindowSizeClass.COMPACT
            widthDp < 840f -> WindowSizeClass.MEDIUM
            else -> WindowSizeClass.EXPANDED
        }

        val heightDp = metrics.bounds.height() /
            resources.displayMetrics.density
        val heightWindowSizeClass = when {
            heightDp < 480f -> WindowSizeClass.COMPACT
            heightDp < 900f -> WindowSizeClass.MEDIUM
            else -> WindowSizeClass.EXPANDED
        }

        // Use widthWindowSizeClass and heightWindowSizeClass
    }
}

檢視畫面

public enum WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // ...

        // Replace with a known container that you can safely add a View
        // to where it won't affect the layout and the view won't be
        // replaced.
        ViewGroup container = binding.container;

        // Add a utility view to the container to hook into
        // View.onConfigurationChanged.
        // This is required for all activities, even those that don't
        // handle configuration changes.
        // We also can't use Activity.onConfigurationChanged, since there
        // are situations where that won't be called when the configuration
        // changes.
        // View.onConfigurationChanged is called in those scenarios.
        container.addView(new View(this) {
            @Override
            protected void onConfigurationChanged(Configuration newConfig) {
                super.onConfigurationChanged(newConfig);
                computeWindowSizeClasses();
            }
        });

        computeWindowSizeClasses();
    }

    private void computeWindowSizeClasses() {
        WindowMetrics metrics = WindowMetricsCalculator.getOrCreate()
                .computeCurrentWindowMetrics(this);

        float widthDp = metrics.getBounds().width() /
                getResources().getDisplayMetrics().density;
        WindowSizeClass widthWindowSizeClass;

        if (widthDp < 600f) {
            widthWindowSizeClass = WindowSizeClass.COMPACT;
        } else if (widthDp < 840f) {
            widthWindowSizeClass = WindowSizeClass.MEDIUM;
        } else {
            widthWindowSizeClass = WindowSizeClass.EXPANDED;
        }

        float heightDp = metrics.getBounds().height() /
                getResources().getDisplayMetrics().density;
        WindowSizeClass heightWindowSizeClass;

        if (heightDp < 480f) {
            heightWindowSizeClass = WindowSizeClass.COMPACT;
        } else if (heightDp < 900f) {
            heightWindowSizeClass = WindowSizeClass.MEDIUM;
        } else {
            heightWindowSizeClass = WindowSizeClass.EXPANDED;
        }

        // Use widthWindowSizeClass and heightWindowSizeClass
    }
}

Compose

enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

@Composable
fun Activity.rememberWindowSizeClass() {
    val configuration = LocalConfiguration.current
    val windowMetrics = remember(configuration) {
        WindowMetricsCalculator.getOrCreate()
            .computeCurrentWindowMetrics(this)
    }
    val windowDpSize = with(LocalDensity.current) {
        windowMetrics.bounds.toComposeRect().size.toDpSize()
    }
    val widthWindowSizeClass = when {
        windowDpSize.width < 600.dp -> WindowSizeClass.COMPACT
        windowDpSize.width < 840.dp -> WindowSizeClass.MEDIUM
        else -> WindowSizeClass.EXPANDED
    }

    val heightWindowSizeClass = when {
        windowDpSize.height < 480.dp -> WindowSizeClass.COMPACT
        windowDpSize.height < 900.dp -> WindowSizeClass.MEDIUM
        else -> WindowSizeClass.EXPANDED
    }

    // Use widthWindowSizeClass and heightWindowSizeClass
}

在您觀察應用程式中的視窗大小類別時,您就可以依據目前的大小類別開始變更版面配置。如要瞭解如何使用視窗大小類別,讓以檢視畫面為基礎的版面配置能夠做出回應,請參閱「將 UI 遷移至回應式版面配置」。如要瞭解如何使用視窗大小類別,讓以 Compose 為基礎的版面配置能夠做出回應,請參閱「建構自動調整式版面配置」。

支援不同視窗大小類別的檢查清單

進行變更時,請測試各種視窗大小的版面配置行為,尤其是對精簡、中等和展開的版面配置寬度進行測試。

如果要將現有的版面配置用於較小的螢幕,請先針對展開寬度大小類別最佳化版面配置,因為這提供最多空間來顯示額外的內容或版面配置變更。接著,確認中等寬度類別適用何種版面配置,並考慮加入該大小的專用版面配置。為了提供更優質的使用者體驗,請新增應用程式適用的功能,例如支援折疊式裝置模式,或為鍵盤、滑鼠和觸控筆輸入進行最佳化調整。

如要進一步瞭解如何使應用程式在所有裝置和螢幕大小上都能順利運作,請參閱「大螢幕應用程式品質指南」。

建立回應式版面配置

無論您想先支援哪些硬體設定檔,都必須建立版面配置,以因應螢幕大小的些微變化。

使用 ConstraintLayout

如要針對不同螢幕大小建立回應式版面配置,最好的方法是使用 ConstraintLayout 做為 UI 的基本版面配置。ConstraintLayout 可讓您根據與版面配置中其他檢視畫面之間的空間關係,指定每個檢視畫面的位置和大小。這樣一來,所有檢視畫面都會隨著螢幕大小的變更而移動和延伸。

使用 ConstraintLayout 建立版面配置最簡單的方式,就是使用 Android Studio 中的版面配置編輯器。您可以透過該工具將新的檢視畫面拖曳至版面配置、將其限制附加到父檢視畫面和其他同層級檢視畫面以及編輯檢視畫面的屬性,完全不必手動編輯任何 XML (請參閱圖 3)。

詳情請參閱「使用 ConstraintLayout 建構回應式 UI」。

圖 3. Android Studio 的版面配置編輯器,顯示 ConstraintLayout 檔案。

不過,ConstraintLayout 無法應付每種版面配置情境 (尤其是以動態方式載入的清單,應使用 RecyclerView),但無論版面配置為何,請盡量避免使用硬式編碼的版面配置大小。

避免採用硬式編碼的版面配置大小

為確保版面配置能夠彈性調整,並適應不同螢幕大小,請使用 wrap_contentmatch_parent 來設定大部分檢視畫面元件的寬度和高度,而不要採用硬式編碼的大小:

  • wrap_content 會指示檢視畫面調整大小,以配合檢視畫面中的內容。
  • match_parent 會在父檢視畫面內,盡可能擴大檢視畫面。

例如:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/lorem_ipsum" />

雖然此檢視畫面的實際版面配置取決於其父檢視畫面和任何同層級檢視畫面中的其他屬性,但此 TextView 會將寬度調整為填滿所有可用空間 (match_parent),並將高度設定為正好符合文字高度所需的空間 (wrap_content)。這樣可讓該檢視畫面適應不同的螢幕大小和文字長度。

圖 4 顯示使用 "match_parent" 的文字檢視區塊寬度如何在螢幕寬度隨著裝置方向變更時調整。

圖 4. 彈性文字檢視區塊。

如果您使用的是 LinearLayout,也可以透過版面配置權重來展開子檢視畫面,讓每個檢視畫面依照自身權重值所佔的比例填滿剩餘空間。不過,在巢狀 LinearLayout 中使用權重時,系統必須執行多個版面配置票證,才能判斷每個檢視畫面的大小,而這會降低 UI 的效能。幸好,ConstraintLayout 可以透過 LinearLayout 實現幾乎所有版面配置,而不影響效能,因此建議您嘗試將版面配置轉換成 ConstraintLayout。之後,您就可以使用限制鏈結來定義加權版面配置

對清單/詳細資料 UI 使用 SlidingPaneLayout

清單/詳細資料 UI 可能會針對不同的螢幕大小表現出不同的行為。在大型螢幕上執行時,會有足夠的空間讓清單和詳細資料窗格並排顯示。只要按清單中的項目,即可在詳細資料窗格中顯示詳細資料。不過,如果使用較小的螢幕,則可能會變得太擁擠。與其同時顯示這兩個窗格,不如逐一顯示。一開始,清單窗格會填滿整個視窗。使用者輕觸某個項目時,清單窗格會替換成該項目的詳細資料窗格,並填滿整個視窗。

您可以使用 SlidingPaneLayout 來管理邏輯,以判斷哪一種使用者體驗適合目前的視窗大小:

<?xml version="1.0" encoding="utf-8"?>
<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/item_navigation" />

</androidx.slidingpanelayout.widget.SlidingPaneLayout>

這裡的寬度和權重是決定行為的主要因素。如果視窗夠大,可以同時顯示兩種檢視畫面 (至少 580dp),那麼系統會並排顯示 UI。但是,如果小於上述大小,則會用全螢幕詳細資料 UI 取代全螢幕清單。

採用並排模式時,視窗可能會大於此範例中要求的最小值 580dp。權重值將用於按比例調整兩種窗格的大小。在這個範例中,清單窗格一律為 280dp,而詳細資料窗格則會填滿剩餘的空間。其中一個例外是在折疊式裝置上使用 SlidingPaneLayout V1.2.0-alpha01 以上版本時,SlidingPaneLayout 會自動調整窗格的大小,讓窗格在折線或鉸鏈的任一側。

建立替代版面配置

雖然版面配置應始終透過延伸檢視畫面內部及周圍的空間來回應各種螢幕大小,但這不一定能夠針對所有螢幕大小提供最佳的使用者體驗。舉例來說,專為手機設計的 UI,可能無法在平板電腦上提供良好的使用者體驗。因此,您的應用程式還應該提供替代版面配置資源,以針對特定螢幕大小最佳化 UI 設計。

圖 5. 同一個應用程式在不同大小的螢幕上會使用不同的版面配置。

如要提供螢幕專屬的版面配置,請建立額外的 res/layout/ 目錄 (針對每種需要不同版面配置的螢幕設定建立一個目錄),然後將螢幕設定限定詞附加至 layout 目錄名稱 (例如,假設是可用寬度為 600dp 的螢幕,附加的限定詞為 layout-w600dp)。

這些設定限定詞代表應用程式 UI 可用的顯示畫面空間。系統從應用程式中選擇版面配置時,會考量任何系統裝飾 (例如導覽列) 和視窗設定變更 (例如當使用者啟用多視窗模式)。

如要在 Android Studio (使用 3.0 以上版本) 中建立替代版面配置,請執行以下步驟:

  1. 開啟預設版面配置,然後按一下工具列中的「預覽方向」圖示
  2. 在下拉式清單中,按一下以建立建議的變數,例如「Create Landscape Variation」(建立橫向變數),或按一下「Create Other」(建立其他)
  3. 如果您選取「Create Other」(建立其他),系統會顯示「Select Resource Directory」(選取資源目錄)。請在左側選取螢幕限定詞,然後將其新增至「Chosen qualifiers」(所選限定詞) 清單。限定詞新增完畢後,請按一下「OK」(確定) (如要進一步瞭解螢幕大小限定詞,請參閱後續章節的內容)。

操作完成後,就會在適當的版面配置目錄中建立一個重複的版面配置檔案,以便您開始為該螢幕變化版本自訂版面配置。

使用最小寬度限定詞

您可以使用「最小寬度」螢幕大小限定詞,為具有最小寬度 (以密度獨立像素 dp 或 dip 為測量單位) 的螢幕提供替代版面配置。

Android 以密度獨立像素為度量單位來描述螢幕大小,讓您可為非常具體的螢幕大小建立專用版面配置,而不必擔心像素密度的變化。

舉例來說,您可以建立一個名為 main_activity 的版面配置,並在不同目錄中建立不同版本的檔案,以針對手機和平板電腦進行最佳化處理:

res/layout/main_activity.xml           # For handsets (smaller than 600dp available width)
res/layout-sw600dp/main_activity.xml   # For 7” tablets (600dp wide and bigger)

使用最小寬度限定詞可以指定螢幕兩側的最小大小,且不受裝置當下的螢幕方向影響,因此您可以輕鬆指定版面配置可用的整體螢幕大小。

以下列出幾種最小寬度值與一般螢幕大小的對應關係:

  • 320dp:一般手機螢幕 (240x320 ldpi、320x480 mdpi、480x800 hdpi 等)。
  • 480dp:大型手機螢幕,約 5 吋 (480x800 mdpi)。
  • 600dp:7 吋平板電腦 (600x1024 mdpi)。
  • 720dp:10 吋平板電腦 (720x1280 mdpi、800x1280 mdpi 等)。

圖 6 提供更詳細的視圖,說明不同螢幕 dp 寬度與不同螢幕大小和方向的一般對應關係。

圖 6. 建議使用的寬度中斷點,以支援不同的螢幕大小。

請注意,最小寬度限定詞中的所有數字都是密度獨立像素,因為重點在於系統計算像素密度 (而非原始像素解析度) 之後可用的螢幕空間。

您透過這些限定詞指定的大小並不是實際的螢幕大小,而是活動視窗可用的寬度或高度大小 (以 dp 為單位)。Android 系統可能會將部分螢幕空間用於系統 UI (例如螢幕底端的系統列或頂端的狀態列),因此版面配置可能無法使用部分的螢幕空間。如果您的應用程式以多視窗模式使用,則您的應用程式只能存取該視窗的大小。在調整視窗大小後,系統會以新的視窗大小觸發設定變更,以便系統選取適當的版面配置檔案。因此,您在宣告大小時,應具體指定您的活動需要的大小。系統在宣告可為您的版面配置提供多少空間時,會計算系統 UI 所使用的任何空間。

使用可用的寬度限定詞

建議您根據目前可用的寬度或高度來變更版面配置,而不要根據螢幕的最小寬度來變更版面配置。舉例來說,假設您有一個雙窗格版面配置,您希望在螢幕能夠提供至少 600 dp 的寬度時使用該版面配置,而螢幕寬度會根據裝置螢幕方向是橫向還是直向而改變。在這種情況下,您應該使用「可用寬度」限定詞,如下所示:

res/layout/main_activity.xml         # For handsets (smaller than 600dp available width)
res/layout-w600dp/main_activity.xml  # For 7” tablets or any screen with 600dp available width
                                     # (possibly landscape handsets)

如果您擔心可用高度,可以使用「可用高度」限定詞執行同樣的操作。例如,layout-h600dp 適用於螢幕高度至少為 600 dp 的螢幕。

新增螢幕方向限定詞

雖然您可能只要結合「最小寬度」和「可用寬度」兩個限定詞,就能涵蓋所有的大小變化,但您可能還希望在使用者切換直向或橫向的螢幕方向時改變使用者體驗。

為此,您可以將 portland 限定詞新增到資源目錄名稱中,但要確保這些限定詞位於其他大小限定詞「之後」。例如:

res/layout/main_activity.xml                # For handsets
res/layout-land/main_activity.xml           # For handsets in landscape
res/layout-sw600dp/main_activity.xml        # For 7” tablets
res/layout-sw600dp-land/main_activity.xml   # For 7” tablets in landscape

如要進一步瞭解所有螢幕設定限定詞,請參閱「應用程式資源總覽」。

使用片段將 UI 元件模組化

在針對多種螢幕大小設計應用程式時,請確保您不會在不同活動之間不必要地重複 UI 行為。因此,您應該使用片段將 UI 邏輯擷取至單獨的元件中。然後,如果是在大型螢幕上執行,您可以將片段合併以建立多窗格版面配置;如果是在手機上執行,則可以將其置於單獨的活動中。

舉例來說,平板電腦中的新聞應用程式可能會在左側顯示報導清單,右側顯示完整報導;選取左側的文章即可更新右側的報導檢視畫面。但是,在手機上,這兩個元件會出現在兩個單獨的畫面上;選取清單中的報導會改變整個螢幕畫面,以顯示該報導。

詳情請參閱「片段」總覽。

針對所有螢幕大小進行測試

請務必在各種不同大小的螢幕上測試您的應用程式,以確保 UI 能夠正確縮放。

Android 10 (API 級別 29) 支援更多種長寬比。由於折疊式裝置的關係,板型規格可能會有長而窄的螢幕 (如折疊式裝置的 21:9),也有 1:1 的正方形長寬比。

如要盡可能支援多種裝置,請盡量依據下列螢幕長寬比測試您的應用程式:

圖 7.各種螢幕長寬比。

如果無法支援其中一些長寬比,您可以使用 maxAspectRatiominAspectRatio 來表示應用程式可以處理的最高和最低長寬比。如果螢幕超出這些限制,您的應用程式可能會進入相容模式。

如果您無法取得不同螢幕大小的實體裝置,可以使用 Android Emulator 來模擬任何螢幕大小。

如果您還是想在實體裝置上測試,但手邊沒有,可以使用 Firebase Test Lab 存取 Google 資料中心內的裝置。

宣告支援特定的螢幕大小

如果您決定不要讓您的應用程式在特定螢幕大小下執行,可以對螢幕大小的調整量設定限制,甚至可以根據裝置的螢幕設定,限制哪些裝置可以安裝您的應用程式。詳情請參閱「宣告僅支援部分螢幕大小」。