支援不同的螢幕大小

如果您可以支援各種螢幕大小,應用程式就能盡可能擁有更多使用者族群和適用裝置。

如果想盡可能支援各種螢幕大小,就需要讓應用程式版面配置採用回應式及調適性設計。無論螢幕大小為何,回應式/調適性版面配置都可以提供最佳的使用者體驗,讓應用程式能配合手機、平板電腦、折疊式裝置和 Chrome 作業系統裝置、直向和橫向,以及可調整大小的設定 (例如多視窗模式) 進行調整。

視窗大小類別

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

視窗大小類別會將應用程式能使用的顯示區域分類為「精簡」、「中等」及「展開」。可用的寬度和高度會另外分類,因此應用程式在任何時間點都會有兩種視窗大小類別:寬度類別和高度類別。一般來說,可用的寬度會比高度重要,這是因為直向捲動操作比較常見,因此寬度的視窗大小類別應該對應用程式使用者介面設定更有用。

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

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

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

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

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

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

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

97.59% 直向模式的手機

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

雖然有時使用實體裝置形式顯示大小類別會很有幫助,但是視窗大小類別明確表示不會以裝置螢幕的大小決定。視窗大小類別不適合使用 isTablet-type 邏輯。而是取決於應用程式可用的視窗大小,且不考量執行應用程式的裝置類型,因此有兩個重要結果:

  • 實體裝置不能保證特定的視窗大小類別。由於許多因素影響,應用程式能使用的螢幕空間和裝置畫面大小可能會不相同。行動裝置的分割畫面模式可能會讓兩個應用程式分割螢幕。在 Chrome OS 中,Android 應用程式能用可自由調整大小的任意形式視窗顯示。折疊式裝置可能會因為裝置折疊或未折疊,而分別存取兩種不同大小的螢幕。

  • 視窗大小類別可能在應用程式的生命週期發生變化。執行應用程式時,變更裝置螢幕方向、多工處理和折疊/未折疊都可能會影響可用的螢幕空間。因此,視窗大小類別是動態的,而應用程式的使用者介面應隨之調整。

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

以檢視畫面為基礎的應用程式應根據 Jetpack WindowManager 程式庫提供的目前視窗指標計算視窗大小類別。以下的檢視畫面 (Kotlin)檢視畫面 (Java) 範例程式碼可以示範如何根據中斷點計算視窗大小類別,並在有所變更時收取更新。

以 Compose 為基礎的應用程式應使用 material3-window-size-class 程式庫根據目前的視窗指標透過 calculateWindowSizeClass() 計算 WindowSizeClass

檢視畫面

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

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

        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MyApp(windowSizeClass)
        }
    }
}

在您的應用程式內遵守視窗大小類別後,即可根據目前視窗大小類別變更版面配置。

若想瞭解如何使用視窗大小類別製作回應式的版面配置,請見以下內容:

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

變更版面配置時,請記得測試所有視窗大小下的版面配置行為,特別是精簡、中等及展開等中斷點寬度。

如果現有的版面配置適用於精簡螢幕,請先針對展開寬度大小類別最佳化版面配置,因為這個大小有最多空間可供其他內容及使用者介面變更方式使用。然後再決定哪種版面配置適合中等寬度的大小類別。您可以考慮為中等寬度的螢幕大小新增特設版面配置。

您可以新增應用程式專用的功能,藉此改善使用者體驗,例如支援折疊式裝置手勢,或最佳化鍵盤、滑鼠及觸控筆輸入支援功能等等。

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

回應式設計

如果想支援多種裝置的板型規格,首先需要建立適合多種螢幕大小的回應式版面配置?

ConstraintLayout

若想建立回應式版面配置,最好的方法是使用 ConstraintLayout 做為使用者介面的基本版面配置。ConstraintLayout 可讓您根據與版面配置中其他檢視畫面之間的空間關係,指定每個檢視畫面的位置和大小。如此一來,當螢幕大小變更時,所有檢視畫面便可隨之移動並調整大小。

使用 ConstraintLayout 建立版面配置最簡單的方式,就是使用 Android Studio 中的版面配置編輯器。使用版面配置編輯器時,您可以將新的檢視畫面拖曳到版面配置內,並為父項及同層級檢視畫面套用相關限制,並設定檢視畫面屬性,完全不必手動編輯任何 XML。

