1. 开始之前
Material Design 是 Google 设计师和开发者构建和支持的设计系统,用于为 Android 以及其他移动和 Web 平台构建高质量的数字体验。它提供有关如何以可读、吸引人和一致的方式构建您的应用程序 UI 的指南。
在这个代码实验室中,您将学习 Material 主题,它允许您在您的应用程序中使用 Material Design,并指导您自定义颜色、排版和形状。您可以根据需要自定义应用程序的任何部分。
先决条件
- 熟悉 Kotlin 语言,包括语法、函数和变量。
- 能够使用 Compose 构建布局,包括带有填充的行和列。
- 能够在 Compose 中创建简单的列表。
您将学习什么
- 如何将 Material 主题应用于 Compose 应用程序。
- 如何向您的应用程序添加自定义调色板。
- 如何向您的应用程序添加自定义字体。
- 如何向应用程序中的元素添加自定义形状。
- 如何向您的应用程序添加顶部应用栏。
您将构建什么
- 您将构建一个美丽的应用程序,它融合了 Material Design 的最佳实践。
您需要什么
- 最新版本的 Android Studio。
- 下载启动代码和字体的互联网连接。
2. 应用概述
在这个代码实验室中,您将创建 **Woof**,一个显示狗列表并使用 Material Design 创建精美应用程序体验的应用程序。
通过这个代码实验室,我们将向您展示使用 Material 主题可以实现的一些功能。使用这个代码实验室来了解如何使用 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 应用程序
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()
允许您在 **设计** 面板中查看应用程序的预览。
确保您的模拟器/设备处于亮主题
在这个代码实验室中,您将使用亮主题和暗主题,但是,代码实验室的大部分内容都在亮主题中。在开始之前,请确保您的设备/模拟器处于亮主题。
为了在亮主题下查看您的应用程序,请在您的模拟器或物理设备上
- 转到设备上的 **设置** 应用程序。
- 搜索 **暗主题** 并点击它。
- 如果 **暗主题** 已启用,请将其关闭。
运行启动代码以查看您开始的内容;它是一个显示狗的照片、姓名和年龄的列表。它功能齐全,但外观不太好,所以我们将修复它。
4. 添加颜色
您将在 **Woof** 应用程序中修改的第一件事是配色方案。
配色方案是应用程序使用的颜色的组合。不同的颜色组合会唤起不同的情绪,这会影响人们使用应用程序时的感受。
在 Android 系统中,颜色由十六进制 (hex) 颜色值表示。十六进制颜色代码以井号 (#) 字符开头,后跟六个字母和/或数字,这些字母和/或数字表示该颜色的红色、绿色和蓝色 (RGB) 分量。前两个字母/数字表示红色,接下来的两个表示绿色,最后两个表示蓝色。
颜色还可以包含 alpha 值(字母和/或数字),表示颜色的透明度(#00 为 0% 不透明度(完全透明),#FF 为 100% 不透明度(完全不透明))。包含时,alpha 值是井号 (#) 字符后十六进制颜色代码的前两个字符。如果未包含 alpha 值,则假定为 #FF,即 100% 不透明度(完全不透明)。
以下是一些示例颜色及其十六进制值。
使用 Material 主题构建器创建配色方案
要为我们的应用程序创建自定义配色方案,我们将使用 Material 主题构建器。
- 单击此链接以转到 Material 主题构建器。
- 在左侧窗格中,您将看到核心颜色,单击主要颜色
- 将打开 HCT 颜色选择器。
- 要创建应用程序屏幕截图中显示的配色方案,您将更改此颜色选择器中的主要颜色。在文本框中,将当前文本替换为 **#006C4C**。这将使应用程序的主要颜色变为绿色。
请注意,这如何更新屏幕上的应用程序以采用绿色配色方案。
- 向下滚动页面,您将看到根据您输入的颜色生成的亮主题和暗主题的完整配色方案。
您可能想知道所有这些角色是什么以及它们是如何使用的,这里有一些主要的角色
- **主要** 颜色用于 UI 中的关键组件。
- **次要** 颜色用于 UI 中不太突出的组件。
- **三级** 颜色用于对比色调,可用于平衡主要颜色和次要颜色,或提请注意某个元素(例如输入字段)。
- **on** 颜色元素出现在调色板中其他颜色的 **顶部**,主要应用于文本、图标和笔划。在我们的调色板中,我们有一个 **onSurface** 颜色,它出现在 **surface** 颜色的顶部,以及一个 **onPrimary** 颜色,它出现在 **primary** 颜色的顶部。
拥有这些槽位可以带来具有凝聚力的设计系统,其中相关组件的颜色相似。
关于颜色的理论就足够了——是时候将这个美丽的调色板添加到应用程序中了!
将调色板添加到主题
在 Material 主题构建器页面上,可以选择单击 **导出** 按钮以下载包含您在主题构建器中创建的自定义主题的 **Color.kt** 文件和 **Theme.kt** 文件。
这将有助于将我们创建的自定义主题添加到您的应用程序中。但是,由于生成的 **Theme.kt** 文件不包含我们将在代码实验室后面介绍的动态颜色的代码,因此请复制这些文件。
- 打开 **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中的其他关键组件(如浮动操作按钮)也默认使用主颜色。这意味着你无需显式地为组件分配颜色;当你设置应用的颜色主题时,它会自动映射到颜色槽。你可以通过在代码中显式设置颜色来覆盖此设置。点击这里了解更多关于颜色角色的信息。
在本节中,我们将用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()
中的修饰符传递给Card
,并将Row
的修饰符更新为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
组合项,列表项的颜色已自动更改。颜色看起来很棒,但是列表项之间没有间距。
Dimens文件
就像你使用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添加另一个预览组合项。这样,当你更改代码中的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()
}
}
现在向下滚动到设计面板,查看深色主题下的应用,包括更暗的应用/列表项背景和更浅的文本。比较深色主题和浅色主题之间的差异。
暗主题 | 亮主题 |
在你的设备或模拟器上查看深色主题
为了在模拟器或物理设备上查看你的应用的深色主题
- 转到设备上的 **设置** 应用程序。
- 搜索 **暗主题** 并点击它。
- 打开深色主题。
- 重新打开Woof应用,它将处于深色主题。
本Codelab更侧重于浅色主题,因此在继续进行应用之前,请关闭深色主题。
- 转到设备上的 **设置** 应用程序。
- 选择显示。
- 关闭深色主题。
比较本节开始时应用的外观与现在的外观。列表项和文本更清晰,颜色方案更具视觉吸引力。
无颜色 | 带颜色(浅色主题) | 带颜色(深色主题) |
动态颜色
Material 3 非常注重用户个性化——Material 3 中的一项新功能是动态颜色,它会根据用户的壁纸为你的应用创建主题。这样,如果用户喜欢绿色并且有蓝色手机背景,他们的 Woof 应用也会是蓝色以反映这一点。动态主题仅在运行 Android 12 及更高版本的某些设备上可用。
自定义主题可用于具有强烈品牌色彩的应用,也需要为不支持动态主题的设备实现,以便你的应用仍然具有主题。
- 要启用动态颜色,请打开Theme.kt并转到
WoofTheme()
组合项,并将dynamicColor
参数设置为**true**。
@Composable
fun WoofTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
)
- 要更改设备或模拟器的背景,请转到设置,然后搜索壁纸。
- 将壁纸更改为一种或多种颜色。
- 重新运行你的应用以查看动态主题(请注意,你的设备或模拟器必须是 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中组件的形状。共有三种类型的组件:小、中和大。在本节中,你将修改Card
组件,它定义为medium
大小。组件根据其大小分为形状类别。
在本节中,你将把狗的图像塑造为圆形,并修改列表项的形状。
将狗的图像塑造成圆形
- 打开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
是一个中等组件,因此您添加Shapes
对象的medium参数。对于此应用程序,列表项的右上角和左下角为圆角,但不会使其完全圆形。为此,请将16.dp
传递给medium
属性。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)
由于Card
默认情况下已经使用了中等形状,因此您不必显式地将其设置为中等形状。查看**预览**以查看新形状的Card
!
如果您返回到WoofTheme()
中的**Theme.kt**文件,并查看MaterialTheme()
,您会看到shapes
属性已设置为您刚刚更新的Shapes
val
。
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
以下是列表项在添加形状前后并排比较的结果。注意添加形状后应用程序的视觉效果提升了多少。
未添加形状 | 已添加形状 |
6. 添加排版
Material Design 字体比例
字体比例是在应用程序中使用的字体样式的选择,确保样式灵活且一致。 Material Design 字体比例包含十五种字体样式,这些样式受类型系统支持。命名和分组已简化为:显示、标题、正文和标签,每种都有大、中、小三种尺寸。只有在您想要自定义应用程序时才需要使用这些选项。如果您不知道为每个字体比例类别设置什么值,请知道可以使用默认的字体比例。
字体比例包含可重复使用的文本类别,每个类别都有其预期的应用程序和含义。
显示
作为屏幕上最大的文本,显示样式仅保留用于简短、重要的文本或数字。它们在大型屏幕上效果最佳。
标题
标题最适合用于较小屏幕上的简短、高强调文本。这些样式可以很好地标记文本的主要段落或重要的内容区域。
标题
标题小于标题样式,应用于中等强调的文本,这些文本应保持相对较短。
正文
正文样式用于应用程序中的较长文本段落。
标签
标签样式较小,实用性强,用于组件内部的文本或内容正文中的极小文本,例如字幕。
字体
Android 平台提供各种字体,但您可能希望使用默认情况下未提供的字体来自定义您的应用程序。自定义字体可以增加个性并用于品牌推广。
在本节中,您将添加名为**Abril Fatface**、**Montserrat Bold**和**Montserrat Regular**的自定义字体。您将使用 Material Type 系统中的 displayLarge 和 displayMedium 标题以及 bodyLarge 文本,并将它们添加到应用程序中的文本中。
创建字体 Android 资源目录。
在将字体添加到您的应用程序之前,您需要添加一个字体目录。
- 在 Android Studio 的项目视图中,右键单击**res**文件夹。
- 选择**新建**>**Android 资源目录**。
- 将目录命名为**font**,将资源类型设置为**font**,然后单击**确定**。
- 打开位于**res > font**的新字体资源目录。
下载自定义字体
由于您使用的是 Android 平台未提供的字体,因此您需要下载自定义字体。
- 访问https://fonts.google.com/。
- 搜索Montserrat 并单击**下载字体系列**。
- 解压缩 zip 文件。
- 打开下载的**Montserrat**文件夹。在**static**文件夹中,找到**Montserrat-Bold.ttf**和**Montserrat-Regular.ttf**(**ttf**代表 TrueType 字体,是字体文件的格式)。选择这两种字体,并将它们拖到 Android Studio 中项目的字体资源目录中。
- 在您的字体文件夹中,将**Montserrat-Bold.ttf**重命名为**montserrat_bold.ttf**,并将**Montserrat-Regular.ttf**重命名为**montserrat_regular.ttf**。
- 搜索Abril Fatface 并单击**下载字体系列**。
- 打开下载的**Abril_Fatface**文件夹。选择**AbrilFatface-Regular.ttf**并将其拖到字体资源目录中。
- 在您的字体文件夹中,将**Abril_Fatface_Regular.ttf**重命名为**abril_fatface_regular.ttf**。
这就是您的项目中包含三个自定义字体文件的字体资源目录的样子
初始化字体
- 在项目窗口中,打开**ui.theme** > **Type.kt**。在导入语句下方和
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**下方,通过将其设置为
FontFamily
并传入带有字体文件montserrat_regular
的Font
来初始化**Montserrat**。对于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**作为字体,字体粗细为普通,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
)
)
如果您转到WoofTheme()
中的**Theme.kt**文件并查看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
`有四种不同的类型:居中、小、中和大。在本Codelab中,您将实现一个居中的顶部应用栏。您将创建一个看起来像下面屏幕截图的可组合项,并将其插入`Scaffold
`的`topBar
`部分。
对于此应用,我们的顶部栏由一个带有徽标图像和应用标题文本的`Row
`组成。徽标显示一个可爱的渐变爪印和应用的标题!
向顶部栏添加图像和文本
- 在**MainActivity.kt**中,创建一个名为`
WoofTopAppBar()
`的可组合项,并带有一个可选的`modifier
`。
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
}
Scaffold
支持`contentWindowInsets
`参数,该参数可以帮助指定脚手架内容的内边距。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()
`可组合项中,添加一个`CenterAlignedTopAppBar()
`并将`modifier
`参数设置为传递给`WoofTopAppBar()
`的修饰符。
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
`可组合项。
- 使用`
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主题允许您在应用中使用Material Design,并指导您自定义颜色、排版和形状。
- **Theme.kt**文件是在一个名为`
[your app name]+Theme()
`(在本应用中为`WoofTheme()
`)的可组合项中定义主题的地方。在此函数中,`MaterialTheme
`对象设置应用的`color
`、`typography
`、`shapes
`和`content
`。 - 在**Color.kt**中,您列出在应用中使用的颜色。然后在**Theme.kt**中,您将`
LightColorPalette
`和`DarkColorPalette
`中的颜色分配给特定槽。并非所有槽都需要分配。 - 您的应用可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您实现深色主题,则可以更好地改善用户体验,因为您可以完全控制应用主题。
- **Shape.kt**是您定义应用形状的地方。有三种形状大小(小、中、大),您可以指定角的圆角方式。
- 形状引导注意力、识别组件、传达状态并表达品牌。
- **Type.kt**是您初始化字体并为Material Design类型比例分配`
fontFamily
`、`fontWeight
`和`fontSize
`的地方。 - Material Design类型比例包括一系列对比鲜明的样式,以支持您的应用及其内容的需求。类型比例是由类型系统支持的15种样式的组合。