1. 准备工作
Material Design 是由 Google 设计师和开发者构建和支持的设计系统,旨在为 Android 以及其他移动和 Web 平台构建高质量的数字体验。它提供了有关如何以易读、美观和一致的方式构建应用界面的指导。
在此 Codelab 中,您将学习 Material Theming,它允许您在应用中使用 Material Design,并提供有关自定义颜色、排版和形状的指导。您可以根据需要自定义应用的各个方面。您还将学习如何添加顶部应用栏以显示应用的名称和图标。
前提条件
- 熟悉 Kotlin 语言,包括语法、函数和变量。
- 能够在 Compose 中构建布局,包括带有内边距的行和列。
- 能够在 Compose 中创建简单的列表。
您将学到什么
- 如何将 Material Theming 应用于 Compose 应用。
- 如何为您的应用添加自定义调色板。
- 如何为您的应用添加自定义字体。
- 如何为应用中的元素添加自定义形状。
- 如何为您的应用添加顶部应用栏。
您将构建什么
- 您将构建一个精美的应用,其中融入了 Material Design 的最佳实践。
您需要什么
- 最新版本的 Android Studio。
- 互联网连接,用于下载起始代码和字体。
2. 应用概览
在此 Codelab 中,您将创建 Woof,这是一个显示狗列表并使用 Material Design 打造精美应用体验的应用。
通过此 Codelab,我们将向您展示使用 Material Theming 的一些可能性。使用此 Codelab 获取关于如何使用 Material Theming 改进未来创建的应用外观和感受的灵感。
调色板
下面是我们将创建的明亮和深色主题的调色板。
这是最终应用在明亮主题和深色主题下的样子。
明亮主题 | 深色主题 |
排版
下面是您将在应用中使用的类型样式。
主题文件
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 的大部分内容都使用明亮主题。开始之前,请确保您的设备/模拟器处于明亮主题。
为了在模拟器或实体设备上以明亮主题查看您的应用
- 转到设备上的 Settings(设置)应用。
- 搜索 Dark theme(深色主题)并点击进入。
- 如果 Dark theme(深色主题)已开启,请将其关闭。
运行起始代码,看看您开始时的样子;它是一个显示狗及其照片、名字和年龄的列表。它功能正常,但看起来不太好,所以我们将修复它。
4. 添加颜色
您将在 Woof 应用中修改的第一件事是配色方案。
配色方案是您的应用使用的颜色组合。不同的颜色组合会唤起不同的情绪,从而影响用户使用应用时的感受。
在 Android 系统中,颜色由十六进制 (hex) 颜色值表示。十六进制颜色代码以井号 (#) 开头,后跟六个字母和/或数字,代表该颜色的红、绿、蓝 (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 颜色(出现在 primary 颜色的顶部)。
拥有这些槽位会形成一个连贯的设计系统,其中相关的组件颜色相似。
关于颜色的理论讲够了——现在是时候将这个美丽的调色板添加到应用中了!
将调色板添加到主题
在 Material Theme Builder 页面上,可以选择点击 Export(导出)按钮,下载包含您在 Theme Builder 中创建的自定义主题的 Color.kt 文件和 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 且构建版本为 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 组件会自动映射到颜色槽位。其他关键的 UI 组件,如浮动操作按钮,也默认使用 Primary 颜色。这意味着您无需显式为组件分配颜色;当您在应用中设置颜色主题时,它会自动映射到颜色槽位。您可以通过在代码中显式设置颜色来覆盖此设置。在此处阅读更多关于颜色角色的信息。
在本节中,我们将使用 Card
包裹包含 DogIcon()
和 DogInformation()
的 Row
,以区分列表项颜色与背景。
- 在
DogItem()
可组合函数中,使用Card()
包裹Row()
。
Card() {
Row(
modifier = modifier
.fillMaxWidth()
.padding(dimensionResource(id = R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
}
}
- 由于
Card
现在是DogItem()
中的第一个子可组合函数,因此将DogItem()
中的 modifier 传递给Card
,并将Row
的 modifier 更新为Modifier
的新实例。
Card(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(dimensionResource(id = R.dimen.padding_small))
) {
DogIcon(dog.imageResourceId)
DogInformation(dog.name, dog.age)
}
}
- 查看
WoofPreview()
。由于Card
可组合函数,列表项现在自动更改了颜色。颜色看起来不错,但列表项之间没有间距。
尺寸文件
就像您使用 strings.xml 来存储应用中的字符串一样,使用名为 dimens.xml 的文件来存储尺寸值也是一种好的做法。这很有帮助,这样您就不会硬编码值,并且如果需要,可以在一个地方更改它们。
转到 app > res > values > dimens.xml 并查看该文件。它存储了 padding_small
、padding_medium
和 image_size
的尺寸值。这些尺寸将在整个应用中使用。
<resources>
<dimen name="padding_small">8dp</dimen>
<dimen name="padding_medium">16dp</dimen>
<dimen name="image_size">64dp</dimen>
</resources>
要添加来自 dimens.xml 文件的值,请使用以下正确格式
例如,要添加 padding_small
,您将传入 dimensionResource(id = R.dimen.
padding_small
)
。
- 在
WoofApp()
中,在调用DogItem()
时,添加一个带有padding_small
的modifier
。
@Composable
fun WoofApp() {
Scaffold { it ->
LazyColumn(contentPadding = it) {
items(dogs) {
DogItem(
dog = it,
modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)
}
}
}
}
现在在 WoofPreview()
中,列表项之间的界限更加清晰。
深色主题
在 Android 系统中,可以选择将设备切换到深色主题。深色主题使用更深、更柔和的颜色,并且
- 可以显著降低电量消耗(取决于设备的屏幕技术)。
- 改善视力低下和对强光敏感的用户的使用体验。
- 使任何人在弱光环境下更容易使用设备。
您的应用可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您自己实现深色主题,用户体验会更好,这样您可以完全控制应用主题。
选择自己的深色主题时,需要注意深色主题的颜色需要满足辅助功能对比度标准。深色主题使用深色表面颜色,并辅以少量颜色点缀。
在预览中查看深色主题
您已经在上一步中添加了深色主题的颜色。要查看深色主题的效果,您需要在 MainActivity.kt 中添加另一个 Preview Composable。这样,当您更改代码中的 UI 布局时,可以同时查看明亮主题和深色主题的预览效果。
- 在
WoofPreview()
下面,创建一个名为WoofDarkThemePreview()
的新函数,并使用@Preview
和@Composable
进行注解。
@Preview
@Composable
fun WoofDarkThemePreview() {
}
- 在
DarkThemePreview()
内部,添加WoofTheme()
。如果不添加WoofTheme()
,您将看不到我们在应用中添加的任何样式。将darkTheme
参数设置为 true。
@Preview
@Composable
fun WoofDarkThemePreview() {
WoofTheme(darkTheme = true) {
}
}
- 在
WoofTheme()
内部调用WoofApp()
。
@Preview
@Composable
fun WoofDarkThemePreview() {
WoofTheme(darkTheme = true) {
WoofApp()
}
}
现在,在 Design 面板中向下滚动,查看应用在深色主题下的样子,包括较深的应用/列表项背景和较亮的文本。比较深色和明亮主题之间的差异。
深色主题 | 明亮主题 |
在设备或模拟器上查看深色主题
为了在模拟器或实体设备上以深色主题查看您的应用
- 转到设备上的 Settings(设置)应用。
- 搜索 Dark theme(深色主题)并点击进入。
- 开启 Dark theme(深色主题)。
- 重新打开 Woof 应用,它将处于深色主题。
此 Codelab 更侧重于明亮主题,因此在您继续开发应用之前,请关闭深色主题。
- 转到设备上的 Settings(设置)应用。
- 选择 Display(显示)。
- 关闭 Dark theme(深色主题)。
比较应用在本节开始时与现在的样子。列表项和文本更加清晰,配色方案也更具视觉吸引力。
无颜色 | 有颜色(明亮主题) | 有颜色(深色主题) |
动态颜色
Material 3 强烈关注用户个性化——Material 3 中的一个新特性是动态颜色,它根据用户的壁纸为您的应用创建主题。这样,如果用户喜欢绿色,并且手机背景是蓝色,那么他们的 Woof 应用也将是蓝色的,以反映这一点。动态主题化仅在运行 Android 12 及更高版本的特定设备上可用。
自定义主题可用于具有强烈品牌颜色的应用,并且还需要为不支持动态主题化的设备实现,以便您的应用仍然具有主题。
- 要启用动态颜色,请打开 Theme.kt 文件,转到
WoofTheme()
可组合函数,并将dynamicColor
参数设置为 true。
@Composable
fun WoofTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
)
- 要更改设备或模拟器的背景,请转到 Settings(设置),然后搜索 Wallpaper(壁纸)。
- 将壁纸更改为某种颜色或一组颜色。
- 重新运行您的应用以查看动态主题(请注意,您的设备或模拟器必须是 Android 12+ 才能看到动态颜色),随意尝试不同的壁纸!
- 此 Codelab 确实侧重于自定义主题,因此在继续之前请禁用
dynamicColor
。
@Composable
fun WoofTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false,
content: @Composable () -> Unit
)
5. 添加形状
应用形状可以极大地改变可组合函数的外观和感受。形状引导注意力、识别组件、传达状态和表达品牌。
许多形状使用 RoundedCornerShape
定义,它描述了具有圆角的矩形。传入的数字定义了圆角的程度。如果使用 RoundedCornerShape(0.dp)
,矩形没有圆角;如果使用 RoundedCornerShape(50.dp)
,角将是完全圆形的。
0.dp | 25.dp | 50.dp |
您还可以通过在每个角上添加不同的圆角百分比来进一步自定义形状。玩形状真的很有趣!
左上:50.dp | 左上:15.dp | 左上:0.dp |
Shape.kt 文件用于在 Compose 中定义组件的形状。组件有三种类型:small(小)、medium(中)和 large(大)。在本节中,您将修改定义为 medium
大小的 Card
组件。组件根据其大小被分组到形状类别中。
在本节中,您将把狗的图片塑造成圆形,并修改列表项的形状。
将狗的图片塑造成圆形
- 打开 Shape.kt 文件,注意 small 参数设置为
RoundedCornerShape(50.dp)
。这将用于将图片塑造成圆形。
val Shapes = Shapes(
small = RoundedCornerShape(50.dp),
)
- 打开 MainActivity.kt。在
DogIcon()
中,为Image
的modifier
添加一个clip
属性;这将把图片裁剪成一个形状。传入MaterialTheme.shapes.small
。
import androidx.compose.ui.draw.clip
@Composable
fun DogIcon(
@DrawableRes dogIcon: Int,
modifier: Modifier = Modifier
) {
Image(
modifier = modifier
.size(dimensionResource(id = R.dimen.image_size))
.padding(dimensionResource(id = R.dimen.padding_small))
.clip(MaterialTheme.shapes.small),
当您查看 WoofPreview()
时,您会注意到狗的图标是圆形的!但是,有些照片在侧边被剪裁了,没有完全显示为圆形。
- 为了使所有照片都呈圆形,添加一个
ContentScale
和一个Crop
属性;这会裁剪图片以适应。请注意,contentScale
是Image
的一个属性,而不是modifier
的一部分。
import androidx.compose.ui.layout.ContentScale
@Composable
fun DogIcon(
@DrawableRes dogIcon: Int,
modifier: Modifier = Modifier
) {
Image(
modifier = modifier
.size(dimensionResource(id = R.dimen.image_size))
.padding(dimensionResource(id = R.dimen.padding_small))
.clip(MaterialTheme.shapes.small),
contentScale = ContentScale.Crop,
这是完整的 DogIcon()
可组合函数。
@Composable
fun DogIcon(
@DrawableRes dogIcon: Int,
modifier: Modifier = Modifier
) {
Image(
modifier = modifier
.size(dimensionResource(R.dimen.image_size))
.padding(dimensionResource(R.dimen.padding_small))
.clip(MaterialTheme.shapes.small),
contentScale = ContentScale.Crop,
painter = painterResource(dogIcon),
// Content Description is not needed here - image is decorative, and setting a null content
// description allows accessibility services to skip this element during navigation.
contentDescription = null
)
}
现在在 WoofPreview()
中,图标是圆形的。
为列表项添加形状
在本节中,您将为列表项添加形状。列表项已经通过 Card
显示。Card
是一个可以容纳单个可组合函数的表面,并包含装饰选项。可以通过边框、形状等添加装饰。在本节中,您将使用 Card
为列表项添加形状。
- 打开 Shape.kt 文件。
Card
是一个 medium 组件,因此您需要添加Shapes
对象的 medium 参数。对于此应用,列表项的右上角和左下角需要有圆角,但不能完全圆形。要实现这一点,请将16.dp
传入medium
属性。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)
由于 Card
默认已经使用 medium 形状,您无需显式将其设置为 medium 形状。查看 Preview,看看新形状的 Card
!
如果您回到 Theme.kt 文件中的 WoofTheme()
,查看 MaterialTheme()
,您会看到 shapes
属性设置为您刚刚更新的 Shapes
val
。
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
下面是添加形状前后列表项的并排视图。注意添加形状后应用在视觉上更具吸引力。
无形状 | 有形状 |
6. 添加排版
Material Design 类型比例
类型比例是一组可以在整个应用中使用的字体样式选择,确保灵活且一致的样式。Material Design 类型比例包括类型系统支持的十五种字体样式。命名和分组已简化为:display(展示)、headline(标题)、title(正标题)、body(正文)和 label(标签),每种都有 large(大)、medium(中)和 small(小)尺寸。只有当您想自定义应用时才需要使用这些选项。如果您不知道为每个类型比例类别设置什么,请记住有一个可以使用的默认排版比例。
类型比例包含可重用的文本类别,每个类别都有预期的应用和含义。
Display(展示)
作为屏幕上最大的文本,展示样式保留用于简短、重要的文本或数字。它们最适用于大屏幕。
Headline(标题)
标题最适合在较小的屏幕上显示简短、高强调的文本。这些样式可以很好地用于标记主要文本段落或重要内容区域。
Title(正标题)
正标题小于标题样式,应用于相对简短的中等强调文本。
Body(正文)
正文样式用于应用中的较长文本段落。
Label(标签)
标签样式是较小、实用的样式,用于组件内的文本或内容主体中非常小的文本,如说明文字。
字体
Android 平台提供了各种字体,但您可能希望使用默认未提供的字体自定义您的应用。自定义字体可以增加个性,并用于品牌推广。
在本节中,您将添加名为 Abril Fatface、Montserrat Bold 和 Montserrat Regular 的自定义字体。您将使用 Material Type 系统中的 displayLarge 和 displayMedium 标题以及 bodyLarge 文本,并将其添加到应用中的文本中。
创建一个字体 Android 资源目录。
在向应用添加字体之前,您需要添加一个字体目录。
- 在 Android Studio 的项目视图中,右键点击 res 文件夹。
- 选择 New(新建)> Android Resource Directory(Android 资源目录)。
- 将目录命名为 font,将资源类型设置为 font,然后点击 OK(确定)。
- 打开位于 res > font 的新字体资源目录。
下载自定义字体
由于您使用的是 Android 平台默认未提供的字体,您需要下载自定义字体。
- 转到 https://fonts.google.com/。
- 搜索 Montserrat 并点击 Download family(下载字体包)。
- 解压 zip 文件。
- 打开下载的 Montserrat 文件夹。在 static 文件夹中,找到 Montserrat-Bold.ttf 和 Montserrat-Regular.ttf(ttf 是 TrueType Font 的缩写,是字体文件的格式)。选择这两个字体并将它们拖到 Android Studio 项目的字体资源目录中。
- 在您的字体文件夹中,将 Montserrat-Bold.ttf 重命名为 montserrat_bold.ttf,将 Montserrat-Regular.ttf 重命名为 montserrat_regular.ttf。
- 搜索 Abril Fatface 并点击 Download family(下载字体包)。
- 打开下载的 Abril_Fatface 文件夹。选择 AbrilFatface-Regular.ttf 并将其拖到字体资源目录中。
- 在您的字体文件夹中,将 Abril_Fatface_Regular.ttf 重命名为 abril_fatface_regular.ttf。
这就是您项目中包含三个自定义字体文件的字体资源目录的样子
初始化字体
- 在项目窗口中,打开 ui.theme > Type.kt。在 import 语句下方和
Typography
val
上方初始化下载的字体。首先,通过将其设置为等于FontFamily
并传入包含字体文件abril_fatface_regular
的Font
来初始化 Abril Fatface。
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import com.example.woof.R
val AbrilFatface = FontFamily(
Font(R.font.abril_fatface_regular)
)
- 在 Abril Fatface 下面初始化 Montserrat,通过将其设置为等于
FontFamily
并传入包含字体文件montserrat_regular
的Font
。对于montserrat_bold
,也包含FontWeight.Bold
。即使您确实传入了字体文件的粗体版本,Compose 也不知道该文件是粗体,因此您需要显式将文件链接到FontWeight.Bold
。
import androidx.compose.ui.text.font.FontWeight
val AbrilFatface = FontFamily(
Font(R.font.abril_fatface_regular)
)
val Montserrat = FontFamily(
Font(R.font.montserrat_regular),
Font(R.font.montserrat_bold, FontWeight.Bold)
)
接下来,将不同类型的标题设置为您刚刚添加的字体。Typography
对象包含上面讨论的 13 种不同字体的参数。您可以根据需要定义任意数量。在此应用中,我们将设置 displayLarge
、displayMedium
和 bodyLarge
。在此应用的下一部分中,您将使用 labelSmall
,因此您将其添加到此处。
下表显示了您要添加的每个标题的字体、字重和大小。
- 对于
displayLarge
属性,将其设置为等于TextStyle
,并使用上表中的信息填充fontFamily
、fontWeight
和fontSize
。这意味着所有设置为displayLarge
的文本将使用 Abril Fatface 字体,字重 normal,fontSize
为36.sp
。
对 displayMedium
、labelSmall
和 bodyLarge
重复此过程。
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.sp
val Typography = Typography(
displayLarge = TextStyle(
fontFamily = AbrilFatface,
fontWeight = FontWeight.Normal,
fontSize = 36.sp
),
displayMedium = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Bold,
fontSize = 20.sp
),
labelSmall = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Bold,
fontSize = 14.sp
),
bodyLarge = TextStyle(
fontFamily = Montserrat,
fontWeight = FontWeight.Normal,
fontSize = 14.sp
)
)
如果您转到 Theme.kt 文件中的 WoofTheme()
并查看 MaterialTheme()
,则 typography
参数等于您刚刚更新的 Typography val
。
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
为应用文本添加排版
现在,您将为应用中的每个文本实例添加标题类型。
- 将
displayMedium
添加为dogName
的样式,因为它是一段简短、重要的信息。将bodyLarge
添加为dogAge
的样式,因为它适用于较小的文本大小。
@Composable
fun DogInformation(
@StringRes dogName: Int,
dogAge: Int,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
Text(
text = stringResource(dogName),
style = MaterialTheme.typography.displayMedium,
modifier = Modifier.padding(top = dimensionResource(id = R.dimen.padding_small))
)
Text(
text = stringResource(R.string.years_old, dogAge),
style = MaterialTheme.typography.bodyLarge
)
}
}
- 现在在
WoofPreview()
中,狗的名字以20.sp
大小显示粗体 Montserrat 字体,狗的年龄以14.sp
大小显示正常 Montserrat 字体。
下面是添加排版前后列表项的并排视图。请注意狗的名字和狗的年龄之间的字体差异。
无排版 | 有排版 |
7. 添加顶部栏
一个 Scaffold
是一种布局,为各种组件和屏幕元素(例如 Image
、Row
或 Column
)提供了槽位。Scaffold
还为 TopAppBar
提供了一个槽位,您将在本节中使用它。
TopAppBar
可以用于多种目的,但在本例中,您将使用它来展示品牌并赋予应用个性。TopAppBar
有四种不同类型:center(居中)、small(小)、medium(中)和 large(大)。在此 Codelab 中,您将实现一个居中顶部应用栏。您将创建一个看起来像下面截图的可组合函数,并将其放入 Scaffold
的 topBar
部分。
对于此应用,我们的顶部栏由一个 Row
组成,其中包含一个徽标图片和应用标题文本。徽标采用可爱的渐变爪印和应用标题!
向顶部栏添加图片和文本
- 在 MainActivity.kt 中,创建一个名为
WoofTopAppBar()
的可组合函数,带有一个可选的modifier
。
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
}
Scaffold
支持contentWindowInsets
参数,该参数可以帮助指定 scaffold 内容的内边距。WindowInsets
是屏幕上您的应用可能与系统 UI 相交的部分,这些内边距通过PaddingValues
参数传递给内容槽位。在此处阅读更多相关信息。
contentWindowInsets
值作为 contentPadding
传递给 LazyColumn
。
@Composable
fun WoofApp() {
Scaffold { it ->
LazyColumn(contentPadding = it) {
items(dogs) {
DogItem(
dog = it,
modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)
}
}
}
}
- 在
Scaffold
中,添加一个topBar
属性并将其设置为WoofTopAppBar()
。
Scaffold(
topBar = {
WoofTopAppBar()
}
)
下面是 WoofApp()
可组合函数的样子
@Composable
fun WoofApp() {
Scaffold(
topBar = {
WoofTopAppBar()
}
) { it ->
LazyColumn(contentPadding = it) {
items(dogs) {
DogItem(
dog = it,
modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
)
}
}
}
}
WoofPreview()
中没有任何变化,因为 WoofTopAppBar()
中没有任何内容。让我们来改变它!
- 在
WoofTopAppBar() Composable
中,添加一个CenterAlignedTopAppBar()
并将 modifier 参数设置为传递到WoofTopAppBar()
中的 modifier。
import androidx.compose.material3.CenterAlignedTopAppBar
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
CenterAlignedTopAppBar(
modifier = modifier
)
}
- 对于 title 参数,传入一个
Row
,它将包含CenterAlignedTopAppBar
的Image
和Text
。
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier){
CenterAlignedTopAppBar(
title = {
Row() {
}
},
modifier = modifier
)
}
- 将徽标
Image
添加到Row
中。
- 将
modifier
中的图片尺寸设置为dimens.xml
文件中的image_size
,将内边距设置为dimens.xml
文件中的padding_small
。 - 使用
painter
将Image
设置为 drawable 文件夹中的ic_woof_logo
。 - 将
contentDescription
设置为 null。在这种情况下,应用徽标不会为视力受损的用户添加任何语义信息,因此我们无需添加内容描述。
Row() {
Image(
modifier = Modifier
.size(dimensionResource(id = R.dimen.image_size))
.padding(dimensionResource(id = R.dimen.padding_small)),
painter = painterResource(R.drawable.ic_woof_logo),
contentDescription = null
)
}
- 接下来,在
Image
之后,在Row
内部添加一个Text
Composable。
- 使用
stringResource()
将其设置为app_name
的值。这将文本设置为应用的名称,该名称存储在strings.xml
中。 - 将文本样式设置为
displayLarge
,因为应用名称是简短且重要的文本。
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.displayLarge
)
这就是 WoofPreview()
中显示的内容,看起来有点不对劲,因为图标和文本没有垂直对齐。
- 为了解决这个问题,向
Row
添加一个verticalAlignment
值参数,并将其设置为等于Alignment.CenterVertically
。
import androidx.compose.ui.Alignment
Row(
verticalAlignment = Alignment.CenterVertically
)
这样看起来好多了!
这是完整的 WoofTopAppBar()
可组合函数
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
CenterAlignedTopAppBar(
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.size(dimensionResource(id = R.dimen.image_size))
.padding(dimensionResource(id = R.dimen.padding_small)),
painter = painterResource(R.drawable.ic_woof_logo),
contentDescription = null
)
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.displayLarge
)
}
},
modifier = modifier
)
}
运行应用,欣赏 TopAppBar
如何精美地将应用串联起来。
无顶部应用栏 | 有顶部应用栏 |
现在,看看最终应用在深色主题下的样子!
恭喜您,您已完成本 Codelab!
8. 获取解决方案代码
要下载已完成的 Codelab 代码,您可以使用以下 git 命令
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git $ cd basic-android-kotlin-compose-training-woof $ git checkout material
或者,您可以将仓库下载为 zip 文件,解压并在 Android Studio 中打开。
如果您想查看解决方案代码,请在 GitHub 上查看。
9. 总结
您刚刚创建了您的第一个 Material 应用!您为明亮主题和深色主题创建了自定义调色板,为不同组件创建了形状,下载了字体并将其添加到应用中,并创建了一个漂亮的顶部栏将所有内容连接起来。利用您在此 Codelab 中学到的技能,更改颜色、形状和排版,创建完全属于您自己的应用!
总结
- Material Theming 允许您在应用中使用 Material Design,并提供有关自定义颜色、排版和形状的指导。
- Theme.kt 文件是通过名为
[您的应用名称]+Theme()
的可组合函数定义主题的位置——在此应用中为WoofTheme()
。在此函数中,MaterialTheme
对象设置应用的color
、typography
、shapes
和content
。 - Color.kt 是您列出应用中使用的颜色的位置。然后在 Theme.kt 中,将
LightColorPalette
和DarkColorPalette
中的颜色分配到特定的槽位。并非所有槽位都需要分配。 - 您的应用可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您自己实现深色主题,用户体验会更好,这样您可以完全控制应用主题。
- Shape.kt 是您定义应用形状的位置。有三种形状大小(small、medium、large),您可以指定圆角的程度。
- 形状引导注意力、识别组件、传达状态和表达品牌。
- Type.kt 是您初始化字体并为 Material Design 类型比例分配
fontFamily
、fontWeight
和fontSize
的位置。 - 测量系统Material Design 类型比例包含一系列对比鲜明的样式,支持您的应用及其内容的需求。类型比例是类型系统支持的 15 种样式的组合。