Android compose 自定义layout (翻译)

Create your custom layout

创建您的自定义布局

Compose promotes reusability of composables as small chunks that can be enough for some custom layouts by combining built-in composables such as Column, Row, or Box.

However, you might need to build something unique to your app that requires measuring and laying out children manually. For that, you can use the Layout composable. In fact all higher level layouts like Column and Row are built with this.

Note: In the View system, creating a custom layout required extending ViewGroup and implementing measure and layout functions. In Compose you simply write a function using the Layout composable.

通常来说,通过诸如Column, Row, 或 Box 之类的内置compose来自定义某些布局来说已经足够了。
但是,您可能需要构建一些独特的应用程序,需要手动测量和布置子项。为此,您可以使用 Layout。事实上,所有更高级别的布局都喜欢用ColumnRow来构建。

Note:在视图系统中,创建自定义布局需要扩展ViewGroup和实现measure和layout函数。在 Compose 中,您只需使用Layout composable函数。

Before diving into how to create custom layouts, we need to know more about the principles of Layouts in Compose.

在深入研究如何创建自定义布局之前,我们需要更多地了解 Compose 中布局的原理。

Principles of layouts in Compose

Some composable functions emit a piece of UI when invoked that is added to a UI tree that will get rendered on the screen. Each emission (or element) has one parent and potentially many children. Also, it has a location within its parent: an (x, y) position, and a size: a width and height.

Compose 中的布局原则

一些可组合函数在调用时会发出一段 UI,被添加到UI树中,呈现在屏幕上。每个发射物(或元素)都有一个父级和潜在的许多子级。此外,它在其父级中有位置-> (x, y) 坐标,和大小->width、height。

Elements are asked to measure themselves with Constraints that should be satisfied. Constraints restrict the minimum and maximum width and height of an element. If an element has child elements it may measure each of them to help determine its own size. Once an element reports its own size, it has an opportunity to place its child elements relative to itself. This will be further explained when creating the custom layout.

元素需要用Constraints来准确的测量自己。Constraints限制元素的最小和最大width和height。如果一个元素有子元素,它可以测量它们中的每一个来帮助确定它自己的大小。一旦一个元素上报了它自己的大小,它就有机会相对于自身放置它的子元素。这将在创建自定义布局时进一步解释。

Compose UI does not permit multi-pass measurement. This means that a layout element may not measure any of its children more than once in order to try different measurement configurations. Single-pass measurement is good for performance, allowing Compose to handle efficiently deep UI trees. If a layout element measured its child twice and that child measured one of its children twice and so on, a single attempt to lay out a whole UI would have to do a lot of work, making it hard to keep your app performing well. However, there are times when you really need additional information on top of what a single child measurement would tell you - for these cases there are ways of doing this, we will talk about them later.

Compose UI 不允许多次测量。这意味着布局元素可能不会多次测量其任何子元素,以便尝试不同的测量架构。单遍测量有利于提高性能,使 Compose 能够有效地处理深层 UI 树。如果一个布局元素测量了它的子元素两次,而该子元素测量了它的一个子元素两次,依此类推,一次尝试布置整个 UI 将不得不做很多工作,这使得你的应用程序很难保持良好的性能。但是,有时您确实需要除了单个子项测量结果之外的其他信息 - 对于这些情况,有一些方法可以做到这一点,我们将在稍后讨论。

Using the layout modifier

Use the layout modifier to manually control how to measure and position an element. Usually, the common structure of a custom layout modifier is as follows:

使用布局修饰符

使用layout修改器手动控制如何测量和定位元素。通常,自定义layout修饰符的常见结构如下:

fun Modifier.customLayoutModifier(...) = Modifier.layout { measurable, constraints ->
  ...
})

When using the layout modifier, you get two lambda parameters:

measurable: child to be measured and placed
constraints: minimum and maximum for the width and height of the child
Let's say you want to display a Text on the screen and control the distance from the top to the baseline of the first line of texts. In order to do that, you'd need to manually place the composable on the screen using the layout modifier. See the desired behavior in the next picture where the distance from top to first baseline is 24.dp:

使用layout修饰符时,您将获得两个 lambda 参数:

measurable:要测量和放置的孩子
constraints: 孩子的宽度和高度的最小值和最大值
假设您想在屏幕上显示 一个文本并控制从第一行文本的顶部到基线的距离。为了做到这一点,您需要使用layout修饰符手动将可组合项放置在屏幕上。在下一张图片中查看所需的行为,其中从顶部到第一个基线的距离为24.dp:



Let's create a firstBaselineToTop modifier first:
让我们先创建一个firstBaselineToTop修饰符:

fun Modifier.firstBaselineToTop(
  firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        ...
    }
)

The first thing to do is measure the composable. As we mentioned in the Principles of Layout in Compose section, you can only measure your children once.

Measure the composable by calling measurable.measure(constraints). When calling measure(constraints), you can pass in the given constraints of the composable available in the constraints lambda parameter or create your own. The result of a measure() call on a Measurable is a Placeable that can be positioned by calling placeRelative(x, y), as we will do later.

For this use case, don't constrain measurement further, just use the given constraints:

首先要做的是衡量可组合。正如我们在 Compose 中的布局原则部分提到的,您只能测量您的孩子一次。

通过调用测量可组合measurable.measure(constraints)。调用measure(constraints)时,您可以传入constraints lambda 参数中constraints,也可以自己创建。Measurable 调用measure()的结果是Placeable可以通过调用placeRelative(x, y)来定位,我们稍后会这样做。

对于这个用例,不要进一步约束测量,只需使用给定的constraints:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)

        ...
    }
)

Now that the composable has been measured, you need to calculate its size and specify it by calling the layout(width, height) method which also accepts a lambda used for placing the content.

In this case, the width of our composable will be the width of the measured composable and the height will be the composable's height with the desired top-to-baseline height minus the first baseline:

既然已经测量了可组合项,您需要计算它的大小并通过调用layout(width, height)函数或者 lambda表达式来实现它。

在这种情况下,我们的可组合物的宽度width将是测量结果的宽度,高度height期望值是top-to-baseline 的高度减去first baseline:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        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) {
            ...
        }
    }
)

Now, you can position the composable on the screen by calling placeable.placeRelative(x, y). If you don't call placeRelative, the composable won't be visible. placeRelative automatically adjusts the position of the placeable based on the current layoutDirection.

Warning: When creating a custom Layout or LayoutModifier, Android Studio will give a warning until the layout function is called.

In this case, the y position of the text corresponds to the top padding minus the position of the first baseline:

现在,您可以通过调用placeable.placeRelative(x, y). 如果您不调用placeRelative,则可组合项将不可见。placeRelative根据当前 自动调整可放置物的位置layoutDirection。

警告:创建自定义Layoutor 时LayoutModifier,Android Studio 会发出警告,直到layout调用该函数。

在这种情况下,y文本的位置对应于顶部填充减去第一个基线的位置:

fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        ...

        // 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)
        }
    }
)

To verify this works as expected, you can use this modifier on a Text as you saw in the picture above:

要验证这是否按预期工作,您可以在 文本上使用此修饰符如上图所示:

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.firstBaselineToTop(32.dp))
  }
}

@Preview
@Composable
fun TextWithNormalPaddingPreview() {
  LayoutsCodelabTheme {
    Text("Hi there!", Modifier.padding(top = 32.dp))
  }
}

With preview:


Using the Layout composable

Instead of controlling how a single composable gets measured and laid out on the screen, you might have the same necessity for a group of composables. For that, you can use the Layout composable to manually control how to measure and position the layout's children. Usually, the common structure of a composable that uses Layout is as follows:

使用布局可组合

