【Android-JetpackCompose】7、布局、Material 组件和布局、自定义布局

一、布局基础知识

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 不会根据布局方向(从左到右与从右到左)发生变化。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值