Compose 的基本版面配置

1. 簡介

Compose 是 UI 工具包,可讓您輕鬆實現應用程式設計。您可以說明希望 UI 呈現的效果,而 Compose 會負責在螢幕上繪製出來。本程式碼研究室會說明如何編寫 Compose UI。本文假設您已瞭解基本程式碼研究室中說明的概念,因此請務必先完成此程式碼研究室。在基本概念程式碼研究室中,您已瞭解如何使用 SurfacesRowsColumns 實作簡單的版面配置。此外,您還使用 paddingfillMaxWidthsize 等修飾符來擴充這些版面配置。

在本程式碼研究室中,您將實作更真實複雜的版面配置,並在過程中瞭解各種立即可用的可組合項修飾符。完成本程式碼研究室後,您應能將基本應用程式的設計轉換成可運作的程式碼。

本程式碼研究室不會在應用程式中新增任何實際行為。如要瞭解狀態和互動,請改為完成有關 Compose 狀態的程式碼研究室

如果您在閱讀本程式碼研究室時需要更多支援,請觀看下面的「一起寫程式」示範影片:

課程內容

在本程式碼研究室,您將學到:

  • 修飾符如何協助您擴增可組合項。
  • Column 和 LazyRow 等標準版面配置元件如何置放子項可組合項。
  • 對齊和排列方式會如何變更子項可組合項在上層元件中的位置。
  • Scaffold 和底部導覽列等 Material 可組合函式如何協助您建立全面的版面配置。
  • 如何使用 Slot API 建構彈性的可組合函式。
  • 如何針對不同的螢幕設定建構版面配置。

軟硬體需求

建構項目

在本程式碼研究室中,您將根據設計人員提供的模擬程式碼,實現真實的應用程式設計。MySoothe 是一款身心健康的應用程式,其中列出多種可以改善身心的方法。這個應用程式包含兩個部分,一個列出使用者最愛的收藏,另一個則顯示各種體能運動。應用程式應如下所示:

應用程式的直向版本

應用程式的橫向版本

2. 開始設定

在這個步驟中,您將下載包含主題設定和一些基本設定的程式碼。

取得程式碼

您可以在 codelab-android-compose GitHub 存放區中找到本程式碼研究室的程式碼。如要複製該存放區,請執行下列命令:

$ git clone https://github.com/android/codelab-android-compose

或者,您也可以下載兩個 ZIP 檔案:

查看程式碼

下載的程式碼包含所有可用 Compose 程式碼研究室的程式碼。如要完成本程式碼研究室,請在 Android Studio 中開啟 BasicLayoutsCodelab 專案。

建議您先從 main 分支版本的程式碼著手,依自己的步調逐步完成本程式碼研究室。

3. 預先規畫

我們會從實作應用程式的直向設計著手,以下將進一步說明:

直向設計

當系統要求您實作設計時,最好先瞭解其結構。不要直接開始編寫程式碼,而是先分析設計本身。試著思考看看:如何將這個 UI 分成多個可重複使用的部分

一起來設計一下。而在最高抽象層,我們可以將這個設計分成兩個部分:

  • 螢幕的內容。
  • 底部導覽。

應用程式設計分析

往下一層,畫面內容包含三個子部分:

  • 搜尋列。
  • 名為「Align your body」的區段。
  • 名為「Favorite collections」的部分。

應用程式設計分析

每個部分也會列出重複使用的低階元件:

  • 顯示在水平捲軸的「align your body」元素。

「align your body」元素

  • 「Favorite Collection」資訊卡,顯示在水平捲動的格狀檢視中。

「favorite collection」資訊卡

現在您已完成設計分析,可以開始為 UI 的各個可辨識元件實作可組合函式了!先從最低級別的可組合項開始,然後繼續合併以形成較複雜的可組合項。完成程式碼研究室後,您的新應用程式看起來會像是提供的設計。

4. 搜尋列 - 修飾符

要轉換成可組合項的第一個元素是搜尋列。接著將說明設計:

搜尋列

