使用 Jetpack Compose 实现 Material 主题设置

1. 准备工作

Material Design 是 Google 设计人员和开发者打造的一套设计体系,旨在帮助开发者针对 Android 以及其他移动平台和网络平台打造优质的数字体验。它提供了一些准则,指导如何以具有可读性、吸引力和一致性的方式构建应用界面。

在此 Codelab 中,您将学习 Material 主题设置,获得自定义颜色、排版和形状方面的指导,以便在您的应用中使用 Material Design。您可以根据应用的需要进行自定义。您还将学习如何添加顶部应用栏,以显示应用的名称和图标。

前提条件

  • 熟悉 Kotlin 语言,包括语法、函数和变量。
  • 能够在 Compose 中构建布局,包括带内边距的行和列。
  • 能够在 Compose 中创建简单列表。

学习内容

  • 如何将 Material 主题设置应用于 Compose 应用。
  • 如何为您的应用添加自定义调色板。
  • 如何为您的应用添加自定义字体。
  • 如何向应用中的元素添加自定义形状。
  • 如何向应用中添加顶部应用栏。

构建内容

  • 您将构建一个遵循 Material Design 最佳实践的精美应用。

所需条件

  • 最新版本的 Android Studio。
  • 用于下载起始代码和字体的互联网连接。

2. 应用概览

在此 Codelab 中,您将创建 Woof,该应用会显示狗狗列表,并使用 Material Design 打造出色的应用体验。

92eca92f64b029cf.png

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

调色板

以下是我们将创建的浅色和深色主题的调色板。

此图片显示的是 Woof 应用的浅色配色方案。

此图片显示的是 Woof 应用的深色配色方案。

以下是采用浅色主题和深色主题的最终应用。

浅色主题

深色主题

排版

以下是您将在应用中使用的字型样式。

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. 打开设备的设置应用。
  2. 搜索深色主题,然后点击进入该主题。
  3. 如果深色主题处于开启状态,请将其关闭。

运行起始代码以查看应用的起始状态;这是一个列表,其中会显示狗狗的照片、名字和年龄。它能正常运行,但外观不好看,所以我们会解决该问题。

6d253ae50c63014d.png

4. 添加颜色

Woof 应用中,您首先要修改的就是配色方案。

配色方案是您的应用使用的颜色组合。不同的颜色组合会激发不同的心情,这会影响用户使用您的应用时的感受。

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

这显示的是用于创建颜色的十六进制数字。

颜色还可以包含 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”(主色):

这显示的是 Material Theme Builder 中的四种核心颜色

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

这是 HCT 颜色选择器,用于在 Material Theme Builder 中选择自定义颜色。

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

这显示的是 HCT 颜色选择器设置为绿色

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

这显示的是 Material Theme Builder 中应用对 HCT 颜色选择器中颜色变化的回应。

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

Material Theme Builder 浅色方案

Material Theme Builder 生成的深色方案

您可能想了解所有这些角色的含义及其使用方法,以下是其中几个主要角色:

  • primary(主色)用于整个界面的关键组件。
  • secondary(副色)用于界面中不太显眼的组件。
  • tertiary(第三色)用于对比强调,可以平衡主色和副色,或者引起用户对某个元素(例如输入字段)的高度关注。
  • on 颜色元素显示在调色板中其他颜色的上层,主要应用于文本、图标和描边。在调色板中,我们有一个 onSurface 颜色(显示在 surface 颜色的上层)和一个 onPrimary 颜色(显示在主要颜色的上层)。

有了这些槽可以实现统一的设计体系,相关组件的颜色也相似。

关于颜色的理论讲解到此为止 - 现在可以向应用中添加这个漂亮的调色板了!

向主题添加调色板

在 Material Theme Builder 页面上,您可以选择点击 Export(导出)按钮,以便下载 Color.kt 文件以及包含您在 Theme Builder 中所建自定义主题的 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,并且 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
   }
  1. 重新运行应用,请注意,应用栏已自动更改颜色。

b48b3fa2ecec9b86.png

颜色映射

Material 组件会自动映射到颜色槽。整个界面中的其他关键组件(例如悬浮操作按钮)也默认为主色。也就是说,您无需为组件明确分配颜色;当您在应用中设置颜色主题时,它会自动映射到颜色槽。您可以通过在代码中明确设置颜色来覆盖此设置。您可以点击此处,详细了解颜色角色。

在本部分中,我们将使用 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() 中的修饰符传递给 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)
   }
}
  1. 您可以查看 WoofPreview()。由于使用了 Card 可组合项,列表项现在已经自动更改了颜色。颜色看上去非常棒,但列表项之间没有间距。

6d49372a1ef49bc7.png

Dimens 文件

就像在应用中使用 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 文件中添加值,请使用以下正确格式:

展示了如何正确设置尺寸资源中所添加值的格式

例如,要添加 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 系统中,可以选择将设备切换为深色主题。深色主题使用更暗、更柔和的颜色,并且:

  • 可以大幅减少耗电量(具体取决于设备的屏幕技术)。
  • 为弱视以及对强光敏感的用户提高可视性。
  • 让所有人都可以在光线较暗的环境中更轻松地使用设备。

