使用 Jetpack Compose 進行 Material Design 主題設定

1. 事前準備

Material Design 是 Google 設計/開發人員打造及維護的設計系統,可協助您建構優質的 Android 及其他行動裝置與網路平台數位體驗。這套系統提供指南,協助您瞭解如何建構應用程式 UI,使其具備可讀性、吸引力和一致性。

在本程式碼研究室中,您將瞭解可協助您在應用程式中使用 Material Design 的 Material Design 主題設定,以及自訂顏色、字體排版和形狀等指引。您可以視需求自訂應用程式,範圍不限。您也將瞭解如何加入頂端應用程式列,以顯示應用程式的名稱和圖示。

必要條件

  • 熟悉 Kotlin 語言,包括語法、函式和變數。
  • 可在 Compose 中建構版面配置,包括有邊框間距的列和欄。
  • 能夠在 Compose 中建立簡單的清單。

課程內容

  • 如何在 Compose 應用程式中套用 Material Design 主題設定。
  • 如何在應用程式中加入自訂調色盤。
  • 如何在應用程式中加入自訂字型。
  • 如何在應用程式的元素中加入自訂形狀。
  • 如何在應用程式中加入頂端應用程式列。

建構項目

  • 您將建構一個結合 Material Design 最佳做法的精美應用程式。

軟硬體需求

  • 最新版 Android Studio。
  • 可下載範例程式碼和字型的網際網路連線。

2. 應用程式總覽

在本程式碼研究室中,您將建立 Woof 應用程式。此應用程式可顯示犬隻清單,並利用 Material Design 打造良好的應用程式體驗。

92eca92f64b029cf.png

透過這個程式碼研究室,我們將向您說明使用 Material Design 主題設定可以完成哪些操作。您可以瞭解如何使用 Material Design 主題設定,強化日後建立的應用程式外觀和風格。

調色盤

以下是我們將建立的淺色和深色主題調色盤。

這張圖片內含 Woof 應用程式的淺色配置。

這張圖片內含 Woof 應用程式的深色配置。

以下分別是套用淺色和深色主題的應用程式成品。

淺色主題

深色主題

字體排版

以下是您將在應用程式中使用的類型樣式。

8ea685b3871d5ffc.png

主題檔案

Theme.kt 檔案用以存放應用程式主題的所有資訊,檔案包含顏色、字體排版和形狀。這是您需特別瞭解的重要檔案。檔案內有可組合函式 WoofTheme(),可用於設定應用程式的顏色、字體排版和形狀。

@Composable
fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColors
        else -> LightColors
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            setUpEdgeToEdge(view, darkTheme)
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        shapes = Shapes,
        typography = Typography,
        content = content
    )
}

/**
 * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
 * light or dark depending on whether the [darkTheme] is enabled or not.
 */
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
    val window = (view.context as Activity).window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.statusBarColor = Color.Transparent.toArgb()
    val navigationBarColor = when {
        Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
        Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
        // Min sdk version for this app is 24, this block is for SDK versions 24 and 25
        else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
    }
    window.navigationBarColor = navigationBarColor
    val controller = WindowCompat.getInsetsController(window, view)
    controller.isAppearanceLightStatusBars = !darkTheme
    controller.isAppearanceLightNavigationBars = !darkTheme
}

MainActivity.kt 也加入了 WoofTheme(),藉此提供整個應用程式的 Material Design 主題設定。

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           WoofTheme {
               Surface(
                   modifier = Modifier.fillMaxSize()
               ) {
                   WoofApp()
               }
           }
       }
   }
}

看一下 WoofPreview()。您可以發現加入 WoofTheme() 後,即可在 WoofPreview() 中提供顯示的 Material Design 主題設定。

@Preview
@Composable
fun WoofPreview() {
    WoofTheme(darkTheme = false) {
        WoofApp()
    }
}

3. 取得範例程式碼

如要開始使用,請先下載範例程式碼:

或者,您也可以複製 GitHub 存放區的程式碼:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout starter

您可以瀏覽 Woof app GitHub 存放區中的程式碼。

探索範例程式碼

  1. 在 Android Studio 中開啟範例程式碼。
  2. 開啟「com.example.woof」>「data」>「Dog.kt」。這包含將用於代表犬隻相片、名稱、年齡和興趣的 Dog data class。其中也包含犬隻清單,以及要做為應用程式資料使用的資訊。
  3. 依序開啟「res」>「drawable」。其中包含此專案所需要的所有圖片素材資源,包括應用程式圖示、犬隻圖片和圖示。
  4. 依序開啟「res」>「values」>「strings.xml」。其中包含在此應用程式中使用的字串,包括應用程式名稱、犬隻名稱和說明等。
  5. 開啟「MainActivity.kt」。此項目包含建立簡易清單的程式碼,可顯示犬隻相片、犬隻名稱和犬隻年齡。
  6. WoofApp() 包含可顯示 DogItemLazyColumn
  7. DogItem() 包含可顯示犬隻相片及其相關資訊的 Row
  8. DogIcon() 可顯示犬隻的相片。
  9. DogInformation() 可顯示犬隻的名稱和年齡。
  10. WoofPreview() 可讓您在「Design」窗格中預覽應用程式。

確保模擬器/裝置採用淺色主題

在這個程式碼研究室中,您將使用淺色和深色兩種主題,但大部分的程式碼研究室都是使用淺色主題。開始設定前,請先確認裝置/模擬器使用淺色主題。