單憑這張螢幕截圖,很難將設計精準地完美實作。一般來說,設計人員可以傳達設計的更多詳細資訊。允許您使用他們的設計工具,或分享所謂的紅線設計。在本例子中,我們的設計人員做出了紅線設計,以用來判斷任何尺寸相關的數值。系統會以 8dp 格線重疊方式顯示設計內容,方便您查看元素之間的空間大小。此外,我們也會明確加入一些間距,清楚呈現特定大小。

搜尋列的紅線設計

您可以看到搜尋列的高度應為 56 密度獨立像素。此外,搜尋列應填滿父項的完整寬度。

如要導入搜尋列,請使用名為「文字欄位」的 Material 元件。Compose Material 程式庫包含名為 TextField 的可組合函式,就是這個 Material 元件的實作。

請先從基本的 TextField 實作著手。在程式碼集中開啟 MainActivity.kt,並搜尋 SearchBar 可組合項。

在名為 SearchBar 的可組合元件中,編寫基本 TextField 實作:

import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
   )
}

注意事項:

  • 您已對文字欄位值進行硬式編碼,onValueChange 回呼則不會執行任何動作。由於這是以版面配置為主的程式碼研究室,因此您會忽略與狀態相關的所有行為。
  • SearchBar 可組合函式接受 modifier 參數,並傳送至 TextField。這是根據「Compose 指南」的最佳做法。如此一來,這個方法的呼叫端就會修改可組合項的外觀與風格,讓可組合項更具彈性,而且可重複使用。您將針對本程式碼研究室中的所有可組合項,繼續採用這項最佳做法。

我們來看看這個可組合函式的預覽畫面。在此提醒,您可以使用 Android Studio 中的預覽功能,快速執行個別可組合項的疊代作業。MainActivity.kt 包含多個預覽內容,分別對應至您將在這個程式碼研究室中建構的每個可組合項。在此情況下,方法 SearchBarPreview 會算繪 SearchBar 的可組合項,帶有一些背景和邊框間距,以提供更多背景資訊。新增實作後看起來會像這樣:

搜尋列預覽畫面

不過,還少了一些東西。首先,讓我們使用修飾符修正可組合項的大小。

編寫可組合項時,您可以使用修飾符執行下列操作:

  • 變更可組合項的大小、版面配置、行為和外觀。
  • 新增資訊,例如無障礙標籤。
  • 處理使用者輸入內容
  • 新增高等級互動,例如讓元素可供點選、可捲動、可拖曳或可縮放。

您呼叫的每個可組合函式都有 modifier 參數,您可設定參數,調整該可組合函式的外觀、風格和行為。設定修飾符時,您可以鏈結多個修飾符方法,打造更複雜的調整方式。

在這種情況下,搜尋列的高度至少為 56dp,且會填滿其父項寬度。如要找出適用此項目的修飾符,請參閱「修飾符清單」,並參閱「大小部分」。如果是高度,您可以使用 heightIn 修飾符。確保可組合項有特定的最小高度。但要是使用者放大了系統字型大小,高度便會隨之變高。如果是寬度,您可以使用 fillMaxWidth 修飾符。這個修飾符可確保搜尋列填滿其父項的所有水平空間。

請更新修飾符,使其符合以下程式碼:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

在此情況下,由於其中一個修飾符會影響寬度,而另一個修飾符會影響高度,所以這些修飾符的順序並不重要。

您也必須設定 TextField 的部分參數。試著設定參數值,讓可組合項看起來像設計。以下再次提供設計讓您參考:

搜尋列

請按照下列步驟更新實作:

  • 新增搜尋圖示。TextField 包含可接受其他可組合項的參數 leadingIcon。您可以在內部設定 Icon,在本範例中應為 Search 圖示。請務必使用正確的 Compose Icon 匯入功能。
  • 您可以使用 TextFieldDefaults.textFieldColors 覆寫特定顏色。請將文字欄位的 focusedContainerColorunfocusedContainerColor 設為 MaterialTheme 的 surface 顏色。
  • 新增預留位置文字「Search」(以字串資源 R.string.placeholder_search 的形式顯示)。

