在 Compose 中使用 Material 3 进行主题设置

1. 简介

在本 Codelab 中,你将了解如何使用 Material Design 3 在 Jetpack Compose 中为应用设置主题。你还将了解 Material Design 3 的关键构建块:配色方案、排版和形状,这些构建块可帮助你以个性化和易于访问的方式为应用设置主题。

此外,你还将探索动态主题的支持以及不同的强调级别。

你将学到什么

在本 Codelab 中,你将学习

  • Material 3 主题的关键方面
  • Material 3 配色方案以及如何为你的应用生成主题
  • 如何为你的应用支持动态主题和明/暗主题
  • 排版和形状,让你的应用更具个性化
  • Material 3 组件及其自定义,为你的应用设置样式

你将构建什么

在本 Codelab 中,你将为名为 Reply 的电子邮件客户端应用设置主题。你将从一个未使用样式、使用基准主题的应用开始,然后运用所学知识为应用设置主题并支持深色主题。

d15db3dc75a9d00f.png

我们的应用使用基准主题的默认起始点。

你将使用配色方案、排版和形状创建主题,然后将其应用到应用的电子邮件列表和详情页面。你还将为应用添加动态主题支持。完成 Codelab 后,你的应用将同时支持颜色主题和动态主题。

Material 3 light

主题设置 Codelab 的终点,应用浅色主题和浅色动态主题。

Material 3 dark

主题设置 Codelab 的终点,应用深色主题和深色动态主题。

你需要什么

2. 设置

在此步骤中,你将下载 Reply 应用的完整代码,你将在本 Codelab 中为该应用设置样式。

获取代码

本 Codelab 的代码位于 codelab-android-compose GitHub 代码库中。如需克隆,请运行

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

或者,你也可以下载两个 zip 文件

查看示例应用

你刚刚下载的代码包含所有可用 Compose Codelab 的代码。如需完成此 Codelab,请在 Android Studio 中打开 ThemingCodelab 项目。

建议你从 main 分支中的代码开始,按照自己的进度逐步完成 Codelab。你可以随时通过更改项目的 Git 分支,在 Android Studio 中运行任一版本。

探索起始代码

主代码包含一个 UI 软件包,其中包含以下你将与之交互的主要软件包和文件

  • MainActivity.kt – 入口 activity,你在此处启动 Reply 应用。
  • com.example.reply.ui.theme – 此软件包包含主题、排版和配色方案。你将在此软件包中添加 Material 主题设置。
  • com.example.reply.ui.components – 包含应用的自定义组件,如列表项、应用栏等。你将对这些组件应用主题。
  • ReplyApp.kt – 这是我们的主可组合函数,UI 树将从这里开始。你将在该文件中应用顶层主题设置。

本 Codelab 将重点介绍 ui 软件包文件。

3. Material 3 主题设置

Jetpack Compose 提供了 Material Design 的实现,这是一个用于创建数字界面的综合设计系统。Material Design 组件(按钮、卡片、开关等)构建在 Material Theming 之上,Material Theming 是一种系统化的方式,用于自定义 Material Design,使其更好地反映你的产品品牌。

Material 3 主题包含以下子系统,可为你的应用添加主题设置:配色方案排版形状。当你自定义这些值时,你的更改会自动反映在你用于构建应用的 M3 组件中。让我们深入了解每个子系统,并在示例应用中实现它。

Sub systems of Material design: Color, Typography and Shapes.

Material 3 的颜色、排版和形状子系统。

4. 配色方案

配色方案的基础是五种关键颜色集,每种颜色都与 Material 3 组件使用的 13 种色调的色调调色板相关。

Five baseline key colors for creating an M3 theming.

创建 M3 主题的五个基准关键颜色。

然后,每种强调色(主要、次要和第三色)都提供四种不同色调的兼容颜色,用于搭配、定义强调和视觉表达。

Four tonal colors of primary, secondary, and tertiary baseline accent colors.

主要、次要和第三基准强调色的四种色调颜色。

类似地,中性色也分为四种兼容色调,用于表面和背景。当放置在任何表面上时,这些颜色对于强调文本图标也很重要。

Four tonal colors of baseline neutral colors.

基准中性色的四种色调颜色。