圖 3. Android Studio 的版面配置編輯器,正在顯示 ConstraintLayout

詳情請參閱使用 ConstraintLayout 建構回應式使用者介面

回應式寬度和高度

為確保版面配置能夠回應不同螢幕大小,請用 wrap_contentmatch_parent0dp (match constraint) 設定大多數檢視畫面元件的寬度及高度,而不要使用硬式編碼數值:

  • wrap_content — 可讓檢視畫面調整大小,以配合檢視畫面中的內容。
  • match_parent — 使檢視畫面可以在父項檢視畫面範圍內盡可能展開。
  • 0dp (match constraint) — 在 ConstraintLayout 內,與 match_parent 相似。使檢視畫面可以按照檢視畫面限制的情況下使用所有可用空間。

例如:

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

圖 4 展示當裝置螢幕方向變更時,TextView 如何按照變更後的螢幕寬度調整寬度和高度。

圖 4. 回應式 TextView

TextView 設定為將寬度填滿所有可用空間 (match_parent),並將高度設定為和內含文字 (wrap_content) 必要高度的空間完全相同,如此一來,檢視畫面即可符合各種不同的螢幕大小及文字量。

如果您使用的是 LinearLayout,也可以透過版面配置權重來展開子檢視畫面,讓檢視畫面按照比例填滿可用空間。不過,在巢狀 LinearLayout 中使用權重時,系統必須執行多個版面配置票證,才能判斷每個檢視畫面的大小,而這會降低 UI 的效能。

ConstraintLayout 可以使用 LinearLayout 建立幾乎所有可以使用的版面配置,且不會影響效能,所以您應該嘗試將巢狀 LinearLayout 轉換為 ConstraintLayout。之後,您就可以使用限制鏈結來定義加權版面配置

調適性設計

應用程式版面配置應該要能夠因應各種螢幕大小進行調整。但是,就算是回應式版面配置,也不能為所有裝置使用者提供最佳的體驗。舉例來說,專為手機設計的使用者介面,可能無法在平板電腦上提供最佳的使用者體驗。調適性設計能夠提供針對不同螢幕尺寸最佳化的替代版面配置。

清單詳細資料使用者介面使用的 SlidingPaneLayout

一般來說,清單詳細資料使用者介面可以為各種大小的螢幕提供不同的使用者體驗。在大型螢幕上,清單和詳細資料窗格通常會並列在一起。選取清單內的項目後,詳細資料窗格便會顯示該項目的資訊,且不會改變使用者介面,這兩個窗格依然會繼續並列。不過,如果在小螢幕上,這兩個窗格就會分開顯示,各自佔滿整個顯示區域。選擇清單窗格內的鄉幕後,詳細資料窗格 (內有選取項目的資訊) 就會取代清單窗格。返回導覽會用清單取代詳細資料窗格。

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>

SlidingPaneLayout 內含有的這兩種檢視畫面屬性 layout_widthlayout_weight 將會決定 SlidingPaneLayout 的行為。在範例中,如果視窗夠大 (寬度至少達 580dp),足以顯示兩個檢視畫面,便會並排顯示窗格。但是如果視窗寬度小於 580dp,窗格會滑動到令一個窗格上,各自佔滿整個應用程式視窗。

如果視窗寬度大於指定的最低寬度總和 (580dp),則可使用 layout_weight 值按照比例調整兩個窗格的大小。在範例中,由於清單窗格並未設定權重,因此一律為 280dp。不過由於檢視畫面的 layout_weight,因此詳細資料窗格一律會填滿超過 580dp 的水平空間。

替代版面配置資源

為了讓您的使用者介面設計得以按照多種螢幕大小調整,請用以資源限定詞識別的替代版面配置。

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

在應用程式原始碼當中建立額外的 res/layout/目錄,即可提供針對螢幕調整的適應性版面配置。為每個需要不同版面配置的螢幕設定建立目錄。然後將螢幕設定限定詞附加至 layout 目錄名稱 (例如,假設是可用寬度為 600dp 的螢幕,附加的限定詞為 layout-w600dp)。

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

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

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