与控制单个可组合项如何在屏幕上测量和布局不同,您可能对一组可组合项具有相同的必要性。为此,您可以使用 Layout可组合来手动控制如何测量和定位布局的子项。通常,使用的可组合的常见结构Layout如下:

@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // custom layout attributes 
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

The minimum required parameters for a CustomLayout are a modifier and content; these parameters are then passed to Layout. In the trailing lambda of Layout (of type MeasurePolicy), you get the same lambda parameters as you get with the layout modifier.

To show Layout in action, let's start implementing a very basic Column using Layout to understand the API. Later, we'll build something more complex to showcase flexibility of the Layout composable.

CustomLayout 所需的最小参数是 modifier和content; 然后将这些参数传递给Layout. 在Layout(类型MeasurePolicy)的 lambda 尾部,layout 的
modifier 与lambda表达式中的参数相同。

为了展示Layout实际效果,让我们来用Layout来实现一个非常基本的Column来理解 API。稍后,我们将构建一些更复杂的东西来展示Layout composable 的灵活性。

Implementing a basic Column

Our custom implementation of Column lays out items vertically. Also, for simplicity, our layout occupies as much space as it can in its parent.

Create a new composable called MyOwnColumn and add the common structure of a Layout composable:

实现一个基本的列

我们的自定义实现Column 垂直布局项目。此外,为简单起见,我们的布局在其父级中占据尽可能多的空间。

创建一个名为的MyOwnColumn的composable函数并添加可组合的通用结构Layout:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // measure and position children given constraints logic here
    }
}

As before, the first thing to do is measure our children that can only be measured once. Similarly to how the layout modifier works, in the measurables lambda parameter, you get all the content that you can measure by calling measurable.measure(constraints).

For this use case, you won't constrain our child views further. When measuring the children, you should also keep track of the width and the maximum height of each row to be able to place them correctly on the screen later:

和以前一样,首先要做的是测量我们只能测量一次的孩子。与布局修饰符的工作方式类似,在lambda 函数中的measurables中,您可以通过调用measurable.measure(constraints)获取所有的可测量的content

对于这个用例,您不会进一步限制我们的子视图。在测量孩子时,您还应该跟踪每行width的最大值和最大值height,以便以后能够将它们正确地放置在屏幕上:

@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 child
            measurable.measure(constraints)
        }
    }
}

Now that you have the list of measured children in our logic, before positioning them on the screen, you need to calculate the size of our version of Column. As you're making it as big as its parent, the size of it is the constraints passed in by the parent. Specify the size of our own Column by calling the layout(width, height) method, which also gives you the lambda used for placing the children:

现在您在我们的逻辑中已经有了可测量子项的列表,在将它们定位到屏幕上之前,您需要计算我们版本的Column. 当你让它和它的父级一样大时,它的大小就是父级传入的约束。Column通过调用layout(width, height)方法指定我们自己的大小,该方法还为您提供用于放置子项的 lambda:

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Measure children - code in the previous code snippet
        ...

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children
        }
    }
}

Lastly, we position our children on the screen by calling placeable.placeRelative(x, y). In order to place the children vertically, we keep track of the y coordinate we have placed children up to. The final code of MyOwnColumn looks like this:

最后,我们通过调用将我们的孩子定位在屏幕上placeable.placeRelative(x, y)。为了垂直放置子项,我们跟踪y放置子项的坐标。最终代码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 child
            measurable.measure(constraints)
        }

        // Track the y co-ord we have placed children up to
        var yPosition = 0

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 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
            }
        }
    }
}

MyOwnColumn in action

Let's see MyOwnColumn on the screen by using it in the BodyContent composable. Replace the content inside BodyContent with the following:

MyOwnColumn 的行为
让我们在BodyContent中使用MyOwnColumn来看它在屏幕上如何显示。将 BodyContent 中的内容替换为以下内容:

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

With preview:


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容