使用 Jetpack Compose 进行 Material 主题化

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 打造精美应用体验的应用。

92eca92f64b029cf.png

通过此 Codelab,我们将向您展示使用 Material Theming 的一些可能性。使用此 Codelab 获取关于如何使用 Material Theming 改进未来创建的应用外观和感受的灵感。

调色板

下面是我们将创建的明亮和深色主题的调色板。

This image has the light color scheme for the Woof app.

This image has the dark color scheme for the Woof app.

这是最终应用在明亮主题和深色主题下的样子。

明亮主题

深色主题

排版

下面是您将在应用中使用的类型样式。

8ea685b3871d5ffc.png

主题文件

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 仓库中浏览代码。

探索起始代码

  1. 在 Android Studio 中打开起始代码。
  2. 打开 com.example.woof > data > Dog.kt。此文件包含 Dog data class,用于表示狗的照片、名称、年龄和爱好。它还包含一个狗列表以及您将在应用中用作数据的信息。
  3. 打开 res > drawable。此文件夹包含本项目所需的所有图片资源,包括应用图标、狗图片和图标。
  4. 打开 res > values > strings.xml。此文件包含您在此应用中使用的字符串,包括应用名称、狗的名字、它们的描述等。
  5. 打开 MainActivity.kt。此文件包含创建简单列表的代码,该列表显示狗的照片、狗的名字和狗的年龄。
  6. WoofApp() 包含一个显示 DogItemLazyColumn
  7. DogItem() 包含一个显示狗的照片及其信息的 Row
  8. DogIcon() 显示狗的照片。
  9. DogInformation() 显示狗的名字和年龄。
  10. WoofPreview() 允许您在 Design 面板中查看应用预览。

确保您的模拟器/设备处于明亮主题

在此 Codelab 中,您将使用明亮和深色主题,但 Codelab 的大部分内容都使用明亮主题。开始之前,请确保您的设备/模拟器处于明亮主题。

为了在模拟器或实体设备上以明亮主题查看您的应用

  1. 转到设备上的 Settings(设置)应用。
  2. 搜索 Dark theme(深色主题)并点击进入。
  3. 如果 Dark theme(深色主题)已开启,请将其关闭。

运行起始代码,看看您开始时的样子;它是一个显示狗及其照片、名字和年龄的列表。它功能正常,但看起来不太好,所以我们将修复它。

6d253ae50c63014d.png

4. 添加颜色

您将在 Woof 应用中修改的第一件事是配色方案。

配色方案是您的应用使用的颜色组合。不同的颜色组合会唤起不同的情绪,从而影响用户使用应用时的感受。

在 Android 系统中,颜色由十六进制 (hex) 颜色值表示。十六进制颜色代码以井号 (#) 开头,后跟六个字母和/或数字,代表该颜色的红、绿、蓝 (RGB) 分量。前两个字母/数字代表红色,接下来的两个代表绿色,最后两个代表蓝色。

This shows the hexadecimal numbers that is used to create colors.

颜色还可以包含一个 alpha 值(字母和/或数字),它代表颜色的透明度(#00 表示 0% 不透明度(完全透明),#FF 表示 100% 不透明度(完全不透明))。包含时,alpha 值是井号 (#) 字符后的十六进制颜色代码的前两个字符。如果不包含 alpha 值,则假定为 #FF,即 100% 不透明度(完全不透明)。

下面是一些示例颜色及其十六进制值。

2753d8cdd396c449.png

使用 Material Theme Builder 创建配色方案

要为我们的应用创建自定义配色方案,我们将使用 Material Theme Builder。

  1. 点击此链接进入 Material Theme Builder
  2. 在左侧窗格中,您将看到 Core Colors(核心颜色),点击 Primary(主要)

This shows four core colors in the Material Theme Builder

  1. HCT 拾色器将打开。

This is the HCT Color Picker to choose a custom color in the Material Theme Builder.

  1. 要创建应用截图所示的配色方案,您需要在此拾色器中更改主要颜色。在文本框中,将当前文本替换为 #006C4C。这将使应用的主要颜色变为绿色。

This shows the HCT Color picker set to green

请注意,这将更新屏幕上的应用以采用绿色配色方案。

This shows the Material Theme Builder's apps reacting to the change in color from the HCT color picker.

  1. 向下滚动页面,您将看到根据您输入的颜色生成的明亮和深色主题的完整配色方案。

Material Theme Builder Light Scheme

Dark Scheme generated by Material Theme Builder

您可能想知道所有这些角色是什么以及它们是如何使用的,以下是一些主要角色:

  • Primary(主要)颜色用于整个界面的关键组件。
  • Secondary(次要)颜色用于界面中不太突出的组件。
  • Tertiary(第三色)颜色用于对比鲜明的点缀,可以用来平衡主要和次要颜色,或提高对某个元素的关注,例如输入字段。
  • On 颜色元素出现在调色板中其他颜色的顶部,主要应用于文本、图标和描边。在我们的调色板中,我们有一个 onSurface 颜色(出现在 surface 颜色的顶部)和一个 onPrimary 颜色(出现在 primary 颜色的顶部)。

拥有这些槽位会形成一个连贯的设计系统,其中相关的组件颜色相似。

关于颜色的理论讲够了——现在是时候将这个美丽的调色板添加到应用中了!

将调色板添加到主题

在 Material Theme Builder 页面上,可以选择点击 Export(导出)按钮,下载包含您在 Theme Builder 中创建的自定义主题的 Color.kt 文件和 Theme.kt 文件。

这会将我们创建的自定义主题添加到您的应用中。但是,由于生成的 Theme.kt 文件不包含我们将在 Codelab 后面介绍的动态颜色的代码,因此请复制代码内容。

  1. 打开 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)
  1. 打开 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
   }
  1. 重新运行您的应用,注意应用栏自动更改了颜色。