如果要檢視使用淺色主題的應用程式,請在模擬器或實體裝置中執行以下操作:

  1. 在裝置上前往「設定」應用程式。
  2. 搜尋「深色主題」,然後按一下即可進入。
  3. 如果「深色主題」已啟用,請將其關閉。

執行範例程式碼,看看該從何著手;您要開始操作的地方是顯示犬隻相片、名稱和年齡的清單。這份清單可以正常運作,但並不美觀,因此我們要加以調整。

6d253ae50c63014d.png

4. 新增顏色

首先,您要在 Woof 應用程式中修改的是色彩配置。

色彩配置是應用程式採用的色彩組合。不同的色彩組合會帶來不同的情緒感觸,進而影響使用者在使用應用程式時的感受。

在 Android 系統中,色彩會以十六進位色彩值表示。十六進位顏色碼以井字符號 (#) 開頭,後方有六個英文字母及/或數字,代表了顏色的紅、綠、藍 (RGB) 等元件。前兩個字母/數字代表紅色,後兩個代表綠色,後兩個代表藍色。

這裡會顯示建立顏色使用的十六進位數字。

一個顏色也可以包含一個 Alpha 值 (字母和/或數字),用以表示色彩的透明度 (#00 是不透明度為 0%,也就是完全透明,而 #FF 是不透明度 100%,也就是完全不透明)。加入後,Alpha 值是井字號 (#) 字元後十六進位顏色代碼的前兩個字元。如沒有加入 Alpha 值,系統會假設該值為 #FF,亦即不透明度 100% (完全不透明)。

以下是顏色範例和其十六進位值。

2753d8cdd396c449.png

使用 Material Design 主題設定建構工具建立色彩配置

我們將使用 Material Design 主題設定建構工具,為應用程式建立自訂色彩配置。

  1. 按一下這個連結前往 Material Design 主題設定建構工具
  2. 左側窗格中會顯示「Core Colors」,請按一下「Primary」:

Material Design 主題設定建構工具顯示四個核心顏色

  1. 系統會開啟 HCT 顏色挑選器。

這是 HCT 顏色挑選器,可讓您在 Material Design 主題設定建構工具中選擇自訂顏色。

  1. 您將在這個顏色挑選器中變更主要顏色,建立應用程式螢幕截圖中顯示的色彩配置。請在文字方塊中,將目前的文字替換成 #006C4C。這會讓應用程式的主要顏色變成綠色。

這張圖顯示 HCT 顏色挑選器已設為綠色

觀察此操作怎麼將畫面上的應用程式更新為採用綠色配置。

這張圖顯示 HCT 顏色挑選器中的顏色變更後,Material Design 主題設定建構工具的應用程式也隨之調整。

  1. 向下捲動頁面,系統會以您輸入的顏色產生淺色和深色主題,而您可以查看整體色彩配置。

Material Design 主題設定建構工具的淺色配置

Material Design 主題設定建構工具產生的深色配置

您可能會疑惑這些設定扮演什麼角色?要怎麼使用?以下列舉幾個要點說明:

  • 「primary」顏色用於 UI 中的主要元件。
  • 「secondary」顏色用於 UI 中較不醒目的元件。
  • 「tertiary」顏色用於對比可平衡主要和次要顏色的強調色,或將引起對某個元素的高度關注,例如輸入欄位。
  • 「On」顏色元素會在調色盤中顯示在其他顏色的「上方」,而且也是套用至文字、圖解和筆觸的主要顏色元素。在調色盤中,我們有「onSurface」顏色 (顯示在「表面」顏色之上),以及「onPrimary」顏色 (顯示在「主要」顏色之上)。

這些版位設計有助於建構連貫的設計系統,讓相關元件採用相似的顏色。

現在我們已對顏色理論有所掌握,接下來可以在應用程式中加入這個精美調色盤了!

新增調色盤至主題

在 Material Design 主題設定建構工具的頁面中,您可以選擇點選「Export」按鈕,下載 Color.kt 檔案和 Theme.kt 檔案,內含您在主題設定建構工具中建立的自訂主題。

這項操作有助於將我們建立的自訂主題加到應用程式中。不過,由於產生的 Theme.kt 檔案不含動態色彩的程式碼 (我們稍後將在程式碼研究室中說明),因此請複製檔案。

  1. 開啟 Color.kt 檔案,然後將內容替換為下方的程式碼,複製新的色彩配置。
package com.example.woof.ui.theme

import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF006C4C)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFF89F8C7)
val md_theme_light_onPrimaryContainer = Color(0xFF002114)
val md_theme_light_secondary = Color(0xFF4D6357)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFCFE9D9)
val md_theme_light_onSecondaryContainer = Color(0xFF092016)
val md_theme_light_tertiary = Color(0xFF3D6373)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFC1E8FB)
val md_theme_light_onTertiaryContainer = Color(0xFF001F29)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFBFDF9)
val md_theme_light_onBackground = Color(0xFF191C1A)
val md_theme_light_surface = Color(0xFFFBFDF9)
val md_theme_light_onSurface = Color(0xFF191C1A)
val md_theme_light_surfaceVariant = Color(0xFFDBE5DD)
val md_theme_light_onSurfaceVariant = Color(0xFF404943)
val md_theme_light_outline = Color(0xFF707973)
val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
val md_theme_light_inverseSurface = Color(0xFF2E312F)
val md_theme_light_inversePrimary = Color(0xFF6CDBAC)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006C4C)
val md_theme_light_outlineVariant = Color(0xFFBFC9C2)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFF6CDBAC)
val md_theme_dark_onPrimary = Color(0xFF003826)
val md_theme_dark_primaryContainer = Color(0xFF005138)
val md_theme_dark_onPrimaryContainer = Color(0xFF89F8C7)
val md_theme_dark_secondary = Color(0xFFB3CCBE)
val md_theme_dark_onSecondary = Color(0xFF1F352A)
val md_theme_dark_secondaryContainer = Color(0xFF354B40)
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE9D9)
val md_theme_dark_tertiary = Color(0xFFA5CCDF)
val md_theme_dark_onTertiary = Color(0xFF073543)
val md_theme_dark_tertiaryContainer = Color(0xFF244C5B)
val md_theme_dark_onTertiaryContainer = Color(0xFFC1E8FB)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF191C1A)
val md_theme_dark_onBackground = Color(0xFFE1E3DF)
val md_theme_dark_surface = Color(0xFF191C1A)
val md_theme_dark_onSurface = Color(0xFFE1E3DF)
val md_theme_dark_surfaceVariant = Color(0xFF404943)
val md_theme_dark_onSurfaceVariant = Color(0xFFBFC9C2)
val md_theme_dark_outline = Color(0xFF8A938C)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
val md_theme_dark_inversePrimary = Color(0xFF006C4C)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF6CDBAC)
val md_theme_dark_outlineVariant = Color(0xFF404943)
val md_theme_dark_scrim = Color(0xFF000000)
  1. 開啟 Theme.kt 檔案,然後將內容替換為下方的程式碼,在主題中加入新顏色。