完成後,可組合函式看起來應像這樣:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       leadingIcon = {
           Icon(
               imageVector = Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.colors(
           unfocusedContainerColor = MaterialTheme.colorScheme.surface,
           focusedContainerColor = MaterialTheme.colorScheme.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

搜尋列

請注意:

  • 您已新增顯示搜尋圖示的 leadingIcon。這個圖示不需要內容說明,因為文字欄位的預留位置已說明文字欄位的含義。提醒您,內容說明通常用於無障礙用途,並為使用者提供應用程式的圖片或圖示的文字說明。
  • 如要調整文字欄位的背景顏色,請設定 colors 屬性。可組合項包含一合併參數,而非個別顏色有不同參數。這裡您傳入了 TextFieldDefaults 資料類別的副本,因此只更新不同的顏色。在本例中,即僅更新 unfocusedContainerColorfocusedContainerColor 顏色。

在這個步驟中,您已瞭解如何使用可組合的參數和修飾符來變更可組合函式的外觀和風格。這適用於 Compose 和 Material 程式庫提供的可組合項,以及您自行編寫的可組合項。您應一律提供參數來自訂撰寫的可組合項。建議您一併新增 modifier 屬性,以便從外部調整可組合的外觀和風格。

5. 調整身體狀態 - 對齊

您將實作的下一個可組合項為「調整身體狀態」元素。我們來看看該元素的設計,包括它旁邊的紅線設計:

「align your body」元件

「align your body」的紅線設計

紅線設計現在也包括以基準線為標準的間距。我們從中取得的資訊如下:

  • 圖片高度應為 88dp。
  • 文字的基準線與圖片之間的間距應為 24dp。
  • 基準線和元素底部之間的間距應為 8dp。
  • 文字應採用 bodyMedium 字體排版樣式。

如要實作這個可組合函式,您需要有 ImageText 可組合函式。它們必須包含在 Column 中,因此位於彼此下方。

請在程式碼中找出 AlignYourBodyElement 可組合項,並根據這個基本實作更新可組合項內容:

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

請注意:

  • 將圖像的 contentDescription 設為空值,因為此圖片只是裝飾性。圖片下方的文字足以充分描述意義,因此不需要為圖片提供額外的說明。
  • 您目前使用硬式編碼的圖片和文字。在下一個步驟中,您將使用 AlignYourBodyElement 可組合項中提供的參數來動態調整。

請看看這個可組合函式的預覽畫面:

「align your body」預覽畫面

不過,還有幾個地方需要改善。最值得注意的是,圖片太大且形狀並非圓形。您可以使用 sizeclip 修飾符和 contentScale 參數來調整 Image 可組合項。

與上一個步驟中的 fillMaxWidthheightIn 修飾符類似,size 修飾符會調整可組合函式,使其符合特定大小。clip 修飾符的運作方式不同,而且會調整可組合函式的外觀。您可以將修飾符設為任何 Shape,讓它依據該形狀裁剪可組合函式的內容。

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

現在,設計的預覽畫面看起來會像這樣:

「align your body」預覽畫面

圖片也需要正確縮放。為此,我們可以使用 ImagecontentScale 參數。選項主要包括:

「align your body」內容預覽畫面

在本例中,裁剪類型是我們要用的正確類型。套用修飾符和參數後,程式碼應如下所示:

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text( text = stringResource(R.string.ab1_inversions) )
   }
}

您的設計現在看起來應像這樣:

「align your body」預覽畫面

下一步是設定 Column 的對齊方式,這裡我們要水平對齊文字。

一般來說,如要對齊上層容器中的可組合項,請設定該父項容器的對齊方式。因此,與其讓子項自行定位父項的位置,不如告訴父項如何對齊子項。

針對 Column,您可以決定其子項的水平對齊方式。可採用的選項包括:

  • 開始
  • CenterHorizontally
  • 結尾

針對 Row,您需要設定垂直對齊。選項與 Column 中的選項類似:

  • 正上方
  • CenterVertically
  • 底部

針對 Box,您可以合併水平和垂直對齊方式。可採用的選項包括:

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • 置中
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

容器的所有子項都會採用相同的對齊模式。您可以在子項中新增 align 修飾符,藉此覆寫單一子項的行為。

在這項設計中,文字應水平置中。方法是將 ColumnhorizontalAlignment 設為水平置中:

import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = modifier
   ) {
       Image(
           //..
       )
       Text(
           //..
       )
   }
}