b48b3fa2ecec9b86.png

颜色映射

Material 组件会自动映射到颜色槽位。其他关键的 UI 组件,如浮动操作按钮,也默认使用 Primary 颜色。这意味着您无需显式为组件分配颜色;当您在应用中设置颜色主题时,它会自动映射到颜色槽位。您可以通过在代码中显式设置颜色来覆盖此设置。在此处阅读更多关于颜色角色的信息。

在本节中,我们将使用 Card 包裹包含 DogIcon()DogInformation()Row,以区分列表项颜色与背景。

  1. DogItem() 可组合函数中,使用 Card() 包裹 Row()
Card() {
   Row(
       modifier = modifier
           .fillMaxWidth()
           .padding(dimensionResource(id = R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
   }
}
  1. 由于 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)
   }
}
  1. 查看 WoofPreview()。由于 Card 可组合函数,列表项现在自动更改了颜色。颜色看起来不错,但列表项之间没有间距。

6d49372a1ef49bc7.png

尺寸文件

就像您使用 strings.xml 来存储应用中的字符串一样,使用名为 dimens.xml 的文件来存储尺寸值也是一种好的做法。这很有帮助,这样您就不会硬编码值,并且如果需要,可以在一个地方更改它们。

转到 app > res > values > dimens.xml 并查看该文件。它存储了 padding_smallpadding_mediumimage_size 的尺寸值。这些尺寸将在整个应用中使用。

<resources>
   <dimen name="padding_small">8dp</dimen>
   <dimen name="padding_medium">16dp</dimen>
   <dimen name="image_size">64dp</dimen>
</resources>

要添加来自 dimens.xml 文件的值,请使用以下正确格式

Shows how to properly format adding values from the dimension resource

例如,要添加 padding_small,您将传入 dimensionResource(id = R.dimen.padding_small)

  1. WoofApp() 中,在调用 DogItem() 时,添加一个带有 padding_smallmodifier
@Composable
fun WoofApp() {
    Scaffold { it ->
        LazyColumn(contentPadding = it) {
            items(dogs) {
                DogItem(
                    dog = it,
                    modifier = Modifier.padding(dimensionResource(R.dimen.padding_small))
                )
            }
        }
    }
}

现在在 WoofPreview() 中,列表项之间的界限更加清晰。

c54f870f121fe02.png

深色主题

在 Android 系统中,可以选择将设备切换到深色主题。深色主题使用更深、更柔和的颜色,并且

  • 可以显著降低电量消耗(取决于设备的屏幕技术)。
  • 改善视力低下和对强光敏感的用户的使用体验。
  • 使任何人在弱光环境下更容易使用设备。

您的应用可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您自己实现深色主题,用户体验会更好,这样您可以完全控制应用主题。

选择自己的深色主题时,需要注意深色主题的颜色需要满足辅助功能对比度标准。深色主题使用深色表面颜色,并辅以少量颜色点缀。

在预览中查看深色主题

您已经在上一步中添加了深色主题的颜色。要查看深色主题的效果,您需要在 MainActivity.kt 中添加另一个 Preview Composable。这样,当您更改代码中的 UI 布局时,可以同时查看明亮主题和深色主题的预览效果。

  1. WoofPreview() 下面,创建一个名为 WoofDarkThemePreview() 的新函数,并使用 @Preview@Composable 进行注解。
