Android Material Design 3 主題與樣式系統

Material Design 3 (M3) 是 Google 最新的設計語言,也被稱為 Material You。它在 Android 12 (API 31) 之後變得尤為重要,核心理念是「個人化」與「動態性」。Jetpack Compose 原生支援 M3,透過 MaterialTheme Composable 來統一管理。

主題的三大支柱

一個定義良好的 M3 主題由以下三個 Composable 參數組成:

MaterialTheme(
    colorScheme = MyColorScheme, // 顏色方案
    typography = MyTypography,   // 排版系統
    shapes = MyShapes,           // 形狀定義
    content = content
)

詳解 Color Scheme (顏色角色)

M3 捨棄了過去簡單的 Primary/Secondary 概念,導入了更詳盡的「角色 (Roles)」。

1. 核心角色

  • Primary:App 最主要的品牌色。
  • Secondary:輔助色,用於較不突出的元件。
  • Tertiary:第三色,用於強調或與品牌色形成對比的裝飾。

2. 「On」顏色與容器 (Containers)

這是 M3 最重要的概念。每種顏色都有對應的 on 顏色和 Container 變體:

  • PrimaryContainer:比 Primary 更淡,適合做為按鈕或卡片的背景。
  • OnPrimary:放在 Primary 顏色之上的文字或圖示色(確保對比度正確)。
  • OnPrimaryContainer:放在 PrimaryContainer 之上的文字色。
val LightColors = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    // ... 設定其他角色
)

3. 動態色彩 (Dynamic Color)

如果裝置支援 Android 12+,你可以直接從使用者的桌布取色,讓你的 App 看起來與系統環境融為一體。

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true, // 預設開啟動態色彩
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        // Android 12+ 且支援動態色彩
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColors
        else -> LightColors
    }
    
    MaterialTheme(colorScheme = colorScheme, content = content)
}

排版系統 (Typography)

M3 定義了 15 種標準樣式,涵蓋了從巨大的顯示文字到微小的標籤:

  1. Display (Large/Medium/Small):最重要的宣傳文字。
  2. Headline (Large/Medium/Small):分區的大標題。
  3. Title (Large/Medium/Small):導航列或中等標題。
  4. Body (Large/Medium/Small):正文內容。
  5. Label (Large/Medium/Small):按鈕文字或小型說明。

自訂字體

你可以整合 Google Fonts 或本地字體檔。

val provider = GoogleFont.Provider(...)
val fontName = GoogleFont("Inter")
val fontFamily = FontFamily(Font(googleFont = fontName, fontProvider = provider))

val MyTypography = Typography(
    bodyLarge = TextStyle(
        fontFamily = fontFamily,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp
    ),
    titleLarge = TextStyle(...)
)

形狀 (Shapes) 與圓角

M3 將組件分為五類形狀需求:

  • Extra Small:小圖示或迷你元件 (預設 4.dp)。
  • Small:提示視窗 (預設 8.dp)。
  • Medium:卡片或選單 (預設 12.dp)。
  • Large:對話框或大容器 (預設 28.dp)。
  • Extra Large:全螢幕背景或最大的圓角塊 (預設 28.dp)。
val MyShapes = Shapes(
    small = RoundedCornerShape(8.dp),
    medium = RoundedCornerShape(12.dp),
    large = RoundedCornerShape(16.dp)
)

// 使用方式
Card(shape = MaterialTheme.shapes.medium) { ... }

Tonal Elevation (色調高度)

在 M3 中,立體感不再只靠陰影。Elevation 現在會影響顏色。 這意味著元件的高度越高,它的底層色調就會混合越多 Primary 色調,顏色變得越深(或在淺色模式下變得更飽和)。

Surface(
    tonalElevation = 4.dp, // 不產生陰影,而是讓背景顏色變深
    shadowElevation = 0.dp
) {
    // 內容
}

實戰範例 (Practical Examples)

1. 自訂樣式的按鈕

結合 Secondary 顏色與 Large 形狀(膠囊形狀)。

Button(
    onClick = { /* TODO */ },
    shape = MaterialTheme.shapes.large, // 使用主題定義的 Large 圓角
    colors = ButtonDefaults.buttonColors(
        containerColor = MaterialTheme.colorScheme.secondary,
        contentColor = MaterialTheme.colorScheme.onSecondary
    )
) {
    Text("了解更多")
}

2. 精美的個人資料卡片

綜合運用排版、顏色與色調高度。

@Composable
fun ProfileCard(userName: String, bio: String) {
    Surface(
        modifier = Modifier.padding(16.dp),
        shape = MaterialTheme.shapes.medium,
        tonalElevation = 2.dp // 使用色調高度增加層次感
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = userName,
                style = MaterialTheme.typography.headlineSmall,
                color = MaterialTheme.colorScheme.primary
            )
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = bio,
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )
        }
    }
}

3. 手動切換深色模式

在實務中,我們常需要讓使用者在設定中手動切換主題。

// 自定義主題 Composable
@Composable
fun AppTheme(
    isDarkMode: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val myColors = if (isDarkMode) DarkColors else LightColors
    
    MaterialTheme(
        colorScheme = myColors,
        typography = MyTypography,
        shapes = MyShapes,
        content = content
    )
}

// 在 UI 中切換
var isUserDarkMode by remember { mutableStateOf(false) }

AppTheme(isDarkMode = isUserDarkMode) {
    Switch(
        checked = isUserDarkMode,
        onCheckedChange = { isUserDarkMode = it }
    )
}

主題開發最佳實踐

  1. 絕對不要硬編碼 (Hard-coding):不要直接寫 Color.WhitefontSize = 18.sp。這樣會破壞深色模式的支援。總是使用 MaterialTheme.colorScheme.surface
  2. 語意化優先:如果你需要一個醒目的顏色,使用 primary;如果你需要一個警示,使用 error
  3. 處理對比度:總是成對使用顏色。例如用 MaterialTheme.colorScheme.primary 當背景時,文字必須用 MaterialTheme.colorScheme.onPrimary
  4. 工具推薦:建議使用 Material Theme Builder 網頁工具來產生配色程式碼,這能確保你的配色邏輯符合 M3 的色調調色盤規則。
如果你是從 Material Design 2 遷移過來,請注意許多 Composable (如 Scaffold, TopAppBar) 現在都位於 androidx.compose.material3 套件下,千萬不要混用 M2 和 M3 的組件,這會導致主題抓取錯誤。