實作這些部分後,您只需要進行幾項小幅變更,讓可組合項與設計保持一致。如果遇到問題,可以嘗試自行實作程式碼,也可以參照最終程式碼。建議您採取下列步驟:

  • 讓圖片和文字保持動態。並將其做為引數傳遞至可組合函式。別忘了更新對應的預覽畫面,並傳入一些硬式編碼資料。
  • 更新文字,使用 bodyMedium 字體排版樣式。
  • 更新每個圖表中,文字元素的基準間距。

「align your body」的紅線設計

完成這些步驟後,您的程式碼應如下所示:

import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
           style = MaterialTheme.typography.bodyMedium
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

現在看看「設計」分頁中的 AlignYourBodyElement 吧!

「align your body」預覽畫面

6. 「最愛的收藏」資訊卡 - Material Surface

下一個可組合項的實作方式與「調整身體狀態」元素的方式類似。設計如下 (包括紅線):

「favorite collection」資訊卡

「favorite collection」資訊卡的紅線設計

此範例提供了可組合函式的完整尺寸。您可以看到文字應採用 titleMedium。

這個容器會使用 surfaceVariant 做為其背景顏色,與整個螢幕的背景不同。此外,該容器也具有圓角。我們要使用 Material 的 Surface 可組合函式,為「favorite collection」資訊卡指定這些屬性。

您可以根據自己的需求設定 Surface 的參數和修飾符。在本範例中,途徑應加上圓角。這時可以使用 shape 參數。以使用 Material 主題中的值,取代上一步將圖片形狀設定為 Shape

其外觀如下:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null
           )
           Text(text = stringResource(R.string.fc2_nature_meditations))
       }
   }
}

我們來看看此實作的預覽:

「favorite collection」預覽畫面

接下來,將上一步中學到的知識學以致用。

  • 設定 Row 的寬度,並垂直對齊相關子項。
  • 設定每個圖表的圖片大小,並在容器中裁剪。

「favorite collection」紅線設計

建議您先自行實作這些變更,再查看解決方案程式碼!

程式碼現在大致如下所示:

import androidx.compose.foundation.layout.width

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

預覽現在應如下所示:

「favorite collection」預覽畫面

如要完成這個可組合函式,請按照下列步驟操作:

  • 讓圖片和文字保持動態。並將其做為引數傳遞至可組合函式。
  • 將顏色更新為 surfaceVariant。
  • 更新文字,使用 titleMedium 字體排版樣式。
  • 更新圖片和文字之間的間距。

最終結果應如下所示:

@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       color = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.titleMedium,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

現在來看看 FavoriteCollectionCardPreview 的預覽畫面吧!

「favorite collection」預覽畫面

7. 「調整身體狀態」列 - 排列

您已建立畫面上顯示的基本可組合項,接著就可以開始建立畫面的不同部分。

從「Align your body」可捲動列開始。

「align your body」可捲動列

以下是這項元件的紅線設計:

「align your body」的紅線設計

請記得,一個格狀區塊代表 8dp。因此在本設計中,資料列的第一個項目之前和最後一個項目之後有 16dp 的空間。每個項目之間留有 8dp 的間距。

在 Compose 中,您可以使用 LazyRow 可組合項,實作這類可捲動的列。清單說明文件 包含關於 Lazy 清單的詳細資訊 (例如 LazyRowLazyColumn)。在本程式碼研究室中,請務必注意 LazyRow 只會轉譯在螢幕上顯示的元素,而非同時顯示所有元素,這有助於維持應用程式效能。

請先從這個 LazyRow 的基本實作著手:

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

如您所見,LazyRow 的子項不是可組合項。請改用 Lazy 清單 DSL,以提供 itemitems 等方法,發出可組合項做為清單項目。針對提供的 alignYourBodyData 中的每個項目,您可以發出先前實作的 AlignYourBodyElement 可組合項。

請注意這將如何展示:

「align your body」預覽畫面

但在紅線的設計中,依然缺少間距。如要實作這些項目,您必須瞭解安排

在上一個步驟中,您學到了對齊的對齊方式,可用於對齊交叉軸容器的子項。Column 的交叉軸是水平軸,Row 的時候交叉軸則是垂直軸。

不過,我們也可以決定如何將子項可組合項置於容器的主軸 (水平為 Row,垂直為 Column)。