@Preview
@Composable
fun WoofDarkThemePreview() {

}
  1. DarkThemePreview() 内部,添加 WoofTheme()。如果不添加 WoofTheme(),您将看不到我们在应用中添加的任何样式。将 darkTheme 参数设置为 true。
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {

   }
}
  1. WoofTheme() 内部调用 WoofApp()
@Preview
@Composable
fun WoofDarkThemePreview() {
   WoofTheme(darkTheme = true) {
       WoofApp()
   }
}

现在,在 Design 面板中向下滚动,查看应用在深色主题下的样子,包括较深的应用/列表项背景和较亮的文本。比较深色和明亮主题之间的差异。

深色主题

明亮主题

在设备或模拟器上查看深色主题

为了在模拟器或实体设备上以深色主题查看您的应用

  1. 转到设备上的 Settings(设置)应用。
  2. 搜索 Dark theme(深色主题)并点击进入。
  3. 开启 Dark theme(深色主题)。
  4. 重新打开 Woof 应用,它将处于深色主题

bc31a94207265b08.png

此 Codelab 更侧重于明亮主题,因此在您继续开发应用之前,请关闭深色主题。

  1. 转到设备上的 Settings(设置)应用。
  2. 选择 Display(显示)。
  3. 关闭 Dark theme(深色主题)。

比较应用在本节开始时与现在的样子。列表项和文本更加清晰,配色方案也更具视觉吸引力。

无颜色

有颜色(明亮主题)

有颜色(深色主题)

动态颜色

Material 3 强烈关注用户个性化——Material 3 中的一个新特性是动态颜色,它根据用户的壁纸为您的应用创建主题。这样,如果用户喜欢绿色,并且手机背景是蓝色,那么他们的 Woof 应用也将是蓝色的,以反映这一点。动态主题化仅在运行 Android 12 及更高版本的特定设备上可用。

自定义主题可用于具有强烈品牌颜色的应用,并且还需要为不支持动态主题化的设备实现,以便您的应用仍然具有主题。

  1. 要启用动态颜色,请打开 Theme.kt 文件,转到 WoofTheme() 可组合函数,并将 dynamicColor 参数设置为 true
@Composable
fun WoofTheme(
   darkTheme: Boolean = isSystemInDarkTheme(),
   dynamicColor: Boolean = true,
   content: @Composable () -> Unit
)
  1. 要更改设备或模拟器的背景,请转到 Settings(设置),然后搜索 Wallpaper(壁纸)。
  2. 将壁纸更改为某种颜色或一组颜色。
  3. 重新运行您的应用以查看动态主题(请注意,您的设备或模拟器必须是 Android 12+ 才能看到动态颜色),随意尝试不同的壁纸!

710bd13f6b189dc5.png

  1. 此 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

Woof list item with shaping

Woof list item with shaping

Woof list item with shaping

您还可以通过在每个角上添加不同的圆角百分比来进一步自定义形状。玩形状真的很有趣!

左上:50.dp
左下:25.dp
右上:0.dp
右下:15.dp

左上:15.dp
左下:50.dp
右上:50.dp
右下:15.dp

左上:0.dp
左下:50.dp
右上:0.dp
右下:50.dp

Woof list item with shaping

Woof list item with shaping

Woof list item with shaping

Shape.kt 文件用于在 Compose 中定义组件的形状。组件有三种类型:small(小)、medium(中)和 large(大)。在本节中,您将修改定义为 medium 大小的 Card 组件。组件根据其大小被分组到形状类别中。

在本节中,您将把狗的图片塑造成圆形,并修改列表项的形状。

将狗的图片塑造成圆形

  1. 打开 Shape.kt 文件,注意 small 参数设置为 RoundedCornerShape(50.dp)。这将用于将图片塑造成圆形。
val Shapes = Shapes(
   small = RoundedCornerShape(50.dp),
)
  1. 打开 MainActivity.kt。在 DogIcon() 中,为 Imagemodifier 添加一个 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() 时,您会注意到狗的图标是圆形的!但是,有些照片在侧边被剪裁了,没有完全显示为圆形。

1d4d1e5eaaddf71e.png

  1. 为了使所有照片都呈圆形,添加一个 ContentScale 和一个 Crop 属性;这会裁剪图片以适应。请注意,contentScaleImage 的一个属性,而不是 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() 中,图标是圆形的。

fc93106990f5e161.png

为列表项添加形状

在本节中,您将为列表项添加形状。列表项已经通过 Card 显示。Card 是一个可以容纳单个可组合函数的表面,并包含装饰选项。可以通过边框、形状等添加装饰。在本节中,您将使用 Card 为列表项添加形状。

