使用 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 中运行这两个版本中的任何一个。

探索起始代码

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

  • MainActivity.kt – 您启动 Reply 应用的入口点活动。
  • com.example.reply.ui.theme – 此包包含主题、排版和颜色方案。您将在此包中添加 Material 主题设置。
  • com.example.reply.ui.components – 包含应用的自定义组件,如列表项、应用栏等。您将对这些组件应用主题。
  • ReplyApp.kt – 这是我们的主要 Composable 函数,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生成的文

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 Composable。

您将MaterialTheme()Composable 包裹在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(/*..*/)
   }
}

您还需要更新预览函数以查看应用于应用预览的主题。将 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

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

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

**次要**关键颜色用于 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 的实现。

您可以通过提供次要颜色色调进一步突出显示一些重要的项目。您将通过使用 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

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

详细信息列表项颜色

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

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 类(以及现有的TextStyle与字体相关的 类)来模拟 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
   )
)

您的排版现在已定义。要将其添加到您的主题中,请将其传递给 MaterialTheme() 可组合函数,该函数位于 AppTheme 内部。

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

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

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。

进一步阅读

示例应用

参考文档