1. 前言
Material Design 是 Google 设计人员和开发者打造的一套设计体系,旨在帮助开发者针对 Android 以及其他移动平台和网络平台打造优质的数字体验。它提供了一些准则,指导如何以具有可读性、吸引力和一致性的方式构建应用界面。
在此 Codelab 中,您将学习 Material 主题设置,获得自定义颜色、排版和形状方面的指导,以便在您的应用中使用 Material Design。您可以根据应用的需要进行自定义。您还将学习如何添加顶部应用栏,以显示应用的名称和图标。
前提条件
- 熟悉 Kotlin 语言,包括语法、函数和变量。
- 能够在 Compose 中构建布局,包括带内边距的行和列。
- 能够在 Compose 中创建简单列表。
学习内容
- 如何将 Material 主题设置应用于 Compose 应用。
- 如何为您的应用添加自定义调色板。
- 如何为您的应用添加自定义字体。
- 如何向应用中的元素添加自定义形状。
- 如何向应用中添加顶部应用栏。
构建内容
- 您将构建一个遵循 Material Design 最佳实践的精美应用。
所需条件
- 最新版本的 Android Studio。
- 用于下载起始代码和字体的互联网连接。
2. 应用概览
在此 Codelab 中,您将创建 Woof,该应用会显示狗狗列表,并使用 Material Design 打造出色的应用体验。

在此 Codelab 中,我们将向您展示使用 Material 主题设置可实现的一些设计。通过此 Codelab,您可以了解到日后如何使用 Material 主题设置来改进应用的外观和风格。
调色板
以下是我们将创建的浅色和深色主题的调色板。


以下是采用浅色主题和深色主题的最终应用。
浅色主题 | 深色主题 |
|
|
排版
以下是您将在应用中使用的字型样式。

主题文件
Theme.kt 会存储所有关于应用主题的信息,这些信息通过颜色、排版和形状进行定义。这是一个需要您了解的重要文件。该文件内是可组合项 WoofTheme(),用于设置应用的颜色、排版和形状。
@Composable
fun WoofTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColors
else -> LightColors
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
setUpEdgeToEdge(view, darkTheme)
}
}
MaterialTheme(
colorScheme = colorScheme,
shapes = Shapes,
typography = Typography,
content = content
)
}
/**
* Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
* light or dark depending on whether the [darkTheme] is enabled or not.
*/
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb()
val navigationBarColor = when {
Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
// Min sdk version for this app is 24, this block is for SDK versions 24 and 25
else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
}
window.navigationBarColor = navigationBarColor
val controller = WindowCompat.getInsetsController(window, view)
controller.isAppearanceLightStatusBars = !darkTheme
controller.isAppearanceLightNavigationBars = !darkTheme
}
在 MainActivity.kt 中,添加了 WoofTheme() 以便为整个应用提供 Material 主题设置。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WoofTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
WoofApp()
}
}
}
}
}
查看 WoofPreview()。添加了 WoofTheme(),以提供您在 WoofPreview() 中看到的 Material 主题设置。
@Preview
@Composable
fun WoofPreview() {
WoofTheme(darkTheme = false) {
WoofApp()
}
}
3. 获取起始代码
首先,请下载起始代码:
或者,您也可以克隆该代码的 GitHub 代码库:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git $ cd basic-android-kotlin-compose-training-woof $ git checkout starter
您可以在 Woof app GitHub 仓库中浏览该代码。
查看起始代码
- 在 Android Studio 中打开起始代码。
- 依次打开 com.example.woof > data > Dog.kt。其中包含用于代表狗狗的照片、名字、年龄和爱好的
Dog data class。它还包含狗狗列表以及您将在应用中用作数据的信息。 - 依次打开 res > drawable。此文件包含此项目所需的所有图片资源,包括应用图标、狗狗图片和图标。
- 依次打开 res > values > strings.xml。此文件包含您在此应用中使用的字符串,包括应用名称、狗狗名字、说明等。
- 打开 MainActivity.kt。此文件包含创建简单列表的代码,用于显示狗狗的照片、名字及年龄。
WoofApp()包含用于显示DogItem的LazyColumn。DogItem()包含用于显示狗狗照片及相关信息的Row。DogIcon()显示小狗的照片。DogInformation()显示狗狗的名字和年龄。WoofPreview()可让您在 Design 窗格中预览应用。
确保您的模拟器/设备采用浅色主题
在此 Codelab 中,您将使用浅色主题和深色主题,不过,此 Codelab 中的大部分主题都采用浅色主题。开始之前,请确保您的设备/模拟器使用的是浅色主题。
如需在浅色主题下查看您的应用,请在模拟器或实体设备上执行以下操作:
- 打开设备的设置应用。
- 搜索深色主题,然后点击进入该主题。
- 如果深色主题处于开启状态,请将其关闭。
运行起始代码以查看应用的起始状态;这是一个列表,其中会显示狗狗的照片、名字和年龄。它能正常运行,但外观不好看,所以我们会解决该问题。

