文章目录
一、布局基础知识
1.1 Row()、Column()、Box()
可组合函数是 Compose 的基本构建块。可组合函数是一种发出 Unit 的函数,用于描述界面中的某一部分。
@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}
Row() 是水平方向依次排列,Column() 是 垂直方向依次排列吗,Box() 是叠放。
Box() 的代码示例如下:
@Composable
fun ArtistAvatar(artist: Artist) {
Box {
Image(/*...*/)
Icon(/*...*/)
}
}
可以在 Row() 等函数的 ()
内设置参数,代码如下:
@Composable
fun ArtistCard(artist: Artist) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
Image(/*...*/)
Column { /*...*/ }
}
}
效果如下:
1.2 布局模型的测量
在布局模型中,通过单次传递即可完成界面树布局。
首先,系统会要求每个节点对自身进行测量,然后以递归方式完成所有子节点的测量,并将尺寸约束条件沿着树向下传递给子节点。再后,确定叶节点的尺寸和放置位置,并将经过解析的尺寸和放置指令沿着树向上回传。
简而言之,父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整。
以下文代码为例:
@Composable
fun SearchResult(...) {
Row(...) {
Image(...)
Column(...) {
Text(...)
Text(..)
}
}
}
其界面数为如下:
SearchResult
Row
Image
Column
Text
Text
在 SearchResult 示例中,界面树布局遵循以下顺序:
- 系统要求根节点 Row 对自身进行测量。
- 根节点 Row 要求其第一个子节点(即 Image)进行测量。
- Image 是一个叶节点(也就是说,它没有子节点),因此该节点会报告尺寸并返回放置指令。
- 根节点 Row 要求其第二个子节点(即 Column)进行测量。
- 节点 Column 要求其第一个子节点 Text 进行测量。
- 由于第一个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令。
- 节点 Column 要求其第二个子节点 Text 进行测量。
- 由于第二个节点 Text 是叶节点,因此该节点会报告尺寸并返回放置指令。
- 现在,节点 Column 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了。
- 现在,根节点 Row 已测量其子节点,并已确定其子节点的尺寸和放置位置,接下来它可以确定自己的尺寸和放置位置了。
1.3 基于槽位的布局
Compose 提供了大量基于 Material Design 的可组合项以及 androidx.compose.material:material 依赖项(在 Android Studio 中创建 Compose 项目时提供),旨在简化界面的构建。诸如 Drawer、FloatingActionButton 和 TopAppBar 之类的元素都有提供。
Material 组件大量使用槽位 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。这种方法使组件变得更加灵活,因为它们接受可以自行配置的子元素,而不必公开子元素的每个配置参数。槽位会在界面中留出空白区域,让开发者按照自己的意愿来填充。例如,下面是您可以在 TopAppBar 中自定义的槽位:
例如,Scaffold 可让您实现具有基本 Material Design 布局结构的界面。Scaffold 可以为最常见的顶级 Material 组件(如 TopAppBar、BottomAppBar、FloatingActionButton 和 Drawer)提供槽位。通过使用 Scaffold,可轻松确保这些组件得到适当放置且正确地协同工作。
@Composable
fun HomeScreen(/*...*/) {
Scaffold(
drawerContent = { /*...*/ },
topBar = { /*...*/ },
content = { /*...*/ }
)
}
二、Material 组件和布局
Jetpack Compose 提供了 Material Design 的实现,包括如下:
Material 组件会使用应用中 MaterialTheme 提供的值:
@Composable
fun MyApp() {
MaterialTheme {
// Material Components like Button, Card, Switch, etc.
}
}
2.1 内部槽
支持内部内容(文本标签、图标等)的 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")
}
2.2 Scaffold
Compose 提供了便捷的布局,用于将 Material 组件组合成常见的屏幕图案。可组合项(例如 Scaffold)提供了适用于各种组件和其他屏幕元素的槽。
2.2.1 屏幕内容
Scaffold 有一个通用 content 尾随 lambda 槽。lambda 会收到应该应用于内容根目录(例如,通过 Modifier.padding)的 PaddingValues 实例,以便偏移顶部栏和底部栏(如果存在的话)。
Scaffold(/* ... */) { contentPadding ->
// Screen content
Box(modifier = Modifier.padding(contentPadding)) { /* ... */ }
}
2.2.2 应用栏
Scaffold 为顶部应用栏或底部应用栏提供了槽。系统将在内部处理可组合项的放置位置。
可以使用 topBar 槽和 TopAppBar,代码如下:
Scaffold(
topBar = {
TopAppBar { /* Top app bar content */ }
}
) {
// Screen content
}
可以使用 bottomBar 槽和 BottomAppBar,代码如下:
Scaffold(
bottomBar = {
BottomAppBar { /* Bottom app bar content */ }
}
) {
// Screen content
}
2.2.3 悬浮操作按钮
可以使用 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 参数将悬浮操作按钮与底部应用栏重叠:
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { /* ... */ }) {
/* FAB content */
}
},
// Defaults to false
isFloatingActionButtonDocked = true,
bottomBar = {
BottomAppBar { /* Bottom app bar content */ }
}
) {
// Screen content
}
2.2.4 信息提示控件
Scaffold 提供了一种显示信息提示控件的方式。
这是通过 ScaffoldState 提供的,其中包含一个 SnackbarHostState 属性。您可以使用 rememberScaffoldState 创建一个 ScaffoldState 实例,并通过 scaffoldState 参数将其传递给 Scaffold。SnackbarHostState 可提供对 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 函数可接受额外的 actionLabel 和 duration 参数,并返回 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
}
2.2.5 抽屉式导航栏
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
}
2.2.6 模态抽屉式导航栏
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
}
2.2.7 底部动作条
如果您要实现标准底部动作条,可以使用 BottomSheetScaffold 可组合项。它接受与 Scaffold 类似的参数,例如 topBar、floatingActionButton 和 snackbarHost。其中包含额外的参数,这些参数可提供底部动作条的显示方式。
可以使用 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
}
三、自定义布局
每个界面元素都有一个父元素,还可能有多个子元素。此外,每个元素在其父元素中都有一个位置,指定为 (x, y) 位置;也都有一个尺寸,指定为 width 和 height。
父元素定义其子元素的约束条件。元素需要在这些约束条件内定义尺寸。约束条件可限制元素的最小和最大 width 和 height。如果某个元素有子元素,它可能会测量每个子元素,以帮助确定其尺寸。
在界面树中布置每个节点的过程分为三个步骤。每个节点必须:
- 测量所有子项
- 确定自己的尺寸
- 放置其子项
3.1 使用布局修饰符
可以使用 layout 修饰符来修改元素的测量和布局方式。Layout 是一个 lambda;它的参数包括您可以测量的元素(以 measurable 的形式传递)以及该可组合项的传入约束条件(以 constraints 的形式传递)。自定义布局修饰符可能如下所示:
fun Modifier.customLayoutModifier(...) =
this.layout { measurable, constraints ->
...
})
假设我们在屏幕上显示 Text,并控制从顶部到第一行文本的基线的距离。这正是 paddingFromBaseline 修饰符的作用,我们在这里将其作为一个示例来实现。为此,请使用 layout 修饰符将可组合项手动放置在屏幕上。Text 上内边距设为 24.dp 时的预期行为如下:
生成该间距的代码如下:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a first baseline
check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
val firstBaseline = placeable[FirstBaseline]
// Height of the composable with padding - first baseline
val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
val height = placeable.height + placeableY
layout(placeable.width, height) {
// Where the composable gets placed
placeable.placeRelative(0, placeableY)
}
}
下面对该代码中发生的过程进行了说明:
- 在 measurable lambda 参数中,您需要通过调用 measurable.measure(constraints) 来测量以可测量参数表示的 Text。
- 需要通过调用 layout(width, height) 方法指定可组合项的尺寸,该方法还会提供一个用于放置被封装元素的 lambda。在本例中,它是最后一条基线和增加的上内边距之间的高度。
- 通过调用 placeable.place(x, y) 将被封装的元素放到屏幕上。如果未放置被封装的元素,它们将不可见。y 位置对应于上内边距,即文本的第一条基线的位置。
如需验证这段代码是否可以发挥预期的作用,请在 Text 上使用以下修饰符:
@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
}
}
@Preview
@Composable
fun TextWithNormalPaddingPreview() {
MyApplicationTheme {
Text("Hi there!", Modifier.padding(top = 32.dp))
}
}
3.2 创建自定义布局
layout 修饰符仅更改调用可组合项。如需测量和布置多个可组合项,请改用 Layout 可组合项。此可组合项允许您手动测量和布置子项。Column 和 Row 等所有较高级别的布局都使用 Layout 可组合项构建而成。
在 View 系统中,创建自定义布局必须扩展 ViewGroup 并实现测量和布局函数。在 Compose 中,您只需使用 Layout 可组合项编写一个函数即可。
我们来构建一个非常基本的 Column。大多数自定义布局都遵循以下模式:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
measurables 与 layout 修饰符类似,是需要测量的子项的列表,而 constraints 是来自父项的约束条件。按照与前面相同的逻辑,可按如下方式实现 MyBasicColumn:
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
该自定义可组合项的使用方式如下:
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
MyBasicColumn(modifier.padding(8.dp)) {
Text("MyBasicColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
3.3 布局方向
可以通过更改 LocalLayoutDirection 来更改可组合项的布局方向。
如果要将可组合项手动放置在屏幕上,则 LayoutDirection 是 layout 修饰符或 Layout 可组合项的 LayoutScope 的一部分。
使用 layoutDirection 时,应使用 place 放置可组合项。与 placeRelative 方法不同,place 不会根据布局方向(从左到右与从右到左)发生变化。