Compose 中的 Material Design 2

Jetpack Compose 提供了 Material Design 的实现,这是一个用于创建数字界面的综合设计系统。 Material Design 组件(按钮、卡片、开关等)建立在 Material Theming 之上,这是一种系统化方法,用于自定义 Material Design 以更好地反映产品的品牌。Material 主题包含 颜色字体形状 属性。当您自定义这些属性时,您的更改会自动反映在您用于构建应用程序的组件中。

Jetpack Compose 使用 MaterialTheme 可组合项来实现这些概念。

MaterialTheme(
    colors = // ...
    typography = // ...
    shapes = // ...
) {
    // app content
}

配置传递给 MaterialTheme 的参数来为您的应用程序设置主题。

Two contrasting screenshots. The first uses default MaterialTheme styling,
the second screenshot uses modified styling.

图 1. 第一个屏幕截图显示了一个未配置 MaterialTheme 的应用程序,因此它使用默认样式。第二个屏幕截图显示了一个传递参数到 MaterialTheme 以自定义样式的应用程序。

颜色

颜色在 Compose 中使用 Color 类建模,这是一个简单的保存数据的类。

val Red = Color(0xffff0000)
val Blue = Color(red = 0f, green = 0f, blue = 1f)

虽然您可以根据需要组织它们(作为顶层常量、在单例中,或内联定义),但我们强烈建议在您的主题中指定颜色,并从那里检索颜色。这种方法可以轻松地支持 深色主题 和嵌套主题。

Example of theme's color palette

图 2. Material 颜色系统。

Compose 提供了 Colors 类来模拟 Material 颜色系统Colors 提供了构建器函数,用于创建 浅色深色 颜色集。

private val Yellow200 = Color(0xffffeb46)
private val Blue200 = Color(0xff91a4fc)
// ...

private val DarkColors = darkColors(
    primary = Yellow200,
    secondary = Blue200,
    // ...
)
private val LightColors = lightColors(
    primary = Yellow500,
    primaryVariant = Yellow400,
    secondary = Blue700,
    // ...
)

定义好 Colors 后,您可以将它们传递给 MaterialTheme

MaterialTheme(
    colors = if (darkTheme) DarkColors else LightColors
) {
    // app content
}

使用主题颜色

您可以使用 MaterialTheme.colors 检索提供给 MaterialTheme 可组合项的 Colors

Text(
    text = "Hello theming",
    color = MaterialTheme.colors.primary
)

表面颜色和内容颜色

许多组件接受一对颜色和内容颜色。

Surface(
    color = MaterialTheme.colors.surface,
    contentColor = contentColorFor(color),
    // ...
) { /* ... */ }

TopAppBar(
    backgroundColor = MaterialTheme.colors.primarySurface,
    contentColor = contentColorFor(backgroundColor),
    // ...
) { /* ... */ }

这使您不仅可以设置可组合项的颜色,还可以为内容(其中包含的可组合项)提供默认颜色。许多可组合项默认情况下使用此内容颜色。例如,Text 基于其父级的 content color 设置其颜色,而 Icon 使用该颜色来设置其色调。

Two examples of the same banner, with different colors

图 3. 设置不同的背景色会产生不同的文本和图标颜色。

contentColorFor() 方法检索任何主题颜色的适当“on”颜色。例如,如果您在 Surface 上设置 primary 背景色,它将使用此函数将 onPrimary 设置为内容颜色。如果您设置非主题背景色,则还应指定适当的内容颜色。使用 LocalContentColor 在层次结构中的给定位置检索当前背景的首选内容颜色。

内容 Alpha

通常您希望改变内容的强调程度,以传达重要性并提供视觉层次结构。 Material Design 文本可读性建议 建议使用不同级别的透明度来传达不同的重要性级别。

Jetpack Compose 通过 LocalContentAlpha 实现此功能。您可以通过 为该 CompositionLocal 提供值 来为层次结构指定内容 alpha。嵌套的可组合项可以使用此值将 alpha 处理应用于其内容。例如,TextIcon 默认情况下使用 LocalContentColor 的组合,并调整为使用 LocalContentAlpha。Material 指定了一些标准 alpha 值(highmediumdisabled),这些值由 ContentAlpha 对象建模。

// By default, both Icon & Text use the combination of LocalContentColor &
// LocalContentAlpha. De-emphasize content by setting content alpha
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
    Text(
        // ...
    )
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
    Icon(
        // ...
    )
    Text(
        // ...
    )
}

要了解有关 CompositionLocal 的更多信息,请查看 使用 CompositionLocal 进行局部范围数据指南

Screenshot of an article title, showing different levels of text
emphasis

图 4. 将不同级别的强调应用于文本,以直观地传达信息层次结构。第一行文本是标题,包含最重要的信息,因此使用 ContentAlpha.high。第二行包含不太重要的元数据,因此使用 ContentAlpha.medium