4. 添加颜色
在 Woof 应用中,您首先要修改的就是配色方案。
配色方案是您的应用使用的颜色组合。不同的颜色组合会激发不同的心情,这会影响用户使用您的应用时的感受。
在 Android 系统中,颜色用十六进制颜色值表示。十六进制颜色代码以井号 (#) 字符开头,后跟六个字母和/或数字(代表该颜色的红色、绿色和蓝色 [RGB] 分量)。前两个字母/数字表示红色,后面的两个表示绿色,最后两个表示蓝色。

颜色还可以包含 Alpha 值(字母和/或数字),用于表示颜色的透明度(#00 表示不透明度为 0% [完全透明],#FF 表示不透明度为 100% [完全不透明])。若添加 alpha 值,则该值为井号 (#) 字符后的十六进制颜色代码的前两个字符。如果未添加 alpha 值,系统会假定它是 #FF,即 100% 不透明(完全不透明)。
以下是一些颜色及其十六进制值的示例。

使用 Material Theme Builder 创建配色方案
为了创建应用的自定义配色方案,我们将使用 Material Theme Builder。
- 点击此处链接即可打开 Material Theme Builder。
- 在左侧窗格中,您会看到“Core Colors”(核心颜色),然后点击“Primary”(主色):

- 系统会打开 HCT 颜色选择器。

- 若要创建应用屏幕截图中显示的配色方案,您需要更改此颜色选择器中的主色。在文本框中,将当前文本替换为 #006C4C。这样,应用的主色就会变为绿色。

请注意这是如何将屏幕上的应用更新为采用绿色配色方案的。

- 向下滚动页面,您会看到系统根据您输入的颜色生成的浅色和深色主题的完整配色方案。


您可能想了解所有这些角色的含义及其使用方法,以下是其中几个主要角色:
- primary(主色)用于整个界面的关键组件。
- secondary(副色)用于界面中不太显眼的组件。
- tertiary(第三色)用于对比强调,可以平衡主色和副色,或者引起用户对某个元素(例如输入字段)的高度关注。
- on 颜色元素显示在调色板中其他颜色的上层,主要应用于文本、图标和描边。在调色板中,我们有一个 onSurface 颜色(显示在 surface 颜色的上层)和一个 onPrimary 颜色(显示在主要颜色的上层)。
有了这些槽可以实现统一的设计体系,相关组件的颜色也相似。
关于颜色的理论讲解到此为止 - 现在可以向应用中添加这个漂亮的调色板了!
向主题添加调色板
在 Material Theme Builder 页面上,您可以选择点击 Export(导出)按钮,以便下载 Color.kt 文件以及包含您在 Theme Builder 中所建自定义主题的 Theme.kt 文件。
这样一来,我们创建的自定义主题便会添加到您的应用中。不过,由于生成的 Theme.kt 文件不包含动态颜色的代码(稍后我们会在此 Codelab 中介绍),因此请将这些文件复制进来。
- 打开 Color.kt 文件,并将其内容替换为以下代码,以复制新的配色方案。
package com.example.woof.ui.theme
import androidx.compose.ui.graphics.Color
val md_theme_light_primary = Color(0xFF006C4C)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFF89F8C7)
val md_theme_light_onPrimaryContainer = Color(0xFF002114)
val md_theme_light_secondary = Color(0xFF4D6357)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFCFE9D9)
val md_theme_light_onSecondaryContainer = Color(0xFF092016)
val md_theme_light_tertiary = Color(0xFF3D6373)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFC1E8FB)
val md_theme_light_onTertiaryContainer = Color(0xFF001F29)
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(0xFFFBFDF9)
val md_theme_light_onBackground = Color(0xFF191C1A)
val md_theme_light_surface = Color(0xFFFBFDF9)
val md_theme_light_onSurface = Color(0xFF191C1A)
val md_theme_light_surfaceVariant = Color(0xFFDBE5DD)
val md_theme_light_onSurfaceVariant = Color(0xFF404943)
val md_theme_light_outline = Color(0xFF707973)
val md_theme_light_inverseOnSurface = Color(0xFFEFF1ED)
val md_theme_light_inverseSurface = Color(0xFF2E312F)
val md_theme_light_inversePrimary = Color(0xFF6CDBAC)
val md_theme_light_shadow = Color(0xFF000000)
val md_theme_light_surfaceTint = Color(0xFF006C4C)
val md_theme_light_outlineVariant = Color(0xFFBFC9C2)
val md_theme_light_scrim = Color(0xFF000000)
val md_theme_dark_primary = Color(0xFF6CDBAC)
val md_theme_dark_onPrimary = Color(0xFF003826)
val md_theme_dark_primaryContainer = Color(0xFF005138)
val md_theme_dark_onPrimaryContainer = Color(0xFF89F8C7)
val md_theme_dark_secondary = Color(0xFFB3CCBE)
val md_theme_dark_onSecondary = Color(0xFF1F352A)
val md_theme_dark_secondaryContainer = Color(0xFF354B40)
val md_theme_dark_onSecondaryContainer = Color(0xFFCFE9D9)
val md_theme_dark_tertiary = Color(0xFFA5CCDF)
val md_theme_dark_onTertiary = Color(0xFF073543)
val md_theme_dark_tertiaryContainer = Color(0xFF244C5B)
val md_theme_dark_onTertiaryContainer = Color(0xFFC1E8FB)
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(0xFF191C1A)
val md_theme_dark_onBackground = Color(0xFFE1E3DF)
val md_theme_dark_surface = Color(0xFF191C1A)
val md_theme_dark_onSurface = Color(0xFFE1E3DF)
val md_theme_dark_surfaceVariant = Color(0xFF404943)
val md_theme_dark_onSurfaceVariant = Color(0xFFBFC9C2)
val md_theme_dark_outline = Color(0xFF8A938C)
val md_theme_dark_inverseOnSurface = Color(0xFF191C1A)
val md_theme_dark_inverseSurface = Color(0xFFE1E3DF)
val md_theme_dark_inversePrimary = Color(0xFF006C4C)
val md_theme_dark_shadow = Color(0xFF000000)
val md_theme_dark_surfaceTint = Color(0xFF6CDBAC)
val md_theme_dark_outlineVariant = Color(0xFF404943)
val md_theme_dark_scrim = Color(0xFF000000)
- 打开 Theme.kt 文件,并将其内容替换为以下代码,以为主题添加新的颜色。
package com.example.woof.ui.theme
import android.app.Activity
import android.os.Build
import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
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 WoofTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColors
else -> LightColors
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
setUpEdgeToEdge(view, darkTheme)
}
}
MaterialTheme(
colorScheme = colorScheme,
shapes = Shapes,
typography = Typography,
content = content
)
}
/**
* Sets up edge-to-edge for the window of this [view]. The system icon colors are set to either
* light or dark depending on whether the [darkTheme] is enabled or not.
*/
private fun setUpEdgeToEdge(view: View, darkTheme: Boolean) {
val window = (view.context as Activity).window
WindowCompat.setDecorFitsSystemWindows(window, false)
window.statusBarColor = Color.Transparent.toArgb()
val navigationBarColor = when {
Build.VERSION.SDK_INT >= 29 -> Color.Transparent.toArgb()
Build.VERSION.SDK_INT >= 26 -> Color(0xFF, 0xFF, 0xFF, 0x63).toArgb()
// Min sdk version for this app is 24, this block is for SDK versions 24 and 25
else -> Color(0x00, 0x00, 0x00, 0x50).toArgb()
}
window.navigationBarColor = navigationBarColor
val controller = WindowCompat.getInsetsController(window, view)
controller.isAppearanceLightStatusBars = !darkTheme
controller.isAppearanceLightNavigationBars = !darkTheme
}
在 WoofTheme() 中,colorScheme val 使用了 when 语句
- 如果
dynamicColor为 true,并且 build 版本为 S 或更高,则检查设备是否采用darkTheme。 - 如果采用深色主题,
colorScheme会设为dynamicDarkColorScheme。 - 如果没有采用深色主题,则会设为
dynamicLightColorScheme。 - 如果应用未使用
dynamicColorScheme,则检查该应用是否采用darkTheme。如果采用,那么colorScheme会设为DarkColors。 - 如果这两种情况都不是,则
colorScheme会设为LightColors。
复制进来的 Theme.kt 文件将 dynamicColor 设为了 false,而我们在使用的设备采用了浅色模式,因此 colorScheme 将设为 LightColors。
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColors
else -> LightColors
}
- 重新运行应用,请注意,应用栏已自动更改颜色。

颜色映射
Material 组件会自动映射到颜色槽。整个界面中的其他关键组件(例如悬浮操作按钮)也默认为主色。也就
