使用 Jetpack Compose 进行材质主题设计

1. 开始之前

Material Design 是由 Google 设计师和开发者构建和支持的设计系统,用于为 Android 以及其他移动和 Web 平台构建高质量的数字体验。它提供了有关如何以易读、吸引人和一致的方式构建应用程序 UI 的指南。

在本 Codelab 中,您将学习有关材质主题设计的知识,它允许您在应用中使用 Material Design,并提供有关自定义颜色、排版和形状的指导。您可以根据需要对应用程序进行少量或大量自定义。您还将学习如何添加顶部应用栏以显示应用程序的名称和图标。

先决条件

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

您将学到什么

  • 如何将材质主题应用于 Compose 应用程序。
  • 如何向应用程序添加自定义调色板。
  • 如何向应用程序添加自定义字体。
  • 如何向应用程序中的元素添加自定义形状。
  • 如何向应用程序添加顶部应用栏。

您将构建什么

  • 您将构建一个美丽的应用程序,其中包含 Material Design 的最佳实践。

您需要什么

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

2. 应用程序概述

在本 Codelab 中,您将创建 **Woof**,一个显示狗列表并使用 Material Design 创建精美应用体验的应用程序。

92eca92f64b029cf.png

在本 Codelab 中,我们将向您展示使用材质主题设计可以实现的一些功能。将此 Codelab 用作有关如何使用材质主题设计来改善将来创建的应用程序的外观和感觉的想法。

调色板

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

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() 以提供整个应用程序的材质主题设计。

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           WoofTheme {
               Surface(
                   modifier = Modifier.fillMaxSize()
               ) {
                   WoofApp()
               }
           }
       }
   }
}

查看 WoofPreview()。添加了 WoofTheme() 以提供您在 WoofPreview() 中看到的材质主题设计。

@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 存储库中浏览代码。

探索起始代码

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

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

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

为了在亮色主题下查看您的应用,请在您的模拟器或物理设备上执行以下操作

  1. 转到设备上的 **设置** 应用。
  2. 搜索 **暗色主题** 并点击进入。
  3. 如果已启用 **暗色主题**,请将其关闭。

运行起始代码以查看您开始时的内容;它是一个显示狗的照片、姓名和年龄的列表。它功能齐全,但外观不佳,因此我们将对其进行修复。

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

使用材质主题生成器创建配色方案

要为我们的应用创建自定义配色方案,我们将使用材质主题生成器。

  1. 点击此链接转到 材质主题生成器
  2. 在左侧窗格中,您将看到核心颜色,点击主要颜色

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

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

  • **主要**颜色用于 UI 中的关键组件。
  • **次要**颜色用于 UI 中不太突出的组件。
  • **三级**颜色用于对比色调,可用于平衡主要颜色和次要颜色,或将注意力吸引到某个元素上,例如输入字段。
  • **on** 颜色元素出现在调色板中其他颜色的**顶部**,主要应用于文本、图标和描边。在我们的调色板中,我们有一个 **onSurface** 颜色,它出现在 **surface** 颜色的顶部,还有一个 **onPrimary** 颜色,它出现在 **primary** 颜色的顶部。

拥有这些插槽可以带来一致的设计系统,其中相关组件的颜色相似。

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

将调色板添加到主题中

在材质主题生成器页面上,您可以点击**导出**按钮下载包含您在主题生成器中创建的自定义主题的 **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

颜色映射

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

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

  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** 文件中的值,这是正确的格式

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** 中添加另一个预览可组合项。这样,当您更改代码中的 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()
   }
}

现在向下滚动 **设计** 窗格以查看深色主题下的应用,包括更暗的应用/列表项背景和更浅的文本。比较深色主题和亮色主题之间的差异。

暗色主题

亮色主题

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

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

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

bc31a94207265b08.png

此代码实验室更侧重于亮色主题,因此在继续使用应用之前,请关闭深色主题。

  1. 转到设备上的 **设置** 应用。
  2. 选择 **显示**。
  3. 关闭 **深色主题**。

比较本节开头应用的外观与现在的外观。列表项和文本更加清晰,颜色方案更具视觉吸引力。

无颜色

有颜色(亮色主题)

有颜色(深色主题)

动态颜色

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. 要更改设备或模拟器的背景,请转到 **设置**,然后搜索 **壁纸**。
  2. 将壁纸更改为一种或一组颜色。
  3. 重新运行您的应用以查看动态主题(请注意,您的设备或模拟器必须为 Android 12+ 才能查看动态颜色),随意使用不同的壁纸进行尝试!

710bd13f6b189dc5.png

  1. 此代码实验室确实侧重于自定义主题,因此在继续操作之前,请禁用 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 中组件的形状。组件有三种类型:小、中和大。在本节中,您将修改 Card 组件,该组件定义为 medium 大小。组件根据其大小分为 形状类别

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

将狗的图像塑造成一个圆形

  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 是一个中等组件,因此您添加了 Shapes 对象的 medium 参数。对于此应用,列表项的右上角和左下角,但不使其完全呈圆形。为此,请将 16.dp 传递给 medium 属性。
medium = RoundedCornerShape(bottomStart = 16.dp, topEnd = 16.dp)

由于 Card 默认情况下已使用 medium 形状,因此您无需显式地将其设置为 medium 形状。查看 **预览** 以查看新塑形的 Card

Woof preview with shaped cards

如果您返回到 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 的自定义字体。您将使用 Material Type 系统中的 displayLarge 和 displayMedium 标题以及 bodyLarge 文本,并将这些添加到应用中的文本。

创建字体 Android 资源目录。

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

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

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

  1. 将目录命名为 font,将资源类型设置为 font,然后单击 确定

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

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

下载自定义字体

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

  1. 访问 https://fonts.google.com/
  2. 搜索 Montserrat 并单击 下载字体系列
  3. 解压缩 zip 文件。
  4. 打开下载的 Montserrat 文件夹。在 static 文件夹中,找到 Montserrat-Bold.ttfMontserrat-Regular.ttfttf 代表 TrueType 字体,是字体文件的格式)。选择这两个字体并将它们拖到 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 并单击 下载字体系列
  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 作为字体,字体粗细为正常,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 preview with typography added

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

无排版

有排版

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 参数,该参数可以帮助指定支架内容的内边距。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 可组合项。
  • 使用 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 主题允许您在应用程序中使用 Material Design,并提供有关自定义颜色、排版和形状的指导。
  • Theme.kt 文件是在可组合函数 [your app name]+Theme()(在本例中为 WoofTheme())中定义主题的地方。在此函数中,MaterialTheme 对象设置应用程序的 colortypographyshapescontent
  • Color.kt 是您列出应用程序中使用的颜色的地方。然后在 Theme.kt 中,您将 LightColorPaletteDarkColorPalette 中的颜色分配到特定的插槽。并非所有插槽都需要分配。
  • 您的应用程序可以选择加入强制深色模式,这意味着系统将为您实现深色主题。但是,如果您实现深色主题,以便完全控制应用程序主题,则可以为用户提供更好的体验。
  • Shape.kt 是您定义应用程序形状的地方。有三种形状大小(小、中、大),您可以指定角的圆角方式。
  • 形状引导注意力、识别组件、传达状态并表达品牌。
  • Type.kt 是您初始化字体并为 Material Design 字体比例分配 fontFamilyfontWeightfontSize 的地方。
  • Material Design 字体比例 包括一系列对比鲜明的样式,以满足您的应用程序及其内容的需求。字体比例是类型系统支持的 15 种样式的组合。

10. 了解更多