Compose 中的 Material Design 2

Jetpack Compose 提供了 Material Design 的实现,这是一个用于创建数字界面的综合设计系统。 Material Design 组件(按钮、卡片、开关等)构建在 Material 主题 之上,这是一种系统化的方法,可以自定义 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 检索当前背景在层次结构中给定位置的首选内容颜色。

内容透明度

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

Jetpack Compose 通过 LocalContentAlpha 实现此功能。您可以通过 为该 CompositionLocal 提供一个值 来为层次结构指定内容透明度。嵌套的可组合项可以使用此值将其内容应用透明度处理。例如,TextIcon 默认使用 LocalContentColorLocalContentAlpha 的组合。Material 指定了一些标准的透明度值(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 颜色来为深色主题应用 有限的颜色强调。像 TopAppBarBottomNavigation 这样的 Material 可组合项默认实现此行为。

图 6. 带有有限颜色强调的 Material 深色主题。顶部应用栏在浅色主题中使用 primary 颜色,在深色主题中使用 surface 颜色。

对于自定义场景,请使用 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 视图中的默认样式等效的概念。您可以通过创建自己的“重载”可组合函数来包装 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 可组合函数来实现 Android 视图中的主题叠加 的等效功能。因为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,则Ripple 将用作修饰符(例如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 主题的更多信息,请参阅以下其他资源。

Codelabs

视频