使用 Material 3 在 Compose 中设置主题

1. 简介

在本 Codelab 中,您将学习如何使用 Material Design 3 在 Jetpack Compose 中为应用设置主题。您还将了解 Material Design 3 配色方案、排版和形状的关键组成要素,这有助于您以个性化且易于使用的方式为应用设置主题。

此外,您将探索如何支持动态主题以及不同程度的强调效果。

学习内容

在此 Codelab 中,您将学习:

  • Material 3 主题设置的关键要素
  • Material 3 配色方案以及如何为应用生成主题
  • 如何让应用支持动态和浅色/深色主题
  • 利用排版和形状对应用进行个性化设置
  • Material 3 组件和自定义应用样式

构建内容

在本 Codelab 中,您将为一个名为 Reply 的电子邮件客户端应用设置主题。您将从一个使用基准主题、未设计样式的应用入手,运用所学到的知识来为应用设置主题并让应用支持深色主题。

d15db3dc75a9d00f.png

我们的应用以基准主题为默认起点。

您需要使用配色方案、排版和形状创建主题,然后将其应用于应用的电子邮件列表和详情页面。此外,您还需要让应用支持动态主题。完成本 Codelab 后,您的应用将同时支持颜色和动态主题。

Material 3 浅色质感

在主题设置 Codelab 的最后,应用采用浅色主题和浅色动态主题的效果。

Material 3 深色质感

主题设置 Codelab 结束时,应用采用深色主题设置并且支持深色动态主题。

所需条件

2. 准备工作

在此步骤中,您需要下载将在此 Codelab 中设置样式的 Reply 应用的完整代码。

获取代码

此 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 中运行其中任一版本。

探索起始代码

主代码包含一个界面软件包,其中包含您将与之互动的主要软件包和文件:

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

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

3. Material 3 主题设置

Jetpack Compose 提供了 Material Design 的实现,后者是一个用于创建数字化界面的综合设计体系。Material Design 组件(按钮、卡片、开关等)在 Material 主题设置的基础上构建而成,Material 主题设置是一种系统化的方法,用于自定义 Material Design 以更好地反映您产品的品牌。

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

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

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

4. 配色方案

配色方案的基础是五种关键颜色,其中每种颜色都对应 Material 3 组件使用的 13 色调调色板之一。

创建 M3 主题所需的五种关键基准颜色。

创建 M3 主题所需的五种关键基准颜色。

然后,每种强调色(主色、辅色和第三色)都会提供四种不同色调的兼容色,以便进行配对、定义强调效果和视觉表现。

基准强调色的主色、辅色和第三色的四种色调。

基准强调色的主色、辅色和第三色的四种色调。

同样,中性色也分为四种兼容的色调,用于 Surface 和背景。当这些颜色被应用于任何 Surface 时,它们对于突出文本图标也非常重要。

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

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

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

生成配色方案

虽然您可以手动创建自定义 ColorScheme,但使用品牌中的源颜色通常更容易生成配色方案。您可以使用 Material 主题构建器工具执行此操作,并且可以选择导出 Compose 主题代码。

您可以随意选择您喜欢的颜色,但对于我们的应用场景,您将使用默认的 Reply 主色 #825500。点击左侧核心颜色部分中的主色,然后在颜色选择器中添加代码。

294f73fc9d2a570e.png

在 Material 主题构建器中添加了主色代码。

在 Material 主题构建器中添加主色后,您应该能看到以下主题以及右上角的导出选项。对于本 Codelab,您需要以 Jetpack Compose 的方式导出主题。

Material 主题构建器的右上角显示导出选项。

Material 主题构建器的右上角显示导出选项。

主色 #825500 会生成您将添加到应用中的以下主题。Material 3 提供了各种颜色角色,用于灵活表现组件的状态、显眼程度和强调效果。

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

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

生成的 The 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)

生成的 The 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 应用。

未应用任何主题时以基准主题样式显示的应用。

未应用任何主题时以基准主题样式显示的应用。

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

MainActivity.kt

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

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

此外,您还将更新预览函数,以查看应用于应用预览的主题。使用 AppThemeReplyApp 可组合项封装在 ReplyAppPreview() 中,以便将主题应用于预览。

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

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

主色、辅色和第三色的颜色角色。

主色是基础颜色,用于主要组件,例如显眼的按钮和活动状态。

辅色用于界面中不太显眼的组件,例如过滤组件。

第三色用于提供对比鲜明的强调色,中性色用于应用中的背景和 Surface。

Material 的颜色系统提供标准的色调值和测量方法,可用于实现方便轻松查看的颜色对比度。在主色上面使用 on-primary,在主容器上使用 on-primary-container,对于其他强调色和中性色也做同样的处理,以向用户提供能够感知的颜色对比度。

如需了解详情,请参阅颜色角色和无障碍

色调和阴影高度

Material 3 主要使用色调颜色叠加叠加层来表示高度。这是一种新的方式,用于区分容器和 Surface,增加的色调高度除了使用阴影外,还使用更突出的色调。

色调高度搭配阴影高度处于第 2 级的色调高度,颜色取自主要颜色槽。

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

M3 Surface 是大多数 M3 组件的后备可组合项,同时支持色调和阴影高度:

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

向应用添加颜色

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

be7a661b4553167b.png

应用采用色彩主题,而组件采用默认颜色角色

Surface 颜色

在主页面中,您将主应用可组合项封装在 Surface() 中,为应用提供基础界面,以便在上面放置应用内容。打开 MainActivity.kt 并使用 Surface 封装 ReplyApp() 可组合项。

此外,您还将提供 5.dp 的色调高度,给 Surface 提供主色槽的色调色彩,这有助于与列表项和其顶部的搜索栏形成对比。默认情况下,Surface 的色调和阴影高度为 0.dp。

MainActivity.kt

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

如果您现在运行应用并查看“列表”和“详细信息”页面,应该会看到整个应用都应用了色调 Surface。

be7a661b4553167b.png e70d762495173610.png

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

已应用 Surface 和色调颜色的应用背景(右)。

应用栏颜色

我们自定义的顶部搜索栏并没有设计要求的明确背景。默认情况下,它会采用默认的基础 Surface。您可以提供一个背景,以实现清晰分隔。

5779fc399d8a8187.png

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

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

您现在需要修改 ui/components/ReplyAppBars.kt,其中包含应用栏。您将向 Row 可组合项的 Modifier 添加 MaterialTheme.colorScheme.background

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
   }
}

现在,您会发现色调 Surface 与带有背景颜色的应用栏之间有清晰的分隔。

b1b374b801dadc06.png

带有背景颜色的搜索栏位于色调 Surface 之上。

悬浮操作按钮颜色

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

卡片颜色

主页面上的电子邮件列表使用的是一个卡片组件。默认情况下,它是“填充”卡片,使用 Surface 变体颜色作为容器颜色,以醒目方式区分 Surface 和卡片颜色。Compose 还提供了 ElevatedCardOutlinedCard 的实现。

您可以通过提供辅色色调进一步突出显示某些重要的项目。针对重要电子邮件,您将使用 CardDefaults.cardColors() 更新卡片容器颜色,以修改 ui/components/ReplyEmailListItem.kt

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

在色调 Surface 上使用辅助容器颜色突出显示列表项。

详情列表项颜色

现在,您已经为主页面完成了主题设置。点击任意电子邮件列表项即可查看详情页面。

7a9ea7cf3e91e9c7.png 79b3874aeca4cd1.png

列表项不带主题的默认详情页面(左)。

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

您的列表项未应用任何颜色,因此会回退采用色调 Surface 的默认颜色。您将为列表项应用背景颜色,以分隔内容并添加内边距,以呈现背景周围的间距。

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
    }
}

您可以看出,只需提供背景,即可清晰地分隔色调 Surface 和列表项。

现在,您已经使用正确的颜色角色和用法对首页和详情页面进行了主题设置。接下来,让我们看看您的应用如何利用动态配色提供更加个性化且一致的体验。

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

动态颜色是 Material 3 的关键部分,使用此功能时,算法会从用户的壁纸中派生自定义颜色,以将其应用到其应用和系统界面。

动态主题可让您的应用更加个性化。它还能为用户提供与系统主题一致的顺畅体验。

动态颜色适用于 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 定义了一个字体比例。命名和分组已简化为:显示、大标题、标题、正文和标签,每个都有大号、中号和小号。

999a161dcd9b0ec4.png

Material 3 字体比例。

定义排版

Compose 提供了 M3 Typography 类以及现有的 TextStylefont-related 类,用以对 Material 3 字体比例进行建模。

Typography 构造函数提供每种样式的默认值,因此您可以省略不希望自定义的任何参数。如需了解详情,请参阅排版样式及其默认值

您将在应用中使用五种排版样式:headlineSmalltitleLargebodyLargebodyMediumlabelMedium。这些样式将涵盖主页面和详情页面。

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

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

接下来,转到 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.ktt 中定义的所有排版。

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.ktReplyEmailThreadItem 的所有文本可组合项:

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 Surface 可以用不同的形状显示。形状能够引导用户注意力、区别组件、传达状态以及表达品牌。

定义形状

Compose 提供带有扩展参数的 Shapes 类来实现新的 M3 形状。M3 形状比例与字体比例类似,能够在整个界面中呈现丰富多样的形状。

形状比例中包含不同大小的形状:

  • 特小
  • 中号
  • 特大

默认情况下,每个形状都有一个可以被覆盖的默认值。对于您的应用,您可以使用中等形状来修改列表项,不过您也可以声明其他形状。在 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 组件使用不同类型的形状。

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

在 Compose 中,您还可以使用另外两种形状(RectangleShapeCircleShape)。矩形没有边框半径,圆形则会显示完整的圆角边缘。

您还可以使用能接受形状的 Modifiers 将形状应用于组件,例如使用 Modifier.clipModifier.background,以及Modifier.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 的卡片。不过,在我们的详情页面中,您却使用了带有背景颜色的列。为了统一列表的外观,请为其应用中等形状。

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. 强调效果

界面中的强调功能有助于突出显示某些内容,例如区分标题与字幕。M3 中的强调效果会使用各种颜色变体及其颜色组合。您可以通过以下两种方式添加强调效果:

  1. 同时使用 Surface、surface-variant 和背景以及扩展 M3 颜色系统中的 on-surface 和 on-surface-variants 颜色。

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

Surface 变体还可与强调色一起使用,以实现比 on-accent 颜色更轻一些的强调效果,但仍可访问且符合对比度。

Surface、背景 和 Surface 变体颜色角色。

Surface、背景 和 Surface 变体颜色角色。

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

接下来,更新 ReplyEmailListItem.kt,以使用 Surface 变体呈现强调效果差异。默认情况下,卡片的内容采用默认的内容颜色,具体取决于背景。

您需要将时间文本和正文文本可组合项的颜色更新为 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:

深入阅读

示例应用

参考文档