深色主题

在 Compose 中,您通过为 MaterialTheme 可组合项提供不同的 Colors 集来实现浅色和深色主题。

@Composable
fun MyTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    MaterialTheme(
        colors = if (darkTheme) DarkColors else LightColors,
        /*...*/
        content = content
    )
}

在此示例中,MaterialTheme 被包装在其自己的可组合函数中,该函数接受一个参数,该参数指定是否使用深色主题。在本例中,该函数通过查询 设备主题设置 来获取 darkTheme 的默认值。

您可以使用类似这样的代码来检查当前 Colors 是浅色还是深色。

val isLightTheme = MaterialTheme.colors.isLight
Icon(
    painterResource(
        id = if (isLightTheme) {
            R.drawable.ic_sun_24
        } else {
            R.drawable.ic_moon_24
        }
    ),
    contentDescription = "Theme"
)

海拔叠加

在 Material 中,深色主题中具有较高海拔的表面会收到 海拔叠加,这会使其背景变亮。表面的海拔越高(使其更靠近隐含的光源),该表面就越亮。

当使用深色时,这些叠加会由 Surface 可组合项自动应用,以及使用表面的任何其他 Material 可组合项。

Surface(
    elevation = 2.dp,
    color = MaterialTheme.colors.surface, // color will be adjusted for elevation
    /*...*/
) { /*...*/ }

Screenshot of an app, showing the subtly different colors used for elements
at different elevation levels

图 5. 卡片和底部导航都使用 surface 颜色作为其背景。由于卡片和底部导航在背景之上具有不同的海拔级别,因此它们具有略微不同的颜色 - 卡片比背景更亮,而底部导航比卡片更亮。

对于不涉及 Surface 的自定义场景,请使用 LocalElevationOverlay,这是一个包含 ElevationOverlayCompositionLocal,该 ElevationOverlaySurface 组件使用。

// Elevation overlays
// Implemented in Surface (and any components that use it)
val color = MaterialTheme.colors.surface
val elevation = 4.dp
val overlaidColor = LocalElevationOverlay.current?.apply(
    color, elevation
)

要禁用海拔叠加,请在可组合项层次结构中的所需位置提供 null

MyTheme {
    CompositionLocalProvider(LocalElevationOverlay provides null) {
        // Content without elevation overlays
    }
}

有限的颜色强调

Material 建议通过在大多数情况下优先使用 surface 颜色而不是 primary 颜色来对深色主题应用 有限的颜色强调。Material 可组合项(如 TopAppBarBottomNavigation)默认情况下会实现这种行为。

图 6. 具有有限颜色强调的 Material 深色主题。顶部应用栏在浅色主题中使用主要颜色,而在深色主题中使用表面颜色。

对于自定义场景,请使用 primarySurface 扩展属性。

Surface(
    // Switches between primary in light theme and surface in dark theme
    color = MaterialTheme.colors.primarySurface,
    /*...*/
) { /*...*/ }

字体

Material 定义了 字体系统,鼓励您使用少量语义命名样式。

Example of several different typefaces in various styles

图 7. Material 字体系统。

Compose 使用 TypographyTextStyle字体相关 类来实现字体系统。 Typography 构造函数为每种样式提供了默认值,因此您可以省略您不想自定义的任何样式。

val raleway = FontFamily(
    Font(R.font.raleway_regular),
    Font(R.font.raleway_medium, FontWeight.W500),
    Font(R.font.raleway_semibold, FontWeight.SemiBold)
)

val myTypography = Typography(
    h1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W300,
        fontSize = 96.sp
    ),
    body1 = TextStyle(
        fontFamily = raleway,
        fontWeight = FontWeight.W600,
        fontSize = 16.sp
    )
    /*...*/
)
MaterialTheme(typography = myTypography, /*...*/) {
    /*...*/
}

如果您想在整个应用程序中使用相同的字体,请指定 defaultFontFamily 参数 并省略任何 TextStyle 元素的 fontFamily

val typography = Typography(defaultFontFamily = raleway)
MaterialTheme(typography = typography, /*...*/) {
    /*...*/
}

使用文本样式

TextStyle 通过 MaterialTheme.typography 访问。像这样检索 TextStyle

Text(
    text = "Subtitle2 styled",
    style = MaterialTheme.typography.subtitle2
)

Screenshot showing a mixture of different typefaces for different purposes

图 8. 使用一系列字体和样式来表达您的品牌。

形状

Material 定义了 形状系统,允许您为大型、中型和小型组件定义形状。

Shows a variety of Material Design shapes

图 9. Material 形状系统。

Compose 使用 Shapes 类来实现形状系统,该类允许您为每个尺寸类别指定一个 CornerBasedShape