package com.example.woof.ui.theme

import android.app.Activity
import android.os.Build
import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

private val LightColors = lightColorScheme(
    primary = md_theme_light_primary,
    onPrimary = md_theme_light_onPrimary,
    primaryContainer = md_theme_light_primaryContainer,
    onPrimaryContainer = md_theme_light_onPrimaryContainer,
    secondary = md_theme_light_secondary,
    onSecondary = md_theme_light_onSecondary,
    secondaryContainer = md_theme_light_secondaryContainer,
    onSecondaryContainer = md_theme_light_onSecondaryContainer,
    tertiary = md_theme_light_tertiary,
    onTertiary = md_theme_light_onTertiary,
    tertiaryContainer = md_theme_light_tertiaryContainer,
    onTertiaryContainer = md_theme_light_onTertiaryContainer,
    error = md_theme_light_error,
    errorContainer = md_theme_light_errorContainer,
    onError = md_theme_light_onError,
    onErrorContainer = md_theme_light_onErrorContainer,
    background = md_theme_light_background,
    onBackground = md_theme_light_onBackground,
    surface = md_theme_light_surface,
    onSurface = md_theme_light_onSurface,
    surfaceVariant = md_theme_light_surfaceVariant,
    onSurfaceVariant = md_theme_light_onSurfaceVariant,
    outline = md_theme_light_outline,
    inverseOnSurface = md_theme_light_inverseOnSurface,
    inverseSurface = md_theme_light_inverseSurface,
    inversePrimary = md_theme_light_inversePrimary,
    surfaceTint = md_theme_light_surfaceTint,
    outlineVariant = md_theme_light_outlineVariant,
    scrim = md_theme_light_scrim,
)

private val DarkColors = darkColorScheme(
    primary = md_theme_dark_primary,
    onPrimary = md_theme_dark_onPrimary,
    primaryContainer = md_theme_dark_primaryContainer,
    onPrimaryContainer = md_theme_dark_onPrimaryContainer,
    secondary = md_theme_dark_secondary,
    onSecondary = md_theme_dark_onSecondary,
    secondaryContainer = md_theme_dark_secondaryContainer,
    onSecondaryContainer = md_theme_dark_onSecondaryContainer,
    tertiary = md_theme_dark_tertiary,
    onTertiary = md_theme_dark_onTertiary,
    tertiaryContainer = md_theme_dark_tertiaryContainer,
    onTertiaryContainer = md_theme_dark_onTertiaryContainer,
    error = md_theme_dark_error,
    errorContainer = md_theme_dark_errorContainer,
    onError = md_theme_dark_onError,
    onErrorContainer = md_theme_dark_onErrorContainer,
    background = md_theme_dark_background,
    onBackground = md_theme_dark_onBackground,
    surface = md_theme_dark_surface,
    onSurface = md_theme_dark_onSurface,
    surfaceVariant = md_theme_dark_surfaceVariant,
    onSurfaceVariant = md_theme_dark_onSurfaceVariant,
    outline = md_theme_dark_outline,
    inverseOnSurface = md_theme_dark_inverseOnSurface,
    inverseSurface = md_theme_dark_inverseSurface,
    inversePrimary = md_theme_dark_inversePrimary,
    surfaceTint = md_theme_dark_surfaceTint,
    outlineVariant = md_theme_dark_outlineVariant,
    scrim = md_theme_dark_scrim,
)

@Composable
fun WoofTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> DarkColors
        else -> LightColors
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            setUpEdgeToEdge(view, darkTheme)
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        shapes = Shapes,
        typography = Typography,
        content = content
    )
}

