Compose 中的 Material Design 2

Jetpack Compose 提供了 Material Design 的实现,后者是一个用于创建数字化界面的综合设计系统。各种 Material Design 组件(按钮、卡片、开关等)是在 Material 主题设置的基础上构建的。Material 主题设置是一种系统化的方法,用于自定义 Material Design 以更好地体现您产品的品牌。一个 Material 主题由颜色排版形状属性组成。当您自定义这些属性时,您所做的更改会自动反映在您用来构建应用的组件中。

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

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

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

两个对比鲜明的屏幕截图。第一个屏幕截图使用默认的 MaterialTheme 样式设置,第二个屏幕截图使用修改后的样式设置。

图 1. 第一个屏幕截图显示的应用未配置 MaterialTheme,因此它使用默认的样式设置。第二个屏幕截图显示的应用通过向 MaterialTheme 传递参数,对样式设置进行了自定义。

颜色

颜色在 Compose 中使用 Color 类(一个用来存放数据的简单类)加以模拟。

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

虽然您可以按照自己喜欢的方式随意组织这些颜色(采用顶级常量的形式、在单例中加以组织或者以内嵌方式加以定义),但我们强烈建议您在主题中指定颜色并从主题中检索颜色。采用这种方法可轻松支持深色主题和嵌套主题。

主题调色板的示例

图 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 使用该颜色来设置其色调。

同一横幅的两个分别采用不同颜色的示例

图 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 将数据的作用域限定在局部”指南

一篇文章标题的屏幕截图,显示了以多种不同程度突出文本时的呈现效果

图 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 会获得高度叠加层,这会使其背景颜色变浅。表面的高度(升到较接近隐式光源)越高,表面的颜色就越浅。

使用深色时,Surface 可组合项会自动应用这些叠加层;而对于使用表面的任何其他 Material 可组合项:

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

一个应用的屏幕截图,显示对位于不同高度的元素使用了略有不同的颜色

图 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 颜色)。TopAppBarBottomNavigation 等 Material 可组合项默认会实现此行为。

图 6. 色彩强度有限的 Material 深色主题。顶部应用栏使用浅色主题的主要颜色,并使用深色主题的 Surface 颜色。

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

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

排版

Material 定义了一个字型系统,鼓励您使用少量从语义上命名的样式。

不同样式的几种不同字型的示例

图 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 parameter,并省略所有 TextStyle 元素的 fontFamily

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

使用文本样式

通过 MaterialTheme.typography 访问 TextStyle。使用如下所示的代码检索 TextStyle

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

显示不同用途的不同字型混合在一起的屏幕截图

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

形状

Material 定义了一个形状系统,可让您定义大型、中型和小型组件的形状。

显示各种 Material Design 形状

图 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 默认为“大”。如需查看完整的对应关系,请参阅形状主题参考文档

使用形状

通过 MaterialTheme.shapes 访问 Shape。使用如下所示的代码检索 Shape

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

一个应用的屏幕截图,该应用使用 Material 形状来表达元素所处的状态

图 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 View 等效的主题叠加层。由于 MaterialTheme 默认为颜色、排版和形状使用当前主题值,因此如果某个主题仅设置了其中一个参数,其他参数将保留默认值。

此外,将基于 View 的界面迁移到 Compose 时,请注意 android:theme 属性的用法。您可能需要在 Compose 界面树的相应部分添加新的 MaterialTheme

Owl 示例中,详细信息屏幕上的大部分区域使用 PinkTheme,相关部分则使用 BlueTheme,如下方的屏幕截图和代码所示。

图 11. Owl 示例中的嵌套主题。

@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 组件使用涟漪来表示正在互动。如果您在层次结构中使用 MaterialThemeRipple 将被用作 clickableindication 等修饰符内的默认 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 主题设置,请参阅下面列出的其他资源。

Codelab

视频