val shapes = Shapes(
    small = RoundedCornerShape(percent = 50),
    medium = RoundedCornerShape(0f),
    large = CutCornerShape(
        topStart = 16.dp,
        topEnd = 0.dp,
        bottomEnd = 0.dp,
        bottomStart = 16.dp
    )
)

MaterialTheme(shapes = shapes, /*...*/) {
    /*...*/
}

许多组件默认使用这些形状。例如,ButtonTextFieldFloatingActionButton 默认使用小尺寸,AlertDialog 默认使用中尺寸,ModalDrawer 默认使用大尺寸 - 请查看 形状方案参考 以获取完整的映射。

使用形状

Shape 可通过 MaterialTheme.shapes 访问。使用类似以下代码检索 Shape

Surface(
    shape = MaterialTheme.shapes.medium, /*...*/
) {
    /*...*/
}

Screenshot of an app that uses Material shapes to convey what state an element is in

图 10. 使用形状表达品牌或状态。

默认样式

Compose 中没有与 Android Views 中的 默认样式 等效的概念。您可以通过创建自己的“重载”可组合函数来提供类似的功能,这些函数包装了 Material 组件。例如,要创建一种按钮样式,请将按钮包装在您自己的可组合函数中,直接设置您希望更改的参数,并将其他参数公开到包含的可组合函数中。

@Composable
fun MyButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

主题叠加

您可以通过嵌套 MaterialTheme 可组合函数,在 Compose 中实现与 Android Views 中 主题叠加 等效的功能。由于 MaterialTheme 将颜色、排版和形状默认设置为当前主题值,如果主题只设置了其中一个参数,则其他参数将保留其默认值。

此外,在将基于 View 的屏幕迁移到 Compose 时,请注意 android:theme 属性的使用。您可能需要在 Compose UI 树的该部分中添加一个新的 MaterialTheme

在此示例中,详细信息屏幕对大多数屏幕使用 PinkTheme,然后对相关部分使用 BlueTheme。请查看下面的屏幕截图和代码。

图 11. 嵌套主题。

@Composable
fun DetailsScreen(/* ... */) {
    PinkTheme {
        // other content
        RelatedSection()
    }
}

@Composable
fun RelatedSection(/* ... */) {
    BlueTheme {
        // content
    }
}

组件状态

可以交互(点击、切换等)的 Material 组件可以处于不同的视觉状态。状态包括启用、禁用、按下等。

可组合函数通常具有 enabled 参数。将其设置为 false 将阻止交互,并更改颜色和高度等属性以直观地传达组件状态。

图 12. enabled = true(左)和 enabled = false(右)的按钮。

在大多数情况下,您可以依赖颜色和高度等值的默认设置。如果您希望配置不同状态下使用的值,则可以使用可用的类和便捷函数。请查看下面的按钮示例

Button(
    onClick = { /* ... */ },
    enabled = true,
    // Custom colors for different states
    colors = ButtonDefaults.buttonColors(
        backgroundColor = MaterialTheme.colors.secondary,
        disabledBackgroundColor = MaterialTheme.colors.onBackground
            .copy(alpha = 0.2f)
            .compositeOver(MaterialTheme.colors.background)
        // Also contentColor and disabledContentColor
    ),
    // Custom elevation for different states
    elevation = ButtonDefaults.elevation(
        defaultElevation = 8.dp,
        disabledElevation = 2.dp,
        // Also pressedElevation
    )
) { /* ... */ }

图 13. enabled = true(左)和 enabled = false(右)的按钮,并调整了颜色和高度值。

波纹

Material 组件使用波纹来指示它们正在被交互。如果您在层次结构中使用 MaterialTheme,则 Ripple 将用作 Indication 的默认值,在 clickableindication 等修饰符中。

在大多数情况下,您可以依赖默认的 Ripple。如果您希望配置它们的显示,可以使用 RippleTheme 来更改颜色和 alpha 等属性。

您可以扩展 RippleTheme 并使用 defaultRippleColordefaultRippleAlpha 实用函数。然后,您可以使用 LocalRippleTheme 在层次结构中提供自定义波纹主题。

@Composable
fun MyApp() {
    MaterialTheme {
        CompositionLocalProvider(
            LocalRippleTheme provides SecondaryRippleTheme
        ) {
            // App content
        }
    }
}

@Immutable
private object SecondaryRippleTheme : RippleTheme {
    @Composable
    override fun defaultColor() = RippleTheme.defaultRippleColor(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )

    @Composable
    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
        contentColor = MaterialTheme.colors.secondary,
        lightTheme = MaterialTheme.colors.isLight
    )
}

alt_text

图 14. 通过 RippleTheme 提供了不同波纹值的按钮。

了解更多

要详细了解 Compose 中的 Material 主题,请参阅以下其他资源。

Codelabs

视频