/**
 * Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
 * light or dark depending on whether the [darkTheme] is enabled or not.
 */
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
    val window = (view.context as Activity).window
    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.statusBarColor = Color.Transparent.toArgb()
    val navigationBarColor = when {
        Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
        Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
        // Min sdk version for this app is 24, this block is for SDK versions 24 and 25
        else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
    }
    window.navigationBarColor = navigationBarColor
    val controller = WindowCompat.getInsetsController(window, view)
    controller.isAppearanceLightStatusBars = !darkTheme
    controller.isAppearanceLightNavigationBars = !darkTheme
}

WoofTheme() 中,colorScheme val 使用 when 陳述式。

  • 如果 dynamicColor 為 true,且建構版本在 S 以上,請檢查裝置是否採用 darkTheme
  • 如果裝置採用深色主題,colorScheme 會設為 dynamicDarkColorScheme
  • 如果裝置沒有採用深色主題,colorScheme 則會設為 dynamicLightColorScheme
  • 如果應用程式未使用 dynamicColor,系統會檢查應用程式是否採用 darkTheme。如果是,colorScheme 會設為 DarkColors
  • 如果以上皆非,colorScheme 會設為 LightColors

在複製的 Theme.kt 檔案中,dynamicColor 設為 false,且我們操作的裝置處於淺色模式,因此 colorScheme 會設為 LightColors

val colorScheme = when {
       dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
           val context = LocalContext.current
           if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
       }

       darkTheme -> DarkColors
       else -> LightColors
   }
  1. 重新執行應用程式,請注意,應用程式列已自動變更顏色。

b48b3fa2ecec9b86.png

顏色對應

Material Design 元件會自動對應至顏色版位。UI 上的其他重要元件 (例如懸浮動作按鈕),也都會預設為「Primary」顏色。換句話說,您不需要明確指定元件的顏色;當您在應用程式中設定色彩主題時,系統就會自動將顏色對應至顏色版位。如要覆寫這項設定,請在程式碼中明確設定顏色;如要進一步瞭解顏色扮演的角色,請按這裡

在本節中,我們會利用 Card 納入包含 DogIcon()DogInformation()Row,以便區分清單項目顏色和背景。

  1. DogItem() 可組合函式中,使用 Card() 納入 Row()
Card() {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}
  1. 由於 Card 現在是 DogItem() 中的第一個子項可組合函式,請將修飾符從 DogItem() 傳入至 Card,並將 Row 的修飾符更新為 Modifier 的新例項。
Card(modifier = modifier) {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}
  1. 請看一下 WoofPreview()。由於 Card 可組合函式的關係,清單項目現在已自動變更顏色。顏色雖然看起來漂亮,但清單項目之間沒有間距。

6d49372a1ef49bc7.png

Dimens 檔案

就像使用 strings.xml 在應用程式中儲存字串一樣,使用名為 dimens.xml 的檔案儲存維度值也不失為一個實用的好方法。這樣一來,您就不用對值進行硬式編碼,如果需要的話,也可以在同個位置進行變更。

請依序前往「app」>「res」>「values」>「dimens.xml」,然後查看檔案。檔案會儲存 padding_smallpadding_mediumimage_size 的維度值。這些維度會用於整個應用程式中。

<resources>
   <dimen name="padding_small">8dp</dimen>
   <dimen name="padding_medium">16dp</dimen>
   <dimen name="image_size">64dp</dimen>
</resources>

如要新增 dimens.xml 檔案中的值,以下是正確的格式:

說明如何正確設定格式,新增來自維度資源的值

舉例來說,如要新增 padding_small,您需要傳入 dimensionResource(id = R.dimen.padding_small)

  1. WoofApp() 內,將含有 padding_smallmodifier 加到對 DogItem() 的呼叫中。
@Composable
fun WoofApp() {
    Scaffold { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

WoofPreview() 中,現在清單項目之間更容易區別了。

c54f870f121fe02.png

深色主題

在 Android 系統中,您可以選擇將裝置切換為使用深色主題。深色主題使用較深、較柔和的色彩,且:

  • 可大幅減少耗電量 (視裝置的螢幕技術而定)。
  • 可改善低視能及對明亮光線敏感使用者的可視性。
  • 能讓所有人在低光源環境中輕鬆使用裝置。

您的應用程式可以選擇採用強制使用深色設定,讓系統自動採用深色主題。但是對使用者而言,採用深色主題可以帶來更好的體驗,因此建議您不要開放應用程式主題的完整控制選項。

選擇自己的深色主題時,請務必注意深色主題的顏色必須符合無障礙功能對比標準。深色主題使用深色的介面顏色,可選擇的輔色有限。

在預覽畫面中檢視深色主題

您已在上一步新增深色主題的顏色。如要查看深色主題,請在 MainActivity.kt 中新增另一個預覽可組合函式。這樣一來,在程式碼中變更 UI 版面配置時,就能同時查看淺色主題和深色主題的預覽畫面。

  1. WoofPreview() 下方,建立名為 WoofDarkThemePreview() 的新函式,並使用 @Preview@Composable 加上註解。
@Preview
@Composable
fun WoofDarkThemePreview() {

}
  1. DarkThemePreview() 內加入 WoofTheme()。如果沒有加入 WoofTheme(),就無法查看應用程式新增的任何樣式。將 darkTheme 參數設為 True
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {

   }
}
  1. 呼叫 WoofTheme() 內的 WoofApp()
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {
       WoofApp()
   }
}