The Woof list item with shape dimensions added to it

  1. 打开 Shape.kt 文件。Card 是一个 medium 组件,因此您需要添加 Shapes 对象的 medium 参数。对于此应用,列表项的右上角和左下角需要有圆角,但不能完全圆形。要实现这一点,请将 16.dp 传入 medium 属性。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

由于 Card 默认已经使用 medium 形状,您无需显式将其设置为 medium 形状。查看 Preview,看看新形状的 Card

Woof preview with shaped cards

如果您回到 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(小)尺寸。只有当您想自定义应用时才需要使用这些选项。如果您不知道为每个类型比例类别设置什么,请记住有一个可以使用的默认排版比例。

999a161dcd9b0ec4.png

类型比例包含可重用的文本类别,每个类别都有预期的应用和含义。

Display(展示)

作为屏幕上最大的文本,展示样式保留用于简短、重要的文本或数字。它们最适用于大屏幕。

Headline(标题)

标题最适合在较小的屏幕上显示简短、高强调的文本。这些样式可以很好地用于标记主要文本段落或重要内容区域。

Title(正标题)

正标题小于标题样式,应用于相对简短的中等强调文本。

Body(正文)

正文样式用于应用中的较长文本段落。

Label(标签)

标签样式是较小、实用的样式,用于组件内的文本或内容主体中非常小的文本,如说明文字。

字体

Android 平台提供了各种字体,但您可能希望使用默认未提供的字体自定义您的应用。自定义字体可以增加个性,并用于品牌推广。

在本节中,您将添加名为 Abril FatfaceMontserrat BoldMontserrat Regular 的自定义字体。您将使用 Material Type 系统中的 displayLarge 和 displayMedium 标题以及 bodyLarge 文本,并将其添加到应用中的文本中。

创建一个字体 Android 资源目录。

在向应用添加字体之前,您需要添加一个字体目录。

  1. 在 Android Studio 的项目视图中,右键点击 res 文件夹。
  2. 选择 New(新建)> Android Resource Directory(Android 资源目录)。

This image shows navigating the file structure to the Android Resource Directory.

  1. 将目录命名为 font,将资源类型设置为 font,然后点击 OK(确定)。

This image shows adding a font directory using the New Resource Directory.

  1. 打开位于 res > font 的新字体资源目录。

下载自定义字体

由于您使用的是 Android 平台默认未提供的字体,您需要下载自定义字体。

  1. 转到 https://fonts.google.com/
  2. 搜索 Montserrat 并点击 Download family(下载字体包)。
  3. 解压 zip 文件。
  4. 打开下载的 Montserrat 文件夹。在 static 文件夹中,找到 Montserrat-Bold.ttfMontserrat-Regular.ttfttf 是 TrueType Font 的缩写,是字体文件的格式)。选择这两个字体并将它们拖到 Android Studio 项目的字体资源目录中。

This image shows the contents of the static folder of Montserrat fonts.

  1. 在您的字体文件夹中,将 Montserrat-Bold.ttf 重命名为 montserrat_bold.ttf,将 Montserrat-Regular.ttf 重命名为 montserrat_regular.ttf
  2. 搜索 Abril Fatface 并点击 Download family(下载字体包)。
  3. 打开下载的 Abril_Fatface 文件夹。选择 AbrilFatface-Regular.ttf 并将其拖到字体资源目录中。
  4. 在您的字体文件夹中,将 Abril_Fatface_Regular.ttf 重命名为 abril_fatface_regular.ttf

这就是您项目中包含三个自定义字体文件的字体资源目录的样子

This image shows the font files being added to the font folder.

初始化字体

  1. 在项目窗口中,打开 ui.theme > Type.kt。在 import 语句下方和 Typography val 上方初始化下载的字体。首先,通过将其设置为等于 FontFamily 并传入包含字体文件 abril_fatface_regularFont 来初始化 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)
)
  1. Abril Fatface 下面初始化 Montserrat,通过将其设置为等于 FontFamily 并传入包含字体文件 montserrat_regularFont。对于 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 种不同字体的参数。您可以根据需要定义任意数量。在此应用中,我们将设置 displayLargedisplayMediumbodyLarge。在此应用的下一部分中,您将使用 labelSmall,因此您将其添加到此处。

下表显示了您要添加的每个标题的字体、字重和大小。

8ea685b3871d5ffc.png

  1. 对于 displayLarge 属性,将其设置为等于 TextStyle,并使用上表中的信息填充 fontFamilyfontWeightfontSize。这意味着所有设置为 displayLarge 的文本将使用 Abril Fatface 字体,字重 normal,fontSize36.sp