详细了解配色方案和颜色角色

生成配色方案

虽然你可以手动创建自定义 ColorScheme,但通常更容易使用品牌来源颜色生成一个。通过 Material Theme Builder 工具可以做到这一点,并且可以选择性地导出 Compose 主题代码。

你可以选择任何喜欢的颜色,但在我们的用例中,你将使用默认的 Reply 主要颜色 #825500。点击左侧核心颜色部分的 Primary 颜色,并在颜色选择器中添加代码。

294f73fc9d2a570e.png

在 Material Theme Builder 中添加主要颜色代码。

在 Material Theme Builder 中添加主要颜色后,你应该会看到以下主题以及右上角的导出选项。在本 Codelab 中,你将导出 Jetpack Compose 的主题。

Material Theme Builder with option to export at the top right corner.

Material Theme Builder 及其右上角的导出选项。

主要颜色 #825500 生成了以下主题,你将把它添加到应用中。Material 3 提供了广泛的颜色角色,可以灵活地表达组件的状态、重要性和强调。

Exported Light and Dark color scheme from primary color.

从主要颜色导出的浅色和深色配色方案。

生成的 Color.kt 文件包含主题的颜色,其中定义了浅色和深色主题颜色的所有角色。

Color.kt

package com.example.reply.ui.theme
import androidx.compose.ui.graphics.Color

val md_theme_light_primary = Color(0xFF825500)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
val md_theme_light_secondary = Color(0xFF6F5B40)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
val md_theme_light_tertiary = Color(0xFF51643F)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
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(0xFFFFFBFF)
val md_theme_light_onBackground = Color(0xFF1F1B16)
val md_theme_light_surface = Color(0xFFFFFBFF)
val md_theme_light_onSurface = Color(0xFF1F1B16)
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
val md_theme_light_outline = Color(0xFF817567)
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
val md_theme_light_inverseSurface = Color(0xFF34302A)
val md_theme_light_inversePrimary = Color(0xFFFFB951)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF825500)
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
val md_theme_light_scrim = Color(0xFF000000)

val md_theme_dark_primary = Color(0xFFFFB951)
val md_theme_dark_onPrimary = Color(0xFF452B00)
val md_theme_dark_primaryContainer = Color(0xFF633F00)
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
val md_theme_dark_secondary = Color(0xFFDDC2A1)
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
val md_theme_dark_onTertiary = Color(0xFF243515)
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
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(0xFF1F1B16)
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
val md_theme_dark_surface = Color(0xFF1F1B16)
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
val md_theme_dark_outline = Color(0xFF9C8F80)
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
val md_theme_dark_inversePrimary = Color(0xFF825500)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
val md_theme_dark_scrim = Color(0xFF000000)


val seed = Color(0xFF825500)

生成的 Theme.kt 文件包含浅色和深色配色方案以及应用主题的设置。它还包含主要的主题设置可组合函数 AppTheme()

Theme.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.runtime.Composable


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 AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

在 Jetpack Compose 中实现主题设置的核心元素是 MaterialTheme 可组合项。

你将 MaterialTheme() 可组合项封装在 AppTheme() 函数中,该函数接受两个参数

  • useDarkTheme - 此参数与函数 isSystemInDarkTheme() 相关联,用于观察系统主题设置并应用浅色或深色主题。如果你想手动将应用保持在浅色或深色主题下,可以将布尔值传递给 useDarkTheme
  • content - 将应用主题的内容。

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
   val colors = if (!useDarkTheme) {
       LightColors
   } else {
       DarkColors
   }

   MaterialTheme(
       colorScheme = colors,
       content = content
   )
}

如果你现在尝试运行应用,你应该会发现它看起来是一样的。尽管你导入了包含新主题颜色的新配色方案,但你仍然看到基准主题设置,因为你尚未将主题应用于 Compose 应用。

App with baseline theming when no theme is applied.

未应用主题时的应用基准主题设置。

要应用新主题,请在 MainActivity.kt 中,将主可组合项 ReplyApp 封装在主主题函数 AppTheme() 中。

MainActivity.kt

setContent {
   val uiState by viewModel.uiState.collectAsStateWithLifecycle()

   AppTheme {
       ReplyApp(/*..*/)
   }
}

你还将更新预览函数,以查看主题应用于应用预览。将 ReplyAppPreview() 内的 ReplyApp 可组合项封装在 AppTheme 中,以将主题设置应用于预览。

你在预览参数中定义了浅色和深色系统主题,因此你会看到两种预览。

MainActivity.kt

@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_YES,
   name = "DefaultPreviewDark"
)
@Preview(
   uiMode = Configuration.UI_MODE_NIGHT_NO,
   name = "DefaultPreviewLight"
)
@Composable
fun ReplyAppPreview() {
   AppTheme {
       ReplyApp(
           replyHomeUIState = ReplyHomeUIState(
               emails = LocalEmailsDataProvider.allEmails
           )
       )
   }
}

如果你现在运行应用,你应该会看到应用预览中显示的是导入的主题颜色,而不是基准主题。

fddf7b9cc99b1fe3.png be7a661b4553167b.png

基准主题的应用(左)。

导入颜色主题的应用(右)。

674cec6cc12db6a0.png

应用导入颜色主题的浅色和深色应用预览。

Material 3 支持浅色和深色配色方案。你只用导入的主题封装了应用;Material 3 组件正在使用默认颜色角色。

在开始将其添加到应用之前,让我们先了解一下颜色角色和用法。

颜色角色和无障碍功能

每种颜色角色都可以用于各种位置,具体取决于组件的状态、重要性和强调。

1f184a05ea57aa84.png

主要、次要和第三颜色的颜色角色。

Primary 是基础颜色,用于主要组件,如突出按钮和活动状态。

Secondary 关键颜色用于 UI 中不那么突出的组件,例如过滤芯片。

Tertiary 关键颜色用于提供对比强调,中性色用于应用的背景和表面。

Material 的颜色系统提供标准色调值和度量,可用于满足无障碍对比度。在 primary 上使用 on-primary,在 primary-container 上使用 on-primary-container,其他强调色和中性色也类似,为用户提供无障碍对比度。

如需了解更多信息,请参阅颜色角色和无障碍功能

色调和阴影高度

Material 3 主要使用色调颜色叠加层表示高度。这是一种区分容器和表面的新方法 — 增加色调高度会使用更突出的色调 — 除了阴影之外。

带有阴影高度的色调高度 色调高度在级别 2,它从主颜色槽获取颜色。

Material Design 3 中,深色主题中的高度叠加层也已更改为色调颜色叠加层。叠加颜色来自主颜色槽。

M3 Surface — 大多数 M3 组件背后的支持可组合项 — 包括对色调和阴影高度的支持

Surface(
   modifier = modifier,
   tonalElevation = {..}
   shadowElevation = {..}
) {
   Column(content = content)
}

为应用添加颜色

如果你运行应用,可以看到导出的颜色显示在组件采用默认颜色的应用中。现在我们了解了颜色角色和用法,让我们用正确的颜色角色为应用设置主题。

be7a661b4553167b.png

应用颜色主题且组件采用默认颜色角色。

表面颜色

在主屏幕上,你将首先将主应用可组合项封装在 Surface() 中,为应用内容在其上方放置提供基础。打开 MainActivity.kt,并使用 Surface 封装 ReplyApp() 可组合项。

你还将提供 5.dp 的色调高度,为表面赋予主要槽的色调颜色,这有助于与列表项和其上方的搜索栏形成对比。默认情况下,表面的色调和阴影高度为 0.dp。

MainActivity.kt

AppTheme {
   Surface(tonalElevation = 5.dp) {
       ReplyApp(
           replyHomeUIState = uiState,
          // other parameters
         )
   }
}

如果你现在运行应用并查看列表页面和详情页面,你应该会看到色调表面应用于整个应用。

be7a661b4553167b.png e70d762495173610.png

没有表面和色调颜色的应用背景(左)。

应用了表面和色调颜色的应用背景(右)。

应用栏颜色

顶部自定义搜索栏没有设计要求的清晰背景。默认情况下,它会回退到默认基础表面。你可以提供背景以实现清晰分离。

5779fc399d8a8187.png

没有背景的自定义搜索栏(左)。

有背景的自定义搜索栏(右)。

现在你将编辑包含应用栏的 ui/components/ReplyAppBars.kt 文件。你将把 MaterialTheme.colorScheme.background 添加到 Row 可组合项的 Modifier 中。

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

现在你应该看到色调表面和具有背景颜色的应用栏之间有清晰的分离。

b1b374b801dadc06.png

色调表面上方带有背景颜色的搜索栏。

浮动操作按钮颜色

70ceac87233fe466.png

未应用任何主题设置的大型 FAB(左)。

应用了第三颜色的主题大型 FAB(右)。

在主屏幕上,你可以增强浮动操作按钮 (FAB) 的外观,使其作为行动号召按钮脱颖而出。要实现这一点,你将对其应用第三强调色。

ReplyListContent.kt 文件中,将 FAB 的 containerColor 更新为 tertiaryContainer 颜色,并将内容颜色更新为 onTertiaryContainer,以保持无障碍性和颜色对比度。

ReplyListContent.kt

ReplyInboxScreen(/*..*/) {
// Email list content
  LargeFloatingActionButton(
    containerColor = MaterialTheme.colorScheme.tertiaryContainer,
    contentColor = MaterialTheme.colorScheme.onTertiaryContainer
  ){
   /*..*/   
  }
}

运行应用以查看应用了主题的 FAB。在本 Codelab 中,你使用的是 LargeFloatingActionButton

卡片颜色

主屏幕上的电子邮件列表使用了卡片组件。默认情况下,它是一个 Filled card,使用表面变体颜色作为容器颜色,以提供表面和卡片颜色之间的清晰分离。Compose 还提供了 ElevatedCardOutlinedCard 的实现。

你可以通过提供次要颜色色调来进一步突出一些重要项目。你将修改 ui/components/ReplyEmailListItem.kt,使用 CardDefaults.cardColors() 为重要电子邮件更新卡片容器颜色

ReplyEmailListItem.kt

Card(
   modifier =  modifier
       .padding(horizontal = 16.dp, vertical = 4.dp)
       .semantics { selected = isSelected }
       .clickable { navigateToDetail(email.id) },
   colors = CardDefaults.cardColors(
       containerColor = if (email.isImportant)
           MaterialTheme.colorScheme.secondaryContainer
       else MaterialTheme.colorScheme.surfaceVariant
   )
){
  /*..*/   
}

5818200be0b01583.png 9367d40023db371d.png

在色调表面上使用次要容器颜色突出显示列表项。

详细列表项颜色

现在,你已经为你的主屏幕设置了主题。通过点击任何电子邮件列表项来查看详情页面。

7a9ea7cf3e91e9c7.png 79b3874aeca4cd1.png

未应用主题列表项的默认详情页面(左)。

应用了背景主题的详细列表项(右)。

你的列表项没有应用任何颜色,因此回退到默认色调表面颜色。你将为列表项应用背景颜色以创建分隔,并添加内边距以在背景周围提供间距。

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(MaterialTheme.colorScheme.background)
           .padding(20.dp)
    ) {
      // List item content
    }
}

你可以看到,只需提供背景,色调表面和列表项之间就有了清晰的分隔。

现在你有了应用了正确颜色角色和用法的主页和详情页面。让我们看看你的应用如何利用动态颜色提供更个性化和内聚的体验。

5. 在应用中添加动态颜色

动态颜色是 Material 3 的关键部分,其中算法从用户的壁纸中派生出自定义颜色,并应用于其应用和系统 UI。

动态主题设置使你的应用更具个性化。它还为用户提供了与系统主题一致且无缝的体验。

动态颜色在 Android 12 及更高版本上可用。如果动态颜色可用,你可以使用 dynamicDarkColorScheme()dynamicLightColorScheme() 设置动态配色方案。如果不可用,你应该回退到使用默认的浅色或深色 ColorScheme

Theme.kt 文件中 AppTheme 函数的代码替换为以下代码

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
   val context = LocalContext.current
   val colors = when {
       (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> {
           if (useDarkTheme) dynamicDarkColorScheme(context)
           else dynamicLightColorScheme(context)
       }
       useDarkTheme -> DarkColors
       else -> LightColors
   }
   
      MaterialTheme(
       colorScheme = colors,
       content = content
     )
}

fecc63b4c6034236.png

从 Android 13 壁纸获取的动态主题。

如果你现在运行应用,你应该会看到使用默认 Android 13 壁纸应用的动态主题设置。

你可能还希望状态栏根据用于为应用设置主题的配色方案动态设置样式。

1095e2b2c1ffdc14.png

未应用状态栏颜色的应用(左)。

应用了状态栏颜色的应用(右)。

要根据主题的主要颜色更新状态栏颜色,请在 AppTheme 可组合项的配色方案选择后添加状态栏颜色

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean =  isSystemInDarkTheme(),
   content: @Composable () -> Unit
) {
 
 // color scheme selection code

 // Add primary status bar color from chosen color scheme.
 val view = LocalView.current
 if (!view.isInEditMode) {
    SideEffect {
        val window = (view.context as Activity).window
        window.statusBarColor = colors.primary.toArgb()
        WindowCompat
            .getInsetsController(window, view)
            .isAppearanceLightStatusBars = useDarkTheme
    }
 }
   
  MaterialTheme(
    colorScheme = colors,
     content = content
   )
}

运行应用后,你应该会看到状态栏采用你的主颜色主题设置。你还可以通过更改系统深色主题来尝试浅色和深色动态主题设置。

69093b5bce31fd43.png

应用了 Android 13 默认壁纸的动态浅色主题(左)和深色主题(右)。

到目前为止,你已经为应用应用了颜色,从而增强了应用的外观。但是,你可以看到应用中的所有文本大小相同,因此你现在可以为应用添加排版。

6. 排版

Material Design 3 定义了一个类型比例。命名和分组已简化为:display、headline、title、body 和 label,每种类型都有 large、medium 和 small 尺寸。

999a161dcd9b0ec4.png

Material 3 类型比例。

定义排版

Compose 提供了 M3 Typography 类 — 以及现有的 TextStylefont-related 类 — 用于建模 Material 3 类型比例。

Typography 构造函数为每种样式提供默认值,因此你可以省略任何不想自定义的参数。如需了解更多信息,请参阅排版样式及其默认值

你将在应用中使用五种排版样式:headlineSmalltitleLargebodyLargebodyMediumlabelMedium。这些样式将涵盖主屏幕和详情屏幕。

Screen showcasing typography usage of title, label and body style.

展示标题、标签和正文样式排版用法的屏幕。

接下来,转到 ui/theme 软件包并打开 Type.kt。添加以下代码为你的一些文本样式提供自己的实现,而不是使用默认值

Type.kt

val typography = Typography(
   headlineSmall = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 24.sp,
       lineHeight = 32.sp,
       letterSpacing = 0.sp
   ),
   titleLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 18.sp,
       lineHeight = 28.sp,
       letterSpacing = 0.sp
   ),
   bodyLarge = TextStyle(
       fontWeight = FontWeight.Normal,
       fontSize = 16.sp,
       lineHeight = 24.sp,
       letterSpacing = 0.15.sp
   ),
   bodyMedium = TextStyle(
       fontWeight = FontWeight.Medium,
       fontSize = 14.sp,
       lineHeight = 20.sp,
       letterSpacing = 0.25.sp
   ),
   labelMedium = TextStyle(
       fontWeight = FontWeight.SemiBold,
       fontSize = 12.sp,
       lineHeight = 16.sp,
       letterSpacing = 0.5.sp
   )
)

你的排版现在已经定义了。要将其添加到你的主题中,请将其传递给 AppTheme 中的 MaterialTheme() 可组合项

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       content = content
   )
}

使用排版

与颜色一样,你将使用 MaterialTheme.typography 访问当前主题的排版样式。这为你提供了排版实例,以便使用 Type.kt 中定义的所有排版。

Text(
   text = "Hello M3 theming",
   style = MaterialTheme.typography.titleLarge
)

Text(
   text = "you are learning typography",
   style = MaterialTheme.typography.bodyMedium
)

你的产品可能不需要 Material Design 类型比例中的所有 15 种默认样式。在本 Codelab 中,选择了五种尺寸,其余的则省略了。

由于你尚未将排版应用于 Text() 可组合项,因此所有文本默认回退到 Typography.bodyLarge

主页列表排版

接下来,将排版应用于 ui/components/ReplyEmailListItem.kt 文件中的 ReplyEmailListItem 函数,以区分标题和标签

ReplyEmailListItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.titleLarge,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   overflow = TextOverflow.Ellipsis
)

90645c0765167bb7.png 6c4af2f412c18bfb.png

未应用排版的主屏幕(左)。

应用了排版的主屏幕(右)。

详细列表排版

类似地,你将通过更新 ui/components/ReplyEmailThreadItem.kt 文件中 ReplyEmailThreadItem 的所有文本可组合项来在详细信息屏幕中添加排版

ReplyEmailThreadItem.kt

Text(
   text = email.sender.firstName,
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = stringResource(id = R.string.twenty_mins_ago),
   style = MaterialTheme.typography.labelMedium
)

Text(
   text = email.subject,
   style = MaterialTheme.typography.bodyMedium,
   modifier = Modifier.padding(top = 12.dp, bottom = 8.dp),
)

Text(
   text = email.body,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

543ac09e43d8761.png 3412771e95a45f36.png

未应用排版的详细信息屏幕(左)。

应用了排版的详细信息屏幕(右)。

自定义排版

使用 Compose,自定义文本样式或提供自定义字体非常容易。你可以修改 TextStyle 以自定义字体类型、字体家族、字母间距等。

你将更改 theme/Type.kt 文件中的文本样式,这会反映到所有使用它的组件。

titleLargefontWeight 更新为 SemiBold,将 lineHeight 更新为 32.sp,这用于列表项中的主题。这将更强调主题并提供清晰的分隔。

Type.kt

...
titleLarge = TextStyle(
   fontWeight = FontWeight.SemiBold,
   fontSize = 18.sp,
   lineHeight = 32.sp,
   letterSpacing = 0.0.sp
),
...

f8d2212819eb0b61.png

将自定义排版应用于主题文本。

7. 形状

Material 表面可以以不同的形状显示。形状引导注意力,识别组件,传达状态,并表达品牌。

定义形状

Compose 提供了 Shapes 类,具有扩展参数以实现新的 M3 形状。M3 形状比例尺,类似于类型比例尺,可在整个 UI 中实现富有表现力的形状范围。

形状比例尺中有不同尺寸的形状

  • 超小
  • 超大

默认情况下,每个形状都有一个可以被覆盖的默认值。对于你的应用,你将使用中等形状来修改列表项,但你也可以声明其他形状。在 ui/theme 软件包中创建一个名为 Shape.kt 的新文件,并添加形状的代码

Shape.kt

package com.example.reply.ui.theme

import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Shapes
import androidx.compose.ui.unit.dp

val shapes = Shapes(
   extraSmall = RoundedCornerShape(4.dp),
   small = RoundedCornerShape(8.dp),
   medium = RoundedCornerShape(16.dp),
   large = RoundedCornerShape(24.dp),
   extraLarge = RoundedCornerShape(32.dp)
)

现在你已经定义了你的 shapes,将其传递给 M3 MaterialTheme,就像你对颜色和排版所做的那样

Theme.kt

@Composable
fun AppTheme(
   useDarkTheme: Boolean = isSystemInDarkTheme(),
   content: @Composable() () -> Unit
) {
  // dynamic theming content

   MaterialTheme(
       colorScheme = colors,
       typography = typography,
       shapes = shapes,
       content = content
   )
}

使用形状

与颜色和排版一样,你可以使用 MaterialTheme.shape 将形状应用于 Material 组件,这为你提供了 Shape 实例来访问 Material 形状。

许多 Material 组件已经应用了默认形状,但你可以通过可用的槽位为组件提供和应用你自己的形状。

Card(shape = MaterialTheme.shapes.medium) { /* card content */ }
FloatingActionButton(shape = MaterialTheme.shapes.large) { /* fab content */}

所有 Material 3 组件的默认形状值。使用不同类型形状的 Material 组件映射。

你可以在形状文档中查看所有组件的形状映射。

还有另外两种可用的形状 — RectangleShapeCircleShape — 它们是 Compose 的一部分。RectangleShape 没有边框半径,而 CircleShape 显示完全圆形的边缘。

你还可以使用接受形状的 Modifiers 为组件应用形状,例如 Modifier.clipModifier.backgroundModifier.border

应用栏形状

我们希望应用栏具有圆角背景

f873392abe535494.png

TopAppBar 使用带有背景颜色的 Row。为了实现圆角背景,通过将 CircleShape 传递给背景修饰符来定义背景形状

ReplyAppBars.kt

@Composable
fun ReplySearchBar(modifier: Modifier = Modifier) {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(16.dp)
           .background(
               MaterialTheme.colorScheme.background,
               CircleShape
           ),
       verticalAlignment = Alignment.CenterVertically
   ) {
       // Search bar content
   }
}

f873392abe535494.png

详细列表项形状

在主屏幕中,你使用了默认使用 Shape.Medium 的卡片。但是,对于我们的详情页面,你使用了带有背景颜色的 Column。为了实现列表外观的统一性,请对其应用中等形状。

3412771e95a45f36.png 80ee881c41a98c2a.png

列表项上没有形状的详细列表项列(左)和列表上具有中等形状(右)。

ReplyEmailThreadItem.kt

@Composable
fun ReplyEmailThreadItem(
   email: Email,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
           .fillMaxWidth()
           .padding(8.dp)
           .background(
               MaterialTheme.colorScheme.background,
               MaterialTheme.shapes.medium
           )
           .padding(16.dp)

   ) {
      // List item content
      
   }
}

现在,运行你的应用,你会看到一个详细屏幕列表项,其形状为 medium

8. 强调

UI 中的强调有助于你突出某些内容,例如当你想区分标题和副标题时。M3 中的强调使用颜色及其搭配颜色的变体。你有两种方法来添加强调

  1. 使用 surface、surface-variant 和 background,以及来自扩展 M3 颜色系统的 on-surface 和 on-surface-variants 颜色。

例如,surface 可以与 on-surface-variant 一起使用,surface-variant 可以与 on-surface 一起使用,以提供不同级别的强调。

表面变体也可以与强调色一起使用,以提供比 on-accent 颜色更低的强调,但仍然具有可访问性并遵循对比度。

Surface, Background, and Surface variant color roles.

表面、背景和表面变体颜色角色。

  1. 为文本使用不同的字体粗细。正如你在排版部分看到的那样,你可以为你的类型比例提供自定义粗细,以提供不同的强调。

接下来,更新 ReplyEmailListItem.kt 以使用表面变体提供强调差异。默认情况下,卡片的内容根据背景采用默认内容颜色。

你将把时间文本和正文文本可组合项的颜色更新为 onSurfaceVariant。这降低了其强调,与默认应用于主题和标题文本可组合项的 onContainerColors 相比。

2c9b7f2bd016edb8.png 6850ff391f21e4ba.png

时间文本和正文文本与主题和标题强调相同(左)。

时间文本和正文文本与主题和标题强调降低(右)。

ReplyEmailListItem.kt

Text(
   text = email.createdAt,
   style = MaterialTheme.typography.labelMedium,
   color = MaterialTheme.colorScheme.onSurfaceVariant
)

Text(
   text = email.body,
   maxLines = 2,
   style = MaterialTheme.typography.bodyLarge,
   color = MaterialTheme.colorScheme.onSurfaceVariant,
   overflow = TextOverflow.Ellipsis
)

对于背景为 secondaryContainer 的重要电子邮件卡片,所有文本颜色默认为 onSecondaryContainer 颜色。对于其他电子邮件,背景是 surfaceVariant,因此所有文本默认采用 onSurfaceVariant 颜色。

9. 恭喜

恭喜!你已成功完成此 Codelab!你已使用颜色、排版和形状以及动态颜色在 Compose 中实现了 Material 主题设置,为你的应用设置主题并提供个性化体验。

2d8fcabf15ac5202.png 5a4d31db0185dca6.png ce009e4ce560834d.png

应用了动态颜色和颜色主题的主题设置结果。

下一步

查看 Compose 学习路径上的其他 Codelab

延伸阅读

示例应用

  • 使用完整的 Material 3 主题设置的Reply 示例应用
  • 展示动态主题设置的JetChat

参考文档