針對 Row,您可以選擇下列排列方式:

資料列排列方式

若是 Column

資料欄排列方式

除了這些排列方式之外,您也可以使用 Arrangement.spacedBy() 方法,在每個可組合子項之間新增固定間距。

在這個範例中,如果您想 LazyRow 中每個項目之間留有 8dp 的間距,則必須使用 spacedBy 方法。

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

現在,設計的外觀如下所示:

「align your body」預覽畫面

您也必須在 LazyRow 的側邊新增邊框間距。在本例中,即使加入簡單的邊框間距修飾符,也不一定能發揮作用。請嘗試在 LazyRow 中加入邊框間距,然後透過互動式預覽功能查看其運作方式:

「align your body」的紅線設計

如您所見,在捲動畫面時,第一個和最後一個可見項目會在螢幕兩側遭到截斷。

為了保持相同的邊框間距,並確保在父項清單的邊界內捲動內容時不會截斷內容,所有清單都會提供名為 contentPadding 的參數給 LazyRow,並將其設為 16.dp

import androidx.compose.foundation.layout.PaddingValues

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

現在,不妨試用互動式預覽功能,查看邊框間距帶來的差異。

「align your body」可捲動列

8. 「最愛的收藏」格線 - Lazy 格線

下一個要導入的部分是畫面中的「最愛的收藏」部分。這個可組合函式需要格狀版面,而非單列版面:

「favorite collections」捲動中

您可以採用與上一個部分類似的方式實作這個部分,做法是建立 LazyRow,並讓每個項目保留包含兩個 FavoriteCollectionCard 例項的 Column。不過,在這個步驟中,您將使用 LazyHorizontalGrid,這種做法可以更妥善地將項目對應至格狀版面元素。

請先實作包含兩個固定列的簡單格狀版面:

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

如您所見,只要將上一個步驟中的 LazyRow 替換成 LazyHorizontalGrid 即可。不過,目前無法提供正確的搜尋結果:

「favorite collections」預覽畫面

格狀空間會佔據父項空間,也就是說,「favorite collections」資訊卡已垂直延伸太多。

請調整可組合函式,達成以下效果:

  • 格狀版面的水平 contentPadding 為 16dp。
  • 水平和垂直排列的間距為 16dp。
  • 格狀版面的高度為 168dp。
  • FavoriteCollectionCard 的修飾符指定高度為 80dp。

最終程式碼應如下所示:

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(16.dp),
       verticalArrangement = Arrangement.spacedBy(16.dp),
       modifier = modifier.height(168.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
       }
   }
}

預覽畫面看起來應像這樣:

「favorite collections」預覽畫面

9. 首頁區段 - Slot API

MySoothe 主畫面有多個區段遵循相同的模式。每個部分都有標題,但某些內容會不一樣。我們想要實作的紅線設計如下:

主畫面部分的紅線設計

如您所見,每個部分都有標題版位。標題包含一些相關的間距和樣式資訊。視區段而定,版位可能會以不同內容動態填入。

如要實作這個彈性區段容器,請使用「Slot API」。開始實作前,請先參閱說明文件頁面的「以版位為基礎的版面配置」一節。這將有助於瞭解什麼是以版位為基礎的版面配置,以及如何使用 Slot API 建立這類版面配置。

調整 HomeSection 可組合項以接收標題和版位內容。您也應調整相關聯的預覽,以呼叫具有「調整身體狀態」標題和內容的 HomeSection

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

您可以將 content 參數用於可組合項的版位。這樣一來,使用 HomeSection 可組合項時,就能使用結尾的 lambda 填滿內容版位。當可組合項提供多個可填入的版位時,您可在大型可組合容器中為其取可代表其函式意義的名稱。舉例來說,Material 的 TopAppBar 提供 titlenavigationIconactions 的版位。

以下是此部分採用上述實作方式後的效果:

主畫面部分的預覽畫面

不過,Text 可組合函式需要更多資訊,才能與設計保持一致。

主畫面部分的紅線設計

更新程式碼,以便:

  • 使用 titleMedium 字體排版。
  • 文字基準線與頂端之間的間距為 40dp。
  • 基準線和元素底部之間的間距為 16dp。
  • 水平邊框間距為 16dp。