displayMediumlabelSmallbodyLarge 重复此过程。

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
)

为应用文本添加排版

现在,您将为应用中的每个文本实例添加标题类型。

  1. 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
       )
   }
}
  1. 现在在 WoofPreview() 中,狗的名字以 20.sp 大小显示粗体 Montserrat 字体,狗的年龄以 14.sp 大小显示正常 Montserrat 字体。

Woof preview with typography added

下面是添加排版前后列表项的并排视图。请注意狗的名字和狗的年龄之间的字体差异。

无排版

有排版

7. 添加顶部栏

一个 Scaffold 是一种布局,为各种组件和屏幕元素(例如 ImageRowColumn)提供了槽位。Scaffold 还为 TopAppBar 提供了一个槽位,您将在本节中使用它。

TopAppBar 可以用于多种目的,但在本例中,您将使用它来展示品牌并赋予应用个性。TopAppBar 有四种不同类型:center(居中)、small(小)、medium(中)和 large(大)。在此 Codelab 中,您将实现一个居中顶部应用栏。您将创建一个看起来像下面截图的可组合函数,并将其放入 ScaffoldtopBar 部分。

172417c7b64372f7.png

对于此应用,我们的顶部栏由一个 Row 组成,其中包含一个徽标图片和应用标题文本。徽标采用可爱的渐变爪印和应用标题!

736f411f5067e0b5.png

向顶部栏添加图片和文本

  1. MainActivity.kt 中,创建一个名为 WoofTopAppBar() 的可组合函数,带有一个可选的 modifier
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
  
}
  1. 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))
                )
            }
        }
    }
}
  1. 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() 中没有任何内容。让我们来改变它!

Woof Preview with typography

  1. WoofTopAppBar() Composable 中,添加一个 CenterAlignedTopAppBar() 并将 modifier 参数设置为传递到 WoofTopAppBar() 中的 modifier。
import androidx.compose.material3.CenterAlignedTopAppBar

@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {
   CenterAlignedTopAppBar(
       modifier = modifier
   )
}
  1. 对于 title 参数,传入一个 Row,它将包含 CenterAlignedTopAppBarImageText
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier){
   CenterAlignedTopAppBar(
       title = {
           Row() {
              
           }
       },
       modifier = modifier
   )
}
  1. 将徽标 Image 添加到 Row 中。
  • modifier 中的图片尺寸设置为 dimens.xml 文件中的 image_size,将内边距设置为 dimens.xml 文件中的 padding_small
  • 使用 painterImage 设置为 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
   )
}
  1. 接下来,在 Image 之后,在 Row 内部添加一个 Text Composable。
  • 使用 stringResource() 将其设置为 app_name 的值。这将文本设置为应用的名称,该名称存储在 strings.xml 中。
  • 将文本样式设置为 displayLarge,因为应用名称是简短且重要的文本。
Text(
   text = stringResource(R.string.app_name),
   style = MaterialTheme.typography.displayLarge
)

Woof Preview with top app bar

这就是 WoofPreview() 中显示的内容,看起来有点不对劲,因为图标和文本没有垂直对齐。

  1. 为了解决这个问题,向 Row 添加一个 verticalAlignment 值参数,并将其设置为等于 Alignment.CenterVertically
import androidx.compose.ui.Alignment

Row(
   verticalAlignment = Alignment.CenterVertically
)

Woof Preview with vertically centered top app bar

这样看起来好多了!

这是完整的 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 如何精美地将应用串联起来。

无顶部应用栏

有顶部应用栏

现在,看看最终应用在深色主题下的样子!

2776e6a45cf3434a.png

恭喜您,您已完成本 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 对象设置应用的 colortypographyshapescontent
  • Color.kt 是您列出应用中使用的颜色的位置。然后在 Theme.kt 中,将 LightColorPaletteDarkColorPalette 中的颜色分配到特定的槽位。并非所有槽位都需要分配。
  • 您的应用可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您自己实现深色主题,用户体验会更好,这样您可以完全控制应用主题。
  • Shape.kt 是您定义应用形状的位置。有三种形状大小(small、medium、large),您可以指定圆角的程度。
  • 形状引导注意力、识别组件、传达状态和表达品牌。
  • Type.kt 是您初始化字体并为 Material Design 类型比例分配 fontFamilyfontWeightfontSize 的位置。
  • 测量系统Material Design 类型比例包含一系列对比鲜明的样式,支持您的应用及其内容的需求。类型比例是类型系统支持的 15 种样式的组合。

10. 了解更多