建構自動調整式版面配置

應用程式的 UI 應配合不同的畫面大小、螢幕方向和板型規格做出回應。自動調整式版面配置會配合可用的畫面空間而變化。從簡單調整版面以填滿空間,到完全改變版面以運用額外空間,都屬於這類變化。

Jetpack Compose 是一種宣告式 UI 工具包,十分適合用來設計及導入可配合各種畫面大小自行調整算繪內容的版面配置。本文件針對如何運用 Compose 打造回應式 UI 提供了一些準則。

明確配合根組件大幅改變版面配置

使用 Compose 安排整個應用程式的版面時,根層級組件占滿應用程式可算繪的所有空間。在這個設計層級中,變更畫面的整體版面配置,以便利用較大的螢幕,是合理的做法。

避免使用實體、硬體數值來決定版面配置。您可能會想根據固定的實際資訊做決策 (該裝置是平板電腦嗎?實體螢幕有特定的長寬比嗎?),但這些問題的答案對於協助您判斷 UI 可以使用的空間,可能沒有幫助。

顯示數種不同裝置板型規格的圖表:手機、折疊式手機、平板電腦和筆記型電腦

在平板電腦上,應用程式可能會以多視窗模式運行,也就是說,應用程式可能會與另一個應用程式分別占用一部分的畫面。而在 Chrome OS 上,應用程式可能會在可調整大小的視窗中運行。裝置上甚至可能有多個實體螢幕,例如折疊式裝置。在這些情況下,實體螢幕大小就無法用來判斷如何顯示內容。

您必須改為根據實際分配給應用程式的畫面部分 (例如 Jetpack WindowManager 程式庫所提供的目前視窗指標) 做決定。如要瞭解如何在 Compose 應用程式中使用 WindowManager,請參考 JetNews 範例。

遵循這個方法,可以讓您的應用程式更加具有彈性,在上述所有情境中都能順利運作。讓您的版面配置自動調整配合可用畫面空間的做法,在支援 Chrome OS 這類平台,以及平板電腦和折疊式裝置等板型規格時,也能降低特殊處理的需求量。

在您觀察應用程式可用的相關空間時,建議您將原始大小轉換為有意義的大小類別,如視窗大小類別一節所述。這樣做可將各種大小分組到標準大小值區,這是一種中斷點,其設計目的是要讓您在最佳化應用程式時兼顧簡單與靈活,能滿足最獨特的情境需求。這些大小類別指的是應用程式的整體視窗,因此請利用這些類別來決定會影響整體畫面版面配置的決策。您可以將這些大小類別當成狀態向下傳遞,或者,也可以執行額外邏輯以建立衍生狀態,再向下傳遞到巢狀組件中。

enum class WindowSizeClass { Compact, Medium, Expanded }

@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass != WindowSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

這種分層做法可將螢幕大小邏輯限制在單一位置,而不會散布到應用程式需要保持同步的其他地方。這個單一位置會產生狀態,您可以將此狀態明確向下傳遞給其他組件,就像傳遞其他應用程式狀態一樣。明確傳遞狀態可簡化非根組件,因為這些組件會是一般可組合函式,伴隨其他資料攜帶著大小類別或指定的設定。

彈性巢狀組件可重複使用

組件若是可以放置在許多不同地方,可提升重複使用的可能性。如果組件預設一律放置在特定位置,採用特定大小,就較難在其他位置重複使用,或是搭配不同的可用空間大小。這也表示可重複使用的非根組件應避免仰賴「全域」大小資訊決定運作方式

舉個例子說明:想像一個巢狀組件導入了「清單 - 詳細資料」版面配置,畫面可能會顯示一個窗格或兩個並排的窗格。

應用程式並排顯兩個窗格的螢幕截圖

顯示一般清單/詳細資料版面配置的應用程式螢幕截圖。1 是清單區域,2 是詳細資料區域。

我們希望這項決策屬於應用程式整體版面配置的一部分,因此如前所述,我們會將決策從根層級組件向下傳遞:

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

如果我們想改為讓組件根據可用空間獨立變更版面配置,該怎麼做?例如資訊卡,我們希望如果空間允許,就顯示更多詳細資訊。我們想依據某些可用大小執行某些邏輯,但具體來說該指定哪個大小?

兩張不同的資訊卡範例:較窄的卡片只顯示圖示和標題,較寬的卡片顯示圖示、標題和簡短說明

如前所述,請避免使用裝置實際螢幕的大小,因為這種做法無法準確配合多螢幕裝置,此外,如果應用程式不是在全螢幕模式下運作,也會有問題。

由於組件並非根層級組件,為了盡量提升重複使用的可能性,我們也不應直接使用目前的視窗指標。如果該元件搭配邊框間距來放置 (例如用於插邊),或是有導覽邊欄或應用程式列之類的元件,則該組件的可用空間大小可能會與應用程式整體可用空間差異極大。

因此,我們應使用實際為組件指定的寬度來算繪組件本身。有兩種方法可以取得這項寬度資訊:

如要變更顯示內容的位置方式,可以使用一系列的修飾符或自訂版面配置將版面配置設為回應式。這可以簡單得像拿一些子項填滿所有可用空間,或是在有足夠空間的情況下,以多個資料欄擺放子項。

如要變更顯示的內容,可以使用 BoxWithConstraints,這是更有效的替代功能。這個組件提供成效評估限制,可讓您用來根據可用空間呼叫不同的組件。不過,這伴隨著一些代價,BoxWithConstraints 會將組成作業延遲到版面配置階段 (此時會知道有哪些限制),導致版面配置期間必須執行更多作業。

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

確保各種不同大小都能取得所有資料

在運用額外螢幕空間時,比起小型螢幕,大型螢幕可能會有空間向使用者顯示更多內容。導入這種行為的組件時,您可能會想提高效率,並載入資料做為目前大小的副作用。

不過,這樣做與單向資料流原則牴觸,在單向資料流中,資料可以提升並單純提供給組件,以便適當進行算繪。必須為組件提供足夠資料,讓組件隨時都具備在任何大小畫面上顯示內容所需的材料,即使其中部分資料可能不會每次都用到。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

Card 範例為基礎,請注意,我們一律會將 description 傳遞至 Card。雖然 description 只有在寬度允許顯示時才會使用,但無論可用寬度為何,Card 一律會要求這個項目。

一律傳遞資料的做法,會減少自動調整式版面配置有狀態的程度,使其變得較簡單,避免在切換不同大小時觸發副作用 (這可能會發生在重新調整視窗大小、螢幕方向改變,或折疊及展開裝置時)。

這個原則也讓您可以在版面配置發生變化時保留狀態。有些資訊可能不會在所有大小中都用到,將這類資訊提升後,我們就能在版面配置大小發生變化時,繼續保留使用者的狀態。舉例來說,我們可以提升 showMore 布林值標記,這樣一來,在畫面大小有所調整,導致版面配置在隱藏和顯示說明之間切換時,使用者的狀態就能保留下來。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

瞭解詳情

如要進一步瞭解 Compose 中的自訂版面配置,請參閱以下其他資源。

範例應用程式

  • JetNews。展示如何設計可調整 UI 的應用程式,以便運用可用空間

影片