系統會複製預設版面配置檔案,並放到新的版面配置目錄內,讓您可以直接自訂該螢幕變化版本的版面配置。

最低寬度限定詞

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

Android 可以用 dp 測量方式描述螢幕大小,讓您得以為特定的螢幕尺寸建立版面配置,而不必擔心像素密度變化。

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

res/layout/main_activity.xml           # For phones (smaller than 600dp smallest width)
res/layout-sw600dp/main_activity.xml   # For 7” tablets (600dp wide or wider)

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

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

  • 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 為單位,這是因為系統計算像素密度 (而不是原始像素解析度) 之後剩餘的螢幕空間才是重點。

您用最小寬度等資源限定詞所指定的大小並非真正的螢幕大小,而是應用程式視窗可用的寬度或高度大小 (以 dp 為單位)。Android 系統可能會將部分螢幕空間用於系統使用者介面 (例如螢幕底端的系統列或頂端的狀態列),因此版面配置可能無法使用部分的螢幕空間。如果您的應用程式使用多視窗模式,則該應用程式只能存取含有該應用程式的視窗大小。如果重新調整視窗大小,則會以新的視窗大小觸發設定變更,以便系統指定適合的版面配置檔案。因此,您宣告的資源限定詞大小應只指定應用程式所需的空間即可。系統為版面配置提供空間時,會計算系統使用者介面所使用的所有空間。

可用寬度限定詞

建議您根據目前可用的寬度或高度來變更版面配置,而不要根據螢幕的最小寬度來變更版面配置。舉例來說,您可能想在當螢幕寬度達 600dp 以上時,使用兩個窗格的版面配置,但是寬度可能會隨著裝置橫向或直向的螢幕方向而有變化。在這種情況下,您應使用可用寬度限定詞,如下所示:

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

如果您的應用程式需要考慮可用高度,則可以使用可用高度限定詞。例如,layout-h600dp 適用於螢幕高度至少為 600dp 的螢幕。

螢幕方向限定詞

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

為此,您可以將 portland 限定詞新增到版面配置目錄名稱中,不過,您要確定螢幕方向限定詞位於尺寸限定詞後方。例如:

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

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

使用片段模組化使用者介面元件

針對多種螢幕大小設計應用程式時,請用片段將使用者介面邏輯擷取為不同的元件,藉此避免在不同活動之間複製使用者介面行為。然後,您可以組合片段,藉此在大型螢幕上建立多個窗格的應用程式,或是將片段置入小型螢幕的不同活動中。

舉例來說,清單詳細資料模式 (請見上文的 SlidingPaneLayout) 可用一個含有清單的片段,以及另一個含有清單項目詳細資料的片段進行實作。在大型螢幕上,片段可以並排顯示;而在小型螢幕上,則可以各自佔滿螢幕。

詳情請參閱片段總覽。

活動嵌入功能

如果應用程式內含多項活動,則可使用活動嵌入功能輕鬆建立適應性使用者介面。

活動嵌入功能可以在應用程式的任務視窗內顯示多項活動,或是同時執行相同活動的多個執行個體。在大型螢幕上,活動可以並排顯示;而在小型螢幕上,則可以堆疊在其他活動上方。

您可以建立 XML 設定檔,讓系統根據此設定檔判斷適合螢幕大小的顯示方式,藉此決定應用程式顯示活動的方法。您也可以進行 Jetpack WindowManager API 呼叫。

活動嵌入可支援裝置螢幕方向改變及折疊式裝置,並可隨著裝置旋轉、折疊及展開將活動堆疊與分拆。

詳情請參閱活動嵌入功能

螢幕大小和長寬比

請用各種螢幕大小和長寬比測試應用程式,確保使用者介面可以正確縮放。

Android 10 (API 級別 29) 以上版本支援多種長寬比設定。折疊式裝置的板型規格有很多種變化,從瘦長的螢幕 (如折疊後 21:9) 到方形的長寬比 (折疊後 1:1) 都有可能。

為了確保能夠相容於更多裝置,請盡可能用以下各種螢幕長寬比測試應用程式,越多越好。

圖 7.螢幕長寬比。

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

如果您無法取得每種想測試的螢幕大小的裝置,可用 Android Emulator 模擬幾乎所有螢幕大小。

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

支援特定螢幕大小

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

其他資源