Material 组件和布局

Jetpack Compose 提供了 Material Design 的实现,后者是一个用于创建数字化界面的综合设计系统。Material 组件(按钮、卡片、开关等)和布局(如 Scaffold)可作为可组合函数提供。

Material 组件是用于创建界面的交互式构建块。Compose 提供了许多此类组件,开箱即可使用。如需了解提供了哪些组件,请参阅 Compose Material API 参考文档

Material 组件会使用应用中 MaterialTheme 提供的值:

@Composable
fun MyApp() {
    MaterialTheme {
        // Material Components like Button, Card, Switch, etc.
    }
}

如需详细了解主题设置,请参阅 Compose 中的设计系统指南

内容槽

支持内部内容(文本标签、图标等)的 Material 组件往往会提供“槽”(即接受可组合内容的通用 lambda),而且还会提供尺寸和内边距等公共常量,从而支持设置内部内容的布局,使之符合 Material 规范。

例如 Button

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

图 1. 使用 content 槽和默认内边距的 Button(左),以及使用提供自定义 contentPaddingcontent 槽的 Button(右)。

Button 有一个通用 content 尾随 lambda 槽,该槽使用 RowScope 将内容可组合项的布局设为行。此外,它还有一个 contentPadding 参数,用于将内边距应用于内部内容。您可以使用通过 ButtonDefaults 提供的常量,也可以使用自定义值。

再比如 ExtendedFloatingActionButton

ExtendedFloatingActionButton(
    onClick = { /* ... */ },
    icon = {
        Icon(
            Icons.Filled.Favorite,
            contentDescription = "Favorite"
        )
    },
    text = { Text("Like") }
)

图 2. 使用 icon 槽和 text 槽的 ExtendedFloatingActionButton

ExtendedFloatingActionButton 有两个槽,分别针对 icontext 标签,而没有通用 content lambda。虽然每个槽都支持通用的可组合内容,但该组件会自行判断这些内部内容的布局方式。它会在内部处理内边距、对齐方式和大小。

Scaffold

Compose 提供了便捷的布局,用于将 Material 组件组合成常见的屏幕图案。可组合项(例如 Scaffold)提供了适用于各种组件和其他屏幕元素的槽。

屏幕内容

Scaffold 有一个通用 content 尾随 lambda 槽。lambda 会收到应该应用于内容根目录(例如,通过 Modifier.padding)的 PaddingValues 实例,以便偏移顶部栏和底部栏(如果存在的话)。

Scaffold(/* ... */) { contentPadding ->
    // Screen content
    Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}

应用栏

Scaffold顶部应用栏底部应用栏提供了槽。系统将在内部处理可组合项的放置位置。

您可以使用 topBar 槽和 TopAppBar

Scaffold(
    topBar = {
        TopAppBar { /* Top app bar content */ }
    }
) {
    // Screen content
}

您可以使用 bottomBar 槽和 BottomAppBar

Scaffold(
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

这些槽可用于 BottomNavigation 等其他 Material 组件。 此外,您还可以使用自定义可组合项 - 例如,查看 Owl 示例中的初始配置屏幕

悬浮操作按钮

Scaffold悬浮操作按钮提供了槽。

您可以使用 floatingActionButton 槽和 FloatingActionButton

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    }
) {
    // Screen content
}

系统将在内部处理 FAB 可组合项的底部放置位置。您可以使用 floatingActionButtonPosition 参数来调整水平位置:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to FabPosition.End
    floatingActionButtonPosition = FabPosition.Center
) {
    // Screen content
}

如果您使用的是 Scaffold 可组合项的 bottomBar 槽,则可以使用 isFloatingActionButtonDocked 参数将 FAB 与底部应用栏重叠:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    // Defaults to false
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar { /* Bottom app bar content */ }
    }
) {
    // Screen content
}

图 3. 使用 floatingActionButton 槽和 bottomBar 槽的 ScaffoldisFloatingActionButtonDocked 参数设为 false(顶部)和 true(底部)。

BottomAppBar 支持带有 cutoutShape 参数的 FAB 刘海屏,它接受任何 Shape。最好提供停靠组件所使用的同一 Shape。例如,FloatingActionButton 使用 MaterialTheme.shapes.small,并将 50% 的边角大小作为其 shape 参数的默认值:

Scaffold(
    floatingActionButton = {
        FloatingActionButton(onClick = { /* ... */ }) {
            /* FAB content */
        }
    },
    isFloatingActionButtonDocked = true,
    bottomBar = {
        BottomAppBar(
            // Defaults to null, that is, No cutout
            cutoutShape = MaterialTheme.shapes.small.copy(
                CornerSize(percent = 50)
            )
        ) {
            /* Bottom app bar content */
        }
    }
) {
  // Screen content
}

图 4. 具有 BottomAppBar 和停靠 FloatingActionButtonScaffoldBottomAppBar 的自定义 cutoutShapeFloatingActionButton 所使用的 Shape 一致。

信息提示控件

Scaffold 提供了一种显示信息提示控件的方式。

这是通过 ScaffoldState 提供的,其中包含一个 SnackbarHostState 属性。您可以使用 rememberScaffoldState 创建一个 ScaffoldState 实例,并通过 scaffoldState 参数将其传递给 ScaffoldSnackbarHostState 可提供对 showSnackbar 函数的访问权限。该挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并可被调用以响应界面事件,从而在 Scaffold 中显示 Snackbar

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    scaffoldState.snackbarHostState
                        .showSnackbar("Snackbar")
                }
            }
        )
    }
) {
    // Screen content
}

您可以提供可选操作,并调整 Snackbar 的时长。snackbarHostState.showSnackbar 函数可接受额外的 actionLabelduration 参数,并返回 SnackbarResult

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Show snackbar") },
            onClick = {
                scope.launch {
                    val result = scaffoldState.snackbarHostState
                        .showSnackbar(
                            message = "Snackbar",
                            actionLabel = "Action",
                            // Defaults to SnackbarDuration.Short
                            duration = SnackbarDuration.Indefinite
                        )
                    when (result) {
                        SnackbarResult.ActionPerformed -> {
                            /* Handle snackbar action performed */
                        }
                        SnackbarResult.Dismissed -> {
                            /* Handle snackbar dismissed */
                        }
                    }
                }
            }
        )
    }
) {
    // Screen content
}

您可以使用 snackbarHost 参数提供自定义 Snackbar。如需了解详情,请参阅 SnackbarHost API reference docs

抽屉式导航栏

Scaffold模态抽屉式导航栏提供了槽。系统将在内部处理可组合项的可拖动动作条和布局。

您可以使用 drawerContent 槽,该槽使用 ColumnScope 将抽屉式导航栏内容可组合项的布局设为列:

Scaffold(
    drawerContent = {
        Text("Drawer title", modifier = Modifier.padding(16.dp))
        Divider()
        // Drawer items
    }
) {
    // Screen content
}

Scaffold 接受一些额外的抽屉式导航栏参数。例如,您可以使用 drawerGesturesEnabled 参数来切换抽屉式导航栏是否响应拖动:

Scaffold(
    drawerContent = {
        // Drawer content
    },
    // Defaults to true
    drawerGesturesEnabled = false
) {
    // Screen content
}