您的应用可以选择启用 Force Dark,这意味着系统会为您实现深色主题。不过,如果您实现深色主题,可为用户提供更好的体验,以便您继续完全控制应用主题。

在选择自己的深色主题时,请务必注意,深色主题的颜色必须符合无障碍功能对比度标准。深色主题使用较深的界面颜色,且色彩强度有限。

在预览中查看深色主题

您在上一步中已经添加了深色主题的颜色。如需查看深色主题的实际效果,您需要向 MainActivity.kt 添加另一个 Preview 可组合项。这样,当您更改代码中的界面布局时,就能同时看到浅色主题和深色主题的预览效果。

  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. 打开设备的设置应用。
  2. 搜索深色主题,然后点击进入该主题。
  3. 开启深色主题
  4. 重新打开 Woof 应用后,该应用将采用深色主题

bc31a94207265b08.png

此 Codelab 将重点介绍浅色主题,因此,请在继续设置此应用之前关闭深色主题。

  1. 打开设备的设置应用。
  2. 选择显示
  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. 若要更改设备或模拟器的背景,请打开设置,然后搜索壁纸
  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 列表项

设置了形状的 Woof 列表项

设置了形状的 Woof 列表项

您还可以为每个角添加不同的圆角百分比,进一步自定义形状。尝试各种形状真是太有意思啦!

左上: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 列表项

设置了形状的 Woof 列表项

设置了形状的 Woof 列表项

Shape.kt 文件用于定义 Compose 中组件的形状。组件分为三种类型:小、中和大。在本部分中,您将修改定义为 medium 大小的 Card 组件。系统会根据组件的大小将组件分组为形状类别

在此部分中,您要将狗狗的图片设置为圆形,并修改列表项的形状。

将狗狗图片的形状设为圆形

  1. 打开 Shape.kt 文件,您会发现 small 参数已设为 RoundedCornerShape(50.dp)。这个设置将用来让图片的形状变为圆形。
val Shapes = Shapes(
   small = RoundedCornerShape(50.dp),
)
  1. 打开 MainActivity.kt。在 DogIcon() 中,将 clip 属性添加到 Imagemodifier;这会将图片裁剪为某种形状。传入 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. 若要将所有照片设为圆形,请添加 ContentScaleCrop 属性,这会根据显示大小裁剪图片。请注意,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 是可以包含一个可组合项并包含装饰选项的 Surface,可通过边框、形状等添加装饰。在本部分中,您将使用 Card 向列表项添加形状。

添加了形状尺寸的 Woof 列表项

  1. 打开 Shape.kt 文件。Card 是中等大小的组件,因此您要添加 Shapes 对象的 medium 参数。对于此应用,列表项的右上角和左下角要设为圆角,但又不是设为完全圆形。为此,请将 16.dp 传递给 medium 属性。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

由于 Card 已默认使用中等形状,因此您无需明确地将其设为中型。查看预览,看看采用新形状的 Card

卡片设置了形状的 Woof 预览

如果您返回到 WoofTheme() 中的 Theme.kt 文件并查看 MaterialTheme(),会看到 shapes 属性设置为您刚刚更新的 Shapes val

MaterialTheme(
   colors = colors,
   typography = Typography,
   shapes = Shapes,
   content = content
)

下图并排展示了列表项在调整形状前后的效果。请注意,如果添加形状,应用的外观会更具吸引力。

不调整形状

调整形状

6. 添加排版

Material Design 字型比例

字型比例是一系列字体样式的选择,可在应用中使用,确保样式既灵活又一致。Material Design 字型比例包含字型系统支持的 15 种字体样式。命名和分组已简化为:显示、大标题、标题、正文和标签,每个都有大号、中号和小号。只有在您想自定义应用时,才需要使用这些选项。如果您不知道为每个字型比例类别设置什么,请注意,您可以使用默认的排版缩放设置。

999a161dcd9b0ec4.png

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

显示

作为屏幕上最大的文本,显示字体专用于简短的重要文字或数字,最适合在大屏幕设备上使用。

大标题

大标题最适合用来在小屏幕设备上显示高强调度的简短文本。这类样式有助于标记文本的主要段落或重要内容区域。

标题

标题比大标题样式要小,适用于内容相对较短的中强调度文本。

正文

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

标签

标签样式是较小的实用样式,用于显示组件内部的文本或内容正文中非常小的文本(例如字幕)。

字体

虽然 Android 平台提供了各种字体,但您可能需要使用非默认提供的字体来自定义应用。自定义字体可以增添个性,并可用于品牌塑造。

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

创建字体 Android 资源目录。

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

  1. 在 Android Studio 的项目视图中,右键点击 res 文件夹。
  2. 依次选择 New > Android Resource Directory

此图片显示了将文件结构导航到 Android 资源目录的情况。

  1. 将目录命名为 font,将资源类型设为 font,然后点击 OK

此图片显示了使用新字体资源目录添加字体目录的过程。

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

下载自定义字体

由于您使用的不是 Android 平台提供的字体,因此您需要下载自定义字体。

  1. 访问 https://fonts.google.com/
  2. 搜索 Montserrat,然后点击 Download family
  3. 解压缩该 ZIP 文件。
  4. 打开下载的 Montserrat 文件夹。在 static 文件夹中,找到 Montserrat-Bold.ttfMontserrat-Regular.ttfttf 代表 TrueType 字体,即字体文件的格式)。选择两种字体,将它们拖动到 Android Studio 中项目的字体资源目录中。

此图片显示了 Montserrat 字体的静态文件夹的内容。

  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

项目中的字体资源目录和三个自定义字体文件应如下所示:

此图片显示了添加到字体文件夹的字体文件。

初始化字体

  1. 在项目窗口中,依次打开 ui.theme > Type.kt。在 import 语句下方和 Typography val 上方初始化下载的字体。首先,初始化 Abril Fatface,方法是将其设为 FontFamily 并使用字体文件 abril_fatface_regular 传入 Font
​​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_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 种不同字体的参数。您可以根据需要定义任意数量。在此应用中,我们将设置 displayLargedisplayMediumbodyLarge。在此应用的下一部分中,您将使用 labelSmall,因此需在此处添加。

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

8ea685b3871d5ffc.png

  1. 对于 displayLarge 属性,应将其设为 TextStyle,并使用上表中的信息填写 fontFamilyfontWeightfontSize。这意味着所有设为 displayLarge 的文本都将使用 Abril Fatface 作为字体,字体粗细正常,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
   )
)

如果您打开 WoofTheme() 中的 Theme.kt 文件并查看 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 预览

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

排版前

排版后

7. 添加顶部栏

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

TopAppBar 可用于许多用途,但在本例中,您会将其用于品牌宣传以及彰显应用个性。TopAppBar 有四种不同的类型:居中、小、中和大。在此 Codelab 中,您将实现一个居中的顶部应用栏。您将创建一个类似于以下屏幕截图的可组合项,并将其放入 ScaffoldtopBar 部分。

172417c7b64372f7.png

对于此应用,顶部栏由一个包含徽标图片和应用名称文字的 Row 组成。徽标包含可爱的渐变色的爪子和应用名称!

736f411f5067e0b5.png

向顶部栏添加图片和文字

  1. MainActivity.kt 中,创建一个名为 WoofTopAppBar() 且带有可选 modifier 的可组合项。
@Composable
fun WoofTopAppBar(modifier: Modifier = Modifier) {

}
  1. Scaffold 支持 contentWindowInsets 参数,该参数有助于为 Scaffold 内容指定边衬区。WindowInsets 是屏幕上应用可与系统界面相交的部分,应通过 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 预览

  1. WoofTopAppBar() Composable 中,添加 CenterAlignedTopAppBar() 并将修饰符参数设置为传入 WoofTopAppBar() 的修饰符。
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 设为可绘制对象文件夹中的 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. 接下来,在 Row 中的 Image. 后添加一个 Text 可组合项。
  • 使用 stringResource() 将其设为 app_name 的值。这会将文本设为存储在 strings.xml 中的应用的名称。
  • 将文本样式设为 displayLarge,因为应用名称是简短的重要文本。
Text(
   text = stringResource(R.string.app_name),
   style = MaterialTheme.typography.displayLarge
)

带有顶部应用栏的 Woof 预览

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

  1. 若要解决此问题,请向 Row 添加 verticalAlignment 值参数,并将其设为 Alignment.CenterVertically
import androidx.compose.ui.Alignment

Row(
   verticalAlignment = Alignment.CenterVertically
)

顶部应用栏居中垂直的 Woof 预览

这样看起来好多了!

以下是完整的 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 主题设置,您可以依据关于自定义颜色、排版和形状的指南,在应用中使用 Material Design。
  • 主题是在 Theme.kt 文件中通过名为 [your app name]+Theme() 的可组合项定义的(在此应用中是 WoofTheme())。在该函数内,MaterialTheme 对象用于设置应用的 colortypographyshapescontent
  • Color.kt 用于列出您在应用中使用的颜色。然后,在 Theme.kt 中,将 LightColorPaletteDarkColorPalette 中的颜色分配给特定槽。并非所有槽都需要分配。
  • 您的应用可以选择启用 Force Dark,这意味着系统会为您实现深色主题。不过,如果您实现深色主题,可为用户提供更好的体验,以便您能够完全控制应用主题。
  • Shape.kt 用于定义应用形状。形状尺寸有三种(小、中、大),您可以指定角的圆角程度。
  • 形状能够引导用户注意力、区别组件、传达状态以及呈现品牌风格。
  • Type.kt 用于初始化字体并为 Material Design 字型比例分配 fontFamilyfontWeightfontSize
  • Material Design 字型比例包含一系列对比鲜明的样式,可支持您的应用及其内容的需求。字型比例是字型系统支持的 15 种样式的组合。

10. 了解更多内容