現在,在「Design」窗格中向下捲動,即可查看使用深色主題的應用程式,包括顏色較深的應用程式/清單項目背景和較淺色的文字。您可以比較深色主題和淺色主題之間的差異。

深色主題

淺色主題

在裝置或模擬器中檢視深色主題

如要在模擬器或實體裝置中以深色主題檢視應用程式:

  1. 在裝置上前往「設定」應用程式。
  2. 搜尋「深色主題」,然後按一下即可進入。
  3. 啟用「深色主題」
  4. 重新開啟 Woof 應用程式,應用程式將採用深色主題

bc31a94207265b08.png

本程式碼研究室著重在淺色主題上,因此繼續設計應用程式前,請先停用深色主題。

  1. 在裝置上前往「設定」應用程式。
  2. 選取「Display」
  3. 關閉「Dark theme」

比較本課程一開始與現在的應用程式。清單項目和文字的定義更明確,且色彩配置也更搶眼。

沒有顏色

有顏色 (淺色主題)

有顏色 (深色主題)

動態色彩

Material 3 特別注重使用者的個人化體驗,因此推出動態色彩這項新功能,可根據使用者的桌布為您的應用程式建立主題。這樣一來,即便使用者喜歡綠色,但如果手機背景是藍色,Woof 應用程式也會採用藍色。動態主題設定僅適用於搭載 Android 12 以上版本的特定裝置。

相對地,自訂主題可用於有強烈品牌色彩需求的應用程式;如果是不支援動態主題設定的裝置,則也需實作自訂主題,好讓應用程式保有主題設定。

  1. 如要啟用動態色彩,請開啟 Theme.kt 並前往 WoofTheme() 可組合函式,然後將 dynamicColor 參數設為 true
@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = true,
   content: @Composable () -> Unit
)
  1. 如要變更裝置或模擬器的背景,請前往「Settings」搜尋「Wallpaper」
  2. 將桌布變更為單一顏色或使用一組顏色。
  3. 重新執行應用程式,查看動態主題 (請注意,裝置或模擬器必須搭載 Android 12 以上版本,才能顯示動態色彩);您也可以隨意搭配不同桌布,試試這項功能會有什麼效果。

710bd13f6b189dc5.png

  1. 本程式碼研究室的重點在於自訂主題設定,因此請先停用 dynamicColor 再繼續進行。
@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = false,
   content: @Composable () -> Unit
)

5. 新增形狀

套用形狀可能會大幅改變可組合函式的外觀和風格。形狀可用於吸引注意、識別元件、傳達狀態,以及呈現品牌風格。

許多形狀都是以描述矩形圓角使用的 RoundedCornerShape 定義。傳入的數值會定義圓角。如果使用 RoundedCornerShape(0.dp),則矩形沒有圓角;如果使用 RoundedCornerShape(50.dp),邊角就會是圓角。

0.dp

25.dp

50.dp

套用形狀的 Woof 清單項目

套用形狀的 Woof 清單項目

套用形狀的 Woof 清單項目

您也可以在每個邊角加上不同的弧度百分比,進一步自訂形狀。嘗試不同形狀十分有趣!

左上:50.dp
左下:25.dp
右上:0.dp
右下:15.dp

左上:15.dp
左下:50.dp
右上:50.dp
右下:15.dp

左上:0.dp
左下:50.dp
右上:0.dp
右下:50.dp

套用形狀的 Woof 清單項目

套用形狀的 Woof 清單項目

套用形狀的 Woof 清單項目

Shape.kt 檔案是用以在 Compose 中定義元件形狀。元件分為三種:小、中、大。在本節中,您將修改定義 medium 大小的 Card 元件。元件會根據大小,依形狀類別分組。

在本節中,您要將犬隻的圖片設為圓形,並修改清單項目的形狀。

將犬隻圖片設為圓形

  1. 開啟 Shape.kt 檔案。請注意,small 參數已設為 RoundedCornerShape(50.dp),這會將圖片調整為圓形。
val Shapes = Shapes(
   small = RoundedCornerShape(50.dp),
)
  1. 開啟「MainActivity.kt」。在 DogIcon() 中,將 clip 屬性加入 Imagemodifier 內,這會將圖片裁減成某個形狀。接著,請傳入 MaterialTheme.shapes.small
import androidx.compose.ui.draw.clip

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),

當您查看 WoofPreview() 時,會發現犬隻圖示已顯示為圓形!不過,有些相片會裁切兩側,無法顯示為完整圓形。

1d4d1e5eaaddf71e.png

  1. 如要將所有相片設為圓形,請加入 ContentScaleCrop 屬性,這樣就可以根據顯示大小裁剪圖片。請注意,contentScaleImage 的屬性,並不屬於 modifier
import androidx.compose.ui.layout.ContentScale

@Composable
fun DogIcon(
   @DrawableRes dogIcon: Int,
   modifier: Modifier = Modifier
) {
   Image(
       modifier = modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small))
           .clip(MaterialTheme.shapes.small),
       contentScale = ContentScale.Crop,

以下是完整的 DogIcon() 可組合函式。

