Jetpack Compose 可让您更轻松地设计和构建应用的界面。本文档介绍了 Compose 为了帮助您布置界面元素而提供的一些构建块,并向您展示了如何在需要时构建更专业的布局。
可组合函数是 Compose 的基本构建块。可组合函数是用于描述界面中某一部分的函数。该函数接受一些输入并生成屏幕上显示的内容。如需详细了解可组合项,请参阅 Compose 构思模型文档。
一个可组合函数可能会发出多个界面元素。不过,如果您未提供有关如何排列这些元素的指导,Compose 可能会以您不喜欢的方式排列它们。例如,以下代码会生成两个文本元素:
@Composable
fun ArtistCard() {
Text("Alfred Sisley")
Text("3 minutes ago")
}
如果您未提供有关如何排列这两个文本元素的指导,Compose 会将它们堆叠在一起,使其无法阅读:
Compose 提供了一系列现成可用的布局来帮助您排列界面元素,并且可让您轻松地定义您自己的更专业的布局。
标准布局组件
在许多情况下,您只需使用 Compose 的标准布局元素即可。
使用 Column
可将多个项目垂直地放置在屏幕上。
@Composable
fun ArtistCard() {
Column {
Text("Alfred Sisley")
Text("3 minutes ago")
}
}
同样,使用 Row
可将多个项目水平地放置在屏幕上。Column
和 Row
都支持配置它们所含元素的重心。
@Composable
fun ArtistCard(artist: Artist) {
Row(verticalAlignment = Alignment.CenterVertically) {
Image( /*...*/ )
Column {
Text(artist.name)
Text(artist.lastSeenOnline)
}
}
}
使用 Stack
可将一个元素放在另一个元素上。
通常,您只需要这些构建块。您可以编写自己的可组合函数,将这些布局组合成更精美的布局,让其适合您的应用。
上述每个基本布局都定义了自己的重心设置,从而指定了应如何排列相应的元素。如需配置这些元素,请使用修饰符。
修饰符
借助修饰符,您可以略微调整可组合项的呈现方式。您可以使用修饰符来执行以下操作:
- 更改可组合项的行为和外观
- 添加信息,如无障碍标签
- 处理用户输入
- 添加高级交互,如使元素可点击、可滚动、可拖动或可缩放
修饰符是标准的 Kotlin 对象。您可以通过调用某个 Modifier
类函数来创建修饰符。您可以将以下函数连在一起以将其组合起来:
@Composable
fun ArtistCard(
artist: Artist,
onClick: () -> Unit
) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
Row(verticalAlignment = Alignment.CenterVertically) { /*...*/ }
Spacer(Modifier.preferredSize(padding))
Card(elevation = 4.dp) { /*...*/ }
}
}
在上面的代码中,请注意,结合使用了不同的修饰符函数。
clickable()
使可组合项响应用户输入。padding()
在元素周围留出空间。fillMaxWidth()
使可组合项填充其父项给它的最大宽度。preferredSize()
指定元素的首选宽度和高度。
修饰符函数的顺序非常重要。由于每个函数都会对上一个函数返回的 Modifier
进行更改,因此顺序会影响最终结果。让我们来看看这方面的一个示例:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.clickable(onClick = onClick)
.padding(padding)
.fillMaxWidth()
) {
// rest of the implementation
}
}
在上面的代码中,整个区域(包括周围的内边距)都是可点击的,因为 padding
修饰符应用在 clickable
修饰符后面。如果这两个修饰符的应用顺序反过来,则由 padding
添加的空间不会响应用户输入:
@Composable
fun ArtistCard(/*...*/) {
val padding = 16.dp
Column(
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
) {
// rest of the implementation
}
}
可滚动布局
使用 ScrollableRow
或 ScrollableColumn
可使 Row
或 Column
内的元素滚动。
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
ScrollableColumn(Modifier.fillMaxSize()) {
feedItems.forEach {
ArtistCard(it, onSelected)
}
}
}
如果要显示的元素很少,这种方法效果很好,但对于大型数据集,很快就会出现性能问题。如需仅显示屏幕上可见的部分元素,请使用 LazyColumnFor
或 LazyRowFor
。
@Composable
fun Feed(
feedItems: List<Artist>,
onSelected: (Artist) -> Unit
) {
Surface(Modifier.fillMaxSize()) {
LazyColumnFor(feedItems) { item ->
ArtistCard(item, onSelected)
}
}
}
内置 Material 组件
Compose 最高级别的界面抽象语言是 Material Design。Compose 提供了大量开箱即用的可组合项来简化界面的构建。诸如 Drawer
、FloatingActionButton
和 TopAppBar
之类的元素都有提供。
Material 组件大量使用插槽 API,这是 Compose 引入的一种模式,它在可组合项之上带来一层自定义设置。插槽会在界面中留出空白区域,让开发者按照自己的意愿来填充。例如,下面是您可以在 TopAppBar
中自定义的插槽:
可组合项通常采用 content
可组合的 lambda (content: @Composable
() -> Unit
)。插槽 API 会针对特定用途提供多个 content
参数。例如,TopAppBar
可让您为 title
、navigationIcon
和 actions
提供内容。
Material 支持的最高级别的可组合项是 Scaffold
。Scaffold
可让您实现具有基本 Material Design 布局结构的界面。Scaffold
可以为最常见的顶级 Material 组件(如 TopAppBar
、BottomAppBar
、FloatingActionButton
和 Drawer
)提供插槽。通过使用 Scaffold
,很容易确保这些组件得到适当放置且正确地协同工作。
@Composable
fun HomeScreen( /*...*/ ) {
Scaffold (
drawerContent = { /*...*/ },
topBar = { /*...*/ },
bodyContent = { /*...*/ }
)
}
ConstraintLayout
ConstraintLayout
有助于根据可组合项的相对位置将它们放置在屏幕上,它是使用多个 Row
、Column
和 Stack
元素的替代方案。在实现对齐要求比较复杂的较大布局时,ConstraintLayout
很有用。
Compose 中的 ConstraintLayout
支持 DSL:
- 引用是使用
createRefs()
或createRefFor()
创建的,ConstraintLayout
中的每个可组合项都需要有与之关联的引用。 - 约束条件是使用
constrainAs()
修饰符提供的,该修饰符将引用作为参数,可让您在主体 lambda 中指定其约束条件。 - 约束条件是使用
linkTo()
或其他有用的方法指定的。 parent
是一个现有的引用,可用于指定对ConstraintLayout
可组合项本身的约束条件。
下面是使用 ConstraintLayout
的可组合项的示例:
@Composable
fun ConstraintLayoutContent() {
ConstraintLayout {
// Create references for the composables to constrain
val (button, text) = createRefs()
Button(
onClick = { /* Do something */ },
// Assign reference "button" to the Button composable
// and constrain it to the top of the ConstraintLayout
modifier = Modifier.constrainAs(button) {
top.linkTo(parent.top, margin = 16.dp)
}
) {
Text("Button")
}
// Assign reference "text" to the Text composable
// and constrain it to the bottom of the Button composable
Text("Text", Modifier.constrainAs(text) {
top.linkTo(button.bottom, margin = 16.dp)
})
}
}
此代码使用 16.dp
的外边距来约束 Button
顶部到父项的距离,同样使用 16.dp
的外边距来约束 Text
到 Button
底部的距离。
如需查看有关如何使用 ConstraintLayout
的更多示例,请参考布局 Codelab。
Decoupled API
在 ConstraintLayout
示例中,约束条件是在应用它们的可组合项中使用修饰符以内嵌方式指定的。不过,在某些情况下,最好将约束条件与应用它们的布局分离开来。例如,您可能希望根据屏幕配置来更改约束条件,或在两个约束条件集之间添加动画效果。
对于此类情况,您可以通过不同的方式使用 ConstraintLayout
:
- 将
ConstraintSet
作为参数传递给ConstraintLayout
。 - 使用
tag
修饰符将在ConstraintSet
中创建的引用分配给可组合项。
@Composable
fun DecoupledConstraintLayout() {
WithConstraints {
val constraints = if (minWidth < 600.dp) {
decoupledConstraints(margin = 16.dp) // Portrait constraints
} else {
decoupledConstraints(margin = 32.dp) // Landscape constraints
}
ConstraintLayout(constraints) {
Button(
onClick = { /* Do something */ },
modifier = Modifier.layoutId("button")
) {
Text("Button")
}
Text("Text", Modifier.layoutId("text"))
}
}
}
private fun decoupledConstraints(margin: Dp): ConstraintSet {
return ConstraintSet {
val button = createRefFor("button")
val text = createRefFor("text")
constrain(button) {
top.linkTo(parent.top, margin = margin)
}
constrain(text) {
top.linkTo(button.bottom, margin)
}
}
}
然后,当您需要更改约束条件时,只需传递不同的 ConstraintSet
即可。
自定义布局
某些可组合函数在被调用后会发出一部分界面,这部分界面随后会被添加到呈现在屏幕上的界面树中。每个界面元素都有一个父元素,还可能有多个子元素。此外,每个元素在其父元素中都有一个位置,指定为 (x, y) 位置;也都有一个大小,指定为 width
和 height
。
要求元素各自定义应满足的约束条件。约束条件可限制元素的最小和最大 width
和 height
。如果某个元素有子元素,父元素可能会测量每个子元素,以帮助确定父元素的大小。一旦某个元素报告了它自己的大小,就有机会相对于自身放置它的子元素,如创建自定义布局中所详述。
单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。如果某个布局元素测量了它的子元素两次,而该子元素又测量了它的一个子元素两次,依此类推,那么一次尝试布置整个界面就不得不做大量的工作,这使得很难让应用保持良好的性能。不过,有时除了子元素的单遍测量告知您的信息之外,您确实还需要一些额外的信息。有一些方法可以有效地处理这样的情况,这些方法在使用布局修饰符部分进行了介绍。
使用布局修饰符
您可以使用 layout
修饰符来修改可组合项的测量和布局方式。Layout
是一个 lambda;它的参数包括您可以测量的可组合项(以 measurable
的形式传递)以及该可组合项传递的约束条件(以 constraints
的形式传递)。大多数自定义 layout
修饰符都遵循以下模式:
fun Modifier.customLayoutModifier(...) =
Modifier.layout { measurable, constraints ->
...
})
假设我们在屏幕上显示 Text
,并控制从顶部到第一行文本基线的距离。为此,使用 layout
修饰符将可组合项手动放置在屏幕上。Text
上内边距设为 24.dp
时的预期行为如下:
生成该间距的代码如下:
fun Modifier.firstBaselineToTop(
firstBaselineToTop: Dp
) = Modifier.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.toIntPx() - 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.placeRelative(x, y)
将子项放置在屏幕上。如果未放置子项,它们将不可见。y
位置对应于上内边距,即文本的第一条基线的位置。placeRelative
在从右到左的上下文中自动镜像位置。
如需验证这段代码是否可以发挥预期的作用,请在 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))
}
}
创建自定义布局
layout
修饰符仅更改一个可组合项。如需手动控制多个可组合项,请改用 Layout
可组合项。此可组合项允许您手动测量和布置子项。Column
和 Row
等所有较高级别的布局都使用 Layout
可组合项构建而成。
下面我们来构建简单的 Column
实现。大多数自定义布局都遵循以下模式:
@Composable
fun MyOwnColumn(
modifier: Modifier = Modifier,
content: @Composable() () -> Unit
) {
Layout(
modifier = modifier,
children = content
) { measurables, constraints ->
// measure and position children given constraints logic here
}
}
与 layout
修饰符类似,measurables
是需要测量的子项的列表,而 constraints
是传递给 Layout
的约束条件。按照与前面相同的逻辑,可按如下方式实现 MyOwnColumn
:
@Composable
fun MyOwnColumn(
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
}
}
}
}
可组合子项受 Layout
约束条件的约束,它们的放置基于前一个可组合项的 yPosition
。
该自定义可组合项的使用方式如下:
@Composable
fun CallingComposable(modifier: Modifier = Modifier) {
MyOwnColumn(modifier.padding(8.dp)) {
Text("MyOwnColumn")
Text("places items")
Text("vertically.")
Text("We've done it by hand!")
}
}
布局方向
您可以使用 LayoutDirection
上下文环境来更改可组合项的布局方向。
如果您要将可组合项手动放置在屏幕上,则 LayoutDirection
是 layout
修饰符或 Layout
可组合项的 LayoutScope
的一部分。
使用 layoutDirection
时,应使用 place
放置可组合项。与 placeRelative
方法不同,place
不会根据阅读方向(从左到右而不是从右到左)而发生变化。
了解详情
如需了解详情,请参阅 Jetpack Compose 中的布局 Codelab。