您可以通过 ScaffoldState 完成以编程方式打开和关闭抽屉式导航栏的操作,其中包含一个 DrawerState 属性,该属性应使用 scaffoldState 参数传递给 ScaffoldDrawerState 可提供对 openclose 函数的访问权限,以及对与当前抽屉式导航栏状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
    scaffoldState = scaffoldState,
    drawerContent = {
        // Drawer content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Open or close drawer") },
            onClick = {
                scope.launch {
                    scaffoldState.drawerState.apply {
                        if (isClosed) open() else close()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

如果您想实现不含 Scaffold 的模态抽屉式导航栏,可以使用 ModalDrawer 可组合项。它接受与 Scaffold 类似的抽屉式导航栏参数。

val drawerState = rememberDrawerState(DrawerValue.Closed)
ModalDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

如果您要实现底部抽屉式导航栏,可以使用 BottomDrawer 可组合项:

val drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
BottomDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Drawer content
    }
) {
    // Screen content
}

底部动作条

如果您要实现标准底部动作条,可以使用 BottomSheetScaffold 可组合项。它接受与 Scaffold 类似的参数,例如 topBarfloatingActionButtonsnackbarHost。其中包含额外的参数,这些参数可提供底部动作条的显示方式。

您可以使用 sheetContent 槽,该槽使用 ColumnScope 将动作条内容可组合项的布局设为列:

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

BottomSheetScaffold 接受一些额外的动作条参数。例如,您可以使用 sheetPeekHeight 参数设置动作条的窥视高度。此外,您还可以使用 sheetGesturesEnabled 参数来切换抽屉式导航栏是否响应拖动。

BottomSheetScaffold(
    sheetContent = {
        // Sheet content
    },
    // Defaults to BottomSheetScaffoldDefaults.SheetPeekHeight
    sheetPeekHeight = 128.dp,
    // Defaults to true
    sheetGesturesEnabled = false

) {
    // Screen content
}

您可以通过 BottomSheetScaffoldState 完成以编程方式展开和收起动作条的操作,其中包含一个 BottomSheetState 属性。您可以使用 rememberBottomSheetScaffoldState 创建一个 BottomSheetScaffoldState 实例,并通过 scaffoldState 参数将其传递给 BottomSheetScaffoldBottomSheetState 可提供对 expandcollapse 函数的访问权限,以及对与当前动作条状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

val scaffoldState = rememberBottomSheetScaffoldState()
val scope = rememberCoroutineScope()
BottomSheetScaffold(
    scaffoldState = scaffoldState,
    sheetContent = {
        // Sheet content
    },
    floatingActionButton = {
        ExtendedFloatingActionButton(
            text = { Text("Expand or collapse sheet") },
            onClick = {
                scope.launch {
                    scaffoldState.bottomSheetState.apply {
                        if (isCollapsed) expand() else collapse()
                    }
                }
            }
        )
    }
) {
    // Screen content
}

如果您要实现模态底部动作条,可以使用 ModalBottomSheetLayout 可组合项:

val sheetState = rememberModalBottomSheetState(
    ModalBottomSheetValue.Hidden
)
ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
        // Sheet content
    }
) {
    // Screen content
}

背景幕

如果您要实现背景幕,可以使用 BackdropScaffold 可组合项。

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)

BackdropScaffold 接受一些额外的背景幕参数。例如,您可以使用 peekHeightheaderHeight 参数来设置后层的窥视高度和前层的最小非活动高度。此外,您还可以使用 gesturesEnabled 参数来切换背景幕是否响应拖动。

BackdropScaffold(
    appBar = {
        // Top app bar
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    },
    // Defaults to BackdropScaffoldDefaults.PeekHeight
    peekHeight = 40.dp,
    // Defaults to BackdropScaffoldDefaults.HeaderHeight
    headerHeight = 60.dp,
    // Defaults to true
    gesturesEnabled = false
)

您可以通过 BackdropScaffoldState 完成以编程方式显示和隐藏背景幕的操作。您可以使用 rememberBackdropScaffoldState 创建一个 BackdropScaffoldState 实例,并通过 scaffoldState 参数将其传递给 BackdropScaffoldBackdropScaffoldState 可提供对 revealconceal 函数的访问权限,以及对与当前背景幕状态相关的属性的访问权限。这些挂起函数需要 CoroutineScope(例如,使用 rememberCoroutineScope),并且可被调用以响应界面事件。

val scaffoldState = rememberBackdropScaffoldState(
    BackdropValue.Concealed
)
val scope = rememberCoroutineScope()
BackdropScaffold(
    scaffoldState = scaffoldState,
    appBar = {
        TopAppBar(
            title = { Text("Backdrop") },
            navigationIcon = {
                if (scaffoldState.isConcealed) {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.reveal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Menu,
                            contentDescription = "Menu"
                        )
                    }
                } else {
                    IconButton(
                        onClick = {
                            scope.launch { scaffoldState.conceal() }
                        }
                    ) {
                        Icon(
                            Icons.Default.Close,
                            contentDescription = "Close"
                        )
                    }
                }
            },
            elevation = 0.dp,
            backgroundColor = Color.Transparent
        )
    },
    backLayerContent = {
        // Back layer content
    },
    frontLayerContent = {
        // Front layer content
    }
)