@Composable
fun DogIcon(
    @DrawableRes dogIcon: Int,
    modifier: Modifier = Modifier
) {
    Image(
        modifier = modifier
            .size(dimensionResource(R.dimen.image_size))
            .padding(dimensionResource(R.dimen.padding_small))
            .clip(MaterialTheme.shapes.small),
        contentScale = ContentScale.Crop,
        painter = painterResource(dogIcon),

        // Content Description is not needed here - image is decorative, and setting a null content
        // description allows accessibility services to skip this element during navigation.

        contentDescription = null
    )
}

現在,圖示在 WoofPreview() 中會顯示為圓形。

fc93106990f5e161.png

在清單項目中加入形狀

在本節中,您將在清單項目中加入一個形狀。清單項目現在是以 Card 的形式顯示。Card 是能包含單一可組合函式的介面,且內含裝飾選項。您可以透過邊框、形狀、等項目加入裝飾。在本節中,您將使用 Card 在清單項目中加入形狀。

已新增形狀維度的 Woof 清單項目

  1. 開啟 Shape.kt 檔案。Card 是中型元件,因此您需要新增 Shapes 物件的 medium 參數。就這個應用程式而言,這是清單項目的右上角和左下角,不是四角都要設為圓角。要完成此設定,只要在 medium 屬性中傳入 16.dp 即可。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

由於根據預設,Card 已使用中型形狀,因此您不必明確將其設為中型形狀。現在不妨開啟預覽畫面,看看採用新形狀的 Card

Woof 預覽畫面,資訊卡採用新形狀

如果返回 WoofTheme() 中的 Theme.kt 檔案並查看 MaterialTheme(),您會發現 shapes 屬性已設為剛才更新的 Shapes val

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

下方是清單項目套用形狀前後的對照圖。請注意,在應用程式中加入形狀後,可提升視覺上的吸引力。

不使用形狀

使用形狀

6. 新增字體排版

Material Design 輸入比例

輸入比例是可以在應用程式中使用的多種字型樣式,可確保風格靈活、一致。Material Design 輸入比例包含 15 種字型系統支援的字型樣式。此外,命名和分組功能也經過簡化,包括顯示、大標題、標題、內文和標籤 (皆有大、中和小三種尺寸)。只有在需要自訂應用程式時才需要使用這些選項。如果您不知道如何設定每個輸入比例類別,別忘了您可以使用預設的字體排版比例。

999a161dcd9b0ec4.png

輸入比例包含可重複使用的文字類別,每個類別都有對應的應用程式和意義。

顯示

「顯示」樣式會讓畫面中的文字放到最大,適合簡短且具重要性的文字或數字使用。這種樣式在大螢幕上的呈現效果最好。

廣告標題

廣告標題最適合用於在小螢幕上顯示簡短、高度強調的文字。如要標記文字的主要段落,或內容的重要區域,這些樣式就能派上用場。

標題

標題比廣告標題樣式還小,因此應該用於相對簡短的中度強調文字。

內文

「內文」樣式適合應用程式中較長篇幅的文字使用。

標籤

「標籤」樣式雖較小、但功能齊全,可用於像是元件中的文字,或是內容內文中的極小文字,例如字幕。

字型

Android 平台雖提供各式各樣的字型,但建議您使用非預設字型來自訂應用程式。自訂字型可以為品牌增添個性,也可用於宣傳品牌。

在本節中,您將新增自訂字型,名稱為 Abril FatfaceMontserrat BoldMonterrat Regular。您會使用 displayLarge 和 displayMedium 廣告標題,以及 Material Design 輸入系統中的 bodyLarge 文字,並將這些加入應用程式的文字中。

建立字型 Android 資源目錄。

在應用程式中加入字型之前,您必須先加入字型目錄。

  1. 在 Android Studio 的專案檢視畫面中,在「res」資料夾按一下滑鼠右鍵。
  2. 依序選取「New」>「Android Resource Directory」

這張圖片顯示如何瀏覽 Android 資源目錄中的檔案結構。

  1. 將目錄命名為「font」,將資源類型設為「font」,然後按一下「OK」

這張圖片顯示如何使用新資源目錄加入字型目錄。

  1. 依序點選「res」>「font」,開啟其中的新字型資源目錄。

下載自訂字型

由於您使用的字型並非 Android 平台提供,因此您必須下載自訂字型。

  1. 前往 https://fonts.google.com/
  2. 搜尋 Montserrat,然後按一下「Download family」
  3. 解壓縮 zip 檔案。
  4. 開啟下載的 Montserrat 資料夾。在 static 資料夾中,找到 Montserrat-Bold.ttfMontserrat-Regular.ttf (ttf 代表 TrueType 字型,也是字型檔案的格式)。選取這兩種字型,然後拖曳至 Android Studio 中的專案字型資源目錄。

這張圖片顯示 Montserrat 字型的靜態資料夾內容。

  1. 在字型資料夾中,將 Montserrat-Bold.ttf 重新命名為 montserrat_bold.ttf,並將 Montserrat-Regular.ttf 重新命名為 montserrat_regular.ttf
  2. 搜尋 Abril Fatface,然後按一下「Download family」
  3. 開啟下載的 Abril_Fatface 資料夾。選取「AbrilFatface-Regular.ttf」,然後拖曳至字型資源目錄中。
  4. 在字型資料夾中,將「Abril_Fatface_Regular.ttf」重新命名為「abril_fatface_regular.ttf」

專案中的字型資源目錄加入三個自訂字型檔案後會如下所示:

此圖片顯示已新增至字型資料夾的字型檔案。

初始化字型

  1. 在專案視窗中,依序開啟「ui.theme」>「Type.kt」。接著在匯入陳述式下方和 Typography val 上方初始化下載的字型。首先,請初始化 Abril Fatface,方法是將其設為 FontFamily,然後使用字型檔案 abril_fatface_regular 傳入 Font
​​import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.example.woof.R

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)
  1. Abril Fatface 下初始化 Montserrat (方法是將其設為等於 FontFamily,然後使用檔案 montserrat_regular 傳入 Font)。如果是 montserrat_bold,請加入 FontWeight.Bold。即使傳入字型檔案的粗體版,Compose 也無法辨別檔案為粗體,因此您必須明確連結檔案至 FontWeight.Bold
import androidx.compose.ui.text.font.FontWeight

val AbrilFatface = FontFamily(
   Font(R.font.abril_fatface_regular)
)

val Montserrat = FontFamily(
   Font(R.font.montserrat_regular),
   Font(R.font.montserrat_bold, FontWeight.Bold)
)

接著,將剛才新增的字型設定不同類型的標題。Typography 物件包含上述 13 個不同字體的參數。您可以定義的數量沒有限制。在此應用程式中,我們將設定 displayLargedisplayMediumbodyLarge。在這個應用程式的下一個部分,您要使用 labelSmall,因此請在這裡加入標題。

下表顯示顯示您加入的每個廣告標題所用的字型、粗細和大小。

8ea685b3871d5ffc.png

  1. 請將 displayLarge 屬性設為TextStyle,然後使用上表的資訊填入 fontFamilyfontWeightfontSize。這表示所有設為 displayLarge 的文字都會使用 Abril Fatface 字型,且字型粗細為一般,fontSize 則為 36.sp

displayMediumlabelSmallbodyLarge 都要重複此程序。

import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp

val Typography = Typography(
   displayLarge = TextStyle(
       fontFamily = AbrilFatface,
       fontWeight = FontWeight.Normal,
       fontSize = 36.sp
   ),
   displayMedium = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 20.sp
   ),
   labelSmall = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Bold,
       fontSize = 14.sp
   ),
   bodyLarge = TextStyle(
       fontFamily = Montserrat,
       fontWeight = FontWeight.Normal,
       fontSize = 14.sp
   )
)

如果您前往 WoofTheme() 中的 Theme.kt 檔案並查看 MaterialTheme()typography 參數會是您剛才更新的 Typography val

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

在應用程式文字中新增字體排版

現在,您將在應用程式中的每個文字例項中加入標題類型。

  1. dogName 是簡短且重要的資訊,因此請將 displayMedium 新增為樣式。新增 bodyLargedogAge 的樣式,因為這適合使用較小的文字大小。
@Composable
fun DogInformation(
   @StringRes dogName: Int,
   dogAge: Int,
   modifier: Modifier = Modifier
) {
   Column(modifier = modifier) {
       Text(
           text = stringResource(dogName),
           style = MaterialTheme.typography.displayMedium,
           modifier = Modifier.padding(top = dimensionResource(id = R.dimen.padding_small))
       )
       Text(
           text = stringResource(R.string.years_old, dogAge),
           style = MaterialTheme.typography.bodyLarge
       )
   }
}
  1. 現在犬隻名稱在 WoofPreview() 中,會以 20.sp 的粗體 Montserrat 字型顯示,而犬隻年齡則會以 14.sp 的一般 Montserrat 字型顯示。

已新增字體排版的 Woof 預覽畫面

下方是清單項目加入字體排版的前後對照圖。請注意犬隻名稱與犬隻年齡的字型差異。

沒有字體排版

有字體排版

7. 新增頂端列

Scaffold 是一種版面配置,可為多個元件和畫面元素提供版位,例如:ImageRowColumnScaffold 也會為 TopAppBar 提供版位,後者將在此節用到。

TopAppBar 有許多用途,但在本例中會用於宣傳品牌,增添應用程式特色。TopAppBar 分為四種類型:中央、小型、中型,以及大型。在本程式碼研究室中,您將實作位於正上方的應用程式列。您會建立一個可組合函式 (如下方螢幕截圖所示),然後將其排入 ScaffoldtopBar 區段。

172417c7b64372f7.png

就此應用程式而言,頂端列是由含有標誌圖片和應用程式名稱文字的 Row 組成。標誌裡有可愛的漸層狗爪,還有應用程式標題!

736f411f5067e0b5.png

在頂端列中新增圖片和文字

  1. MainActivity.kt 中,建立名為 WoofTopAppBar() 的可組合函式,其中須含有選用的 modifier
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {

}
  1. Scaffold 支援 contentWindowInsets 參數,有助於為 Scaffold 內容指定插邊。WindowInsets 是可讓應用程式與系統 UI 重疊的畫面元素,將會透過 PaddingValues 參數傳遞至內容版位。請按這裡瞭解詳情。

contentWindowInsets 值會以 contentPadding 的形式傳遞至 LazyColumn