最終解決方案應如下所示:

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title),
           style = MaterialTheme.typography.titleMedium,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

10. 主畫面 - 捲動頁面

現在,您已分別建立了不同的建構區塊,可以將其合併為全螢幕實作。

以下是您嘗試實作的設計:

主畫面部分的紅線設計

我們要做的只是將搜尋列和這兩個部分放在一起。您必須加入一些間距,才能確保一切適合目前的設計。我們從未使用過的可組合項為 Spacer,這有助於我們在 Column 中納入額外的空間。如果您改為設定 Column 的邊框間距,系統會提供先前在「最愛的收藏」集合標題中相同的截斷行為。

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

雖然這個設計適合大多數裝置尺寸,但在裝置螢幕空間不足 (例如在橫向模式下) 時,應該要能垂直捲動畫面。您必須新增捲動行為。

如先前所述,LazyRowLazyHorizontalGrid 等 Lazy 版面配置會自動新增捲動行為。不過,您不一定需要 Lazy 版面配置。一般來說,當清單含有許多元素,或需要載入龐大的資料集時,您會使用 Lazy 版面配置。因此,如果一次發送所有項目,就會導致效能降低,並減慢應用程式的執行速度。如果清單僅包含少量元素,您可以改為使用簡單的 ColumnRow,然後手動新增捲動行為。方法是使用 verticalScrollhorizontalScroll 修飾符。需要 ScrollState,其中包含捲動目前的狀態,以便從外部修改捲動狀態。在這種情況下,您無需修改捲動狀態,只需使用 rememberScrollState 建立持續的 ScrollState 執行個體即可。

最終結果應如下所示:

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
   ) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

如要驗證可組合函式的捲動行為,請限制預覽畫面的高度,並透過互動式預覽功能執行:

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

捲動螢幕畫面內容

11. 底部導覽 - Material

由於您已實作螢幕畫面的內容,現在可以新增視窗裝飾了。以 MySoothe 為例,導覽列可讓使用者切換不同畫面。

首先,請實作導覽列可組合函式,然後將其納入應用程式中。

讓我們來看看設計:

底部導覽列設計

所幸,您並不需要自己從頭開始實作整個可組合函式。您可以使用 Compose Material 程式庫中的 NavigationBar 可組合函式。在 NavigationBar 可組合函式中,您可以新增一或多個 NavigationBarItem 元素,隨後 Material 程式庫會自動為其套用樣式。

請先從底部導覽列的基本實作著手:

import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_home)
               )
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_profile)
               )
           },
           selected = false,
           onClick = {}
       )
   }
}

基本實作看起來會像這樣 (內容顏色與導覽列顏色沒有明顯對比):

底部導覽列的預覽畫面

因此,有一些樣式應該調整。首先,您可以設定底部導覽列的 containerColor 參數,更新該導覽列的背景顏色。為此,您可以使用 Material Design 主題中的 surfaceVariant 顏色。最終解決方案應如下所示:

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       containerColor = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

現在導覽列看起來應類似於下圖,請注意此設定如何呈現出較高的對比度。

底部導覽列設計

12. MySoothe 應用程式 - Scaffold

在這一步中,我們要建立包括底部導覽列的全螢幕實作項目。使用 Material 的 Scaffold 可組合函式。Scaffold 針對導入 Material 設計的應用程式,提供頂層可設定的可組合函式。其中包含各種 Material 概念的版位,其中一個版位為底部列。您可以將上一步建立的底部導覽可組合函式放入這個底部列中。

接著,實作 MySootheAppPortrait() 可組合函式。這是應用程式的頂層可組合函式,因此您應該:

  • 套用 MySootheTheme 質感設計主題。
  • 加入 Scaffold
  • 將底部列設為 SootheBottomNavigation 可組合項。
  • 將內容設為 HomeScreen 可組合項。

最終結果應為:

import androidx.compose.material3.Scaffold

@Composable
fun MySootheAppPortrait() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

您的實作程序已經完成!如要查看您實作的版本能否讓像素完美呈現,您可以將此圖與自己的預覽實作畫面比較。

MySoothe 實作項目

13. 導覽邊欄 - Material

