Compose 中的 Material Design 2

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

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 基于其父级的内容颜色来设置其颜色,而 Icon 则使用该颜色来设置其色调。

Two examples of the same banner, with different colors

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

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

内容 Alpha 值

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

Jetpack Compose 通过 LocalContentAlpha 实现这一点。您可以通过为该 CompositionLocal提供值来为层级结构指定内容 Alpha 值。嵌套的可组合项可以使用此值来对其内容应用 Alpha 处理。例如,TextIcon 默认使用 LocalContentColorLocalContentAlpha 结合。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 Design 中,深色主题中具有更高海拔的表面会接收海拔叠加层,这会使其背景变亮。表面的海拔越高(使其更接近隐含的光源),该表面就越亮。

这些叠加层在使用深色时由 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,这是一个包含由 Surface 组件使用的 ElevationOverlayCompositionLocal

// 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, /*...*/) {
    /*...*/
}

使用文本样式

TextStyles 通过 MaterialTheme.typography 访问。按如下方式检索 TextStyles

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 默认使用大尺寸 — 有关完整的映射,请参阅形状方案参考

使用形状

Shapes 通过 MaterialTheme.shapes 访问。使用如下代码检索 Shapes

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 默认将颜色、排版和形状设置为当前主题值,如果一个主题只设置其中一个参数,则其他参数将保留其默认值。

此外,在将基于视图的屏幕迁移到 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,则在 clickableindication 等修饰符内部,Ripple 将用作默认的Indication

在大多数情况下,您可以依赖默认的 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 Theming,请查阅以下其他资源。

Codelabs

视频