@Composable
fun WoofApp() {
    Scaffold { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}
  1. Scaffold 中加入 topBar 屬性,並將其設為 WoofTopAppBar()
Scaffold(
   topBar = {
       WoofTopAppBar()
   }
)

以下是 WoofApp() 可組合函式的外觀:

@Composable
fun WoofApp() {
    Scaffold(
        topBar = {
            WoofTopAppBar()
        }
    ) { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

由於 WoofTopAppBar() 中沒有任何內容,因此 WoofPreview() 內部不會變更。我們來調整一下吧!

套用字體排版的 Woof 預覽畫面

  1. WoofTopAppBar() Composable 中,加入 CenterAlignedTopAppBar() 並將修飾符參數設為傳送至 WoofTopAppBar() 的修飾符。
import androidx.compose.material3.CenterAlignedTopAppBar

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       modifier = modifier
   )
}
  1. 針對標題參數傳入 Row,用於保留 CenterAlignedTopAppBarImageText
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier){
   CenterAlignedTopAppBar(
       title = {
           Row() {

           }
       },
       modifier = modifier
   )
}
  1. Row 中加入 Image 標誌。
  • modifier 中的圖片大小設為 dimens.xml 檔案的 image_size,邊框間距則設為 dimens.xml 檔案的 padding_small
  • 使用 painterImage 設為可繪項目資料夾的 ic_woof_logo
  • contentDescription 設為 null。在這種情況下,應用程式標誌不會為視障使用者加入任何語意資訊,因此我們不必加入內容說明。
Row() {
   Image(
       modifier = Modifier
           .size(dimensionResource(id = R.dimen.image_size))
           .padding(dimensionResource(id = R.dimen.padding_small)),
       painter = painterResource(R.drawable.ic_woof_logo),
       contentDescription = null
   )
}
  1. 接下來,在 Image. 之後的 Row 中新增 Text 可組合函式。
  • 使用 stringResource() 將其設為 app_name 的值。這會將文字設為應用程式的名稱 (儲存在 strings.xml 中)。
  • 應用程式名稱是簡短且重要的文字,因此請將文字樣式設為 displayLarge
Text(
   text = stringResource(R.string.app_name),
   style = MaterialTheme.typography.displayLarge
)

含頂端應用程式列的 Woof 預覽畫面

以下是 WoofPreview() 顯示的內容,因為圖示和文字並未垂直對齊,所以看起來有點不對勁。

  1. 如要修正此問題,請將 verticalAlignment 值參數新增至 Row,並將其設為 Alignment.CenterVertically
import androidx.compose.ui.Alignment

Row(
   verticalAlignment = Alignment.CenterVertically
)

Woof 預覽畫面,應用程式列垂直置中於頂端

這樣看起來好多了!

以下是完整的 WoofTopAppBar() 可組合函式:

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       title = {
           Row(
               verticalAlignment = Alignment.CenterVertically
           ) {
               Image(
                   modifier = Modifier
                       .size(dimensionResource(id = R.dimen.image_size))
                       .padding(dimensionResource(id = R.dimen.padding_small)),
                   painter = painterResource(R.drawable.ic_woof_logo),

                   contentDescription = null
               )
               Text(
                   text = stringResource(R.string.app_name),
                   style = MaterialTheme.typography.displayLarge
               )
           }
       },
       modifier = modifier
   )
}

執行應用程式,然後就可以看到 TopAppBar 如何以美觀的方式將應用程式組合在一起。

沒有頂端應用程式列

有頂端應用程式列

現在,來看看套用深色主題的應用程式成品吧!

2776e6a45cf3434a.png

恭喜,您已經順利完成本程式碼研究室課程!

8. 取得解決方案程式碼

完成程式碼研究室後,如要下載當中用到的程式碼,您可以使用這些 git 指令:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

另外,您也可以下載存放區為 ZIP 檔案,然後解壓縮並在 Android Studio 中開啟。

如要查看解決方案程式碼,請前往 GitHub

9. 結論

您剛剛建立了第一個採用 Material Design 的應用程式!您建立了淺色和深色主題的自訂調色盤,還為各種元件分別建立形狀、下載字型,並將其新增至應用程式,更建立了精美的頂端列合併所有項目。請善加運用您在本程式碼研究室所學到的技巧,變更顏色、形狀和字型,打造出您專屬的應用程式!

摘要

  • Material Design 主題設定可協助您在應用程式中使用 Material Design,依照指引自訂顏色、字體排版和形狀。
  • 主題是由 Theme.kt 檔案中的可組合函式所定義。可組合函式的命名法,以這個應用程式來說,就是 [your app name]+Theme()WoofTheme()。在這個函式中,MaterialTheme 物件會設定應用程式的 colortypographyshapescontent
  • 您可以在 Color.kt 中列出應用程式使用的顏色,然後在 Theme.kt 中將 LightColorPaletteDarkColorPalette 內的顏色指派至特定版位,您不需要指派所有版位。
  • 您的應用程式可以選擇採用強制使用深色設定,讓系統自動採用深色主題。但由您親自實作深色主題,可以對應用程式主題保有全權掌握,為使用者帶來更好的體驗。
  • Shape.kt 可定義應用程式形狀。指定圓角的形狀時,可以選擇三種形狀大小:小、中、大。
  • 形狀可用於吸引注意、識別元件、傳達狀態,以及呈現品牌風格。
  • Types.kt 可讓您初始化字型,並為 Material Design 輸入比例指派 fontFamilyfontWeightfontSize
  • Material Design 輸入比例包含多種對比樣式,可滿足應用程式及其內容的需求。輸入比例是輸入系統支援的 15 種樣式組合。

10. 瞭解詳情