建立應用程式的版面配置時,您也需留意在多種手機設定 (包括橫向模式) 下呈現的效果。以下是橫向模式的應用程式設計,請留意底部導覽列如何變成畫面內容左側的邊欄。

橫向設計

如要實作此設計,請使用 Compose Material 程式庫中的 NavigationRail 可組合函式,其實作方式與用於建立底部導覽列的 NavigationBar 類似。在 NavigationRail 可組合函式中,您將為「主畫面」和「個人資料」新增 NavigationRailItem 元素。

底部導覽列設計

我們先從導覽邊欄的基本實作著手。

import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
   ) {
       Column(
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )

           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

導覽邊欄的預覽畫面

因此,有一些樣式應該調整。

  • 在邊欄的開頭和結尾加入 8dp 的邊框間距。
  • 為此,請使用 Material Design 主題中的背景顏色設定導覽邊欄的 containerColor 參數,更新邊欄的背景顏色。設定背景顏色後,圖示和文字的顏色就會自動調整為主題的 onBackground 顏色。
  • 資料欄應填滿畫面的最大高度。
  • 將資料欄的垂直排列方式設為置中。
  • 將資料欄的水平對齊方式設為水平置中。
  • 在兩個圖示之間加入 8dp 的邊框間距。

最終解決方案應如下所示:

import androidx.compose.foundation.layout.fillMaxHeight

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
       modifier = modifier.padding(start = 8.dp, end = 8.dp),
       containerColor = MaterialTheme.colorScheme.background,
   ) {
       Column(
           modifier = modifier.fillMaxHeight(),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )
           Spacer(modifier = Modifier.height(8.dp))
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

導覽邊欄設計

現在,我們將導覽邊欄加到橫向版面配置中。

橫向設計

對於應用程式的直向版本,您使用的是 Scaffold。不過,如果是橫向,您需使用 Row,將導覽邊欄和螢幕畫面內容並排放置。

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Row {
           SootheNavigationRail()
           HomeScreen()
       }
   }
}

您在直向版本中使用 Scaffold 時,Scaffold 也會為您將內容顏色設為背景。如要設定導覽邊的顏色,請將 Row 納入 Surface 中,並將其設為背景顏色。

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
               SootheNavigationRail()
               HomeScreen()
           }
       }
   }
}

橫向預覽畫面

14. MySoothe 應用程式 - 視窗大小

現在橫向模式的預覽效果十分不錯。但是,如果將在裝置或模擬器上執行的應用程式轉向一側,系統並不會顯示橫向版本。這是因為我們需要告知應用程式何時顯示哪個應用程式設定。為此,請使用 calculateWindowSizeClass() 函式查看手機目前所用的設定。

視窗大小圖表

視窗大小類別共有三種寬度:精簡、中等和展開。應用程式處於直向模式時,會採用精簡寬度,處於橫向模式時,則採用展開寬度。在本程式碼研究室中,我們不會用到中等寬度。

在 MySootheApp 可組合函式中,更新應用程式以採用裝置的 WindowSizeClass。如果採用精簡寬度,請傳入應用程式的直向版本。如果為橫向模式,則請傳入應用程式的橫向版本。

import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

setContent() 中,建立名為 windowSizeClass 的 val,然後將其設為 calculateWindowSize()並傳遞到 MySootheApp() 中。

由於 calculateWindowSize() 仍處於實驗階段,因此您需選擇加入 ExperimentalMaterial3WindowSizeClassApi 類別。

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MainActivity : ComponentActivity() {
   @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val windowSizeClass = calculateWindowSizeClass(this)
           MySootheApp(windowSizeClass)
       }
   }
}

現在,在模擬器或裝置上執行應用程式,觀察螢幕內容在旋轉時如何變化。

應用程式的直向版本

應用程式的橫向版本

15. 恭喜

恭喜!您已成功完成本程式碼研究室,並進一步瞭解 Compose 中的版面配置。透過實作真實世界的設計,您已瞭解修飾符、對齊、排列、Lazy 版面配置、Slot API、捲動、Material Design 元件,以及特定版面配置的設計。

請參閱 Compose 課程中的其他程式碼研究室。並查看程式碼範例

說明文件

如需有關這些主題的更多資訊和指南,請參閱以下說明文件: