使用 Material 3 进行 Compose 主题设置

1. 简介

在本 Codelab 中,您将学习如何在 Jetpack Compose 中使用 Material Design 3 对您的应用进行主题设置。您还将了解 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 中运行这两个版本中的任何一个。

探索起始代码

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

  • MainActivity.kt – 您启动 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 主题设置 的基础上,这是一种系统化的方法,可以自定义 Material Design 以更好地反映您的产品的品牌。

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

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

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

4. 色彩方案

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

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 主题构建器 工具允许您执行此操作,并可以选择导出 Compose 主题设置代码。

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

294f73fc9d2a570e.png

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

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

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

Material 主题构建器,右上角提供导出选项。

主色 #825500 生成了以下主题,您将将其添加到应用中。Material 3 提供了广泛的颜色角色,可以灵活地表达组件的状态、突出程度和强调。

Exported Light and Dark color scheme from primary color.

从主色导出的亮色和暗色色彩方案。

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 应用程序。

App with baseline theming when no theme is applied.

未应用主题时,应用程序使用基本主题。

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

MainActivity.kt

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

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

您还将更新预览函数以查看应用于应用程序预览的主题。将 ReplyApp 可组合项包装在 ReplyAppPreview() 中,并使用 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

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

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

次要关键颜色用于 UI 中不太突出的组件,例如筛选器芯片。

三级关键颜色用于提供对比鲜明的强调色,中性色用于应用程序中的背景和表面。

Material 的颜色系统提供标准的 色调值 和测量值,可用于满足可访问的对比度比率。在主要颜色之上使用 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 并将 ReplyApp() 可组合项包装在 Surface 中。

您还将提供 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,其中包含应用栏。您将在 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
   }
}

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

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。对于此代码实验室,您正在使用 LargeFloatingActionButton

卡片颜色

主屏幕上的电子邮件列表使用卡片组件。默认情况下,它是一个 填充卡片,它使用表面变体颜色作为容器颜色,以便在表面和卡片颜色之间提供清晰的分隔。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 定义了一个字体比例尺。命名和分组已简化为:显示、标题、主体和标签,每个都有大、中、小三种尺寸。

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.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 文件中更改文本样式,这将反映到使用它的所有组件中。

fontWeight 更新为 SemiBold,并将 lineHeight 更新为 32.sp 用于 titleLarge,该样式用于列表项中的主题。它将更加强调主题并提供清晰的分隔。

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 的一部分。矩形形状没有圆角,圆形形状显示完整的圆形边缘。

您还可以使用采用形状的 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. 使用扩展的 M3 色彩系统中的表面、表面变体和背景以及对应表面和对应表面变体颜色。

例如,表面可以与对应表面变体一起使用,而表面变体可以与对应表面一起使用,以提供不同级别的强调。

表面变体还可以与强调色一起使用,以提供比对应强调色更少的强调,但仍然可以访问并遵循对比度比率。

Surface, Background, and Surface variant color roles.

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

  1. 对文本使用不同的字体粗细。如您在排版部分中所见,您可以为字体比例尺提供自定义粗细,以提供不同的强调。

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

您将时间文本和正文文本可组合函数的颜色更新为 onSurfaceVariant。这降低了其相对于 onContainerColors 的强调程度,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

进一步阅读

示例应用

参考文档