原文链接。
MAD 技能:Compose 布局 和 修饰符 第 4 集
欢迎回到 关于 Jetpack Compose 布局 和 修饰符 的 MAD 技能系列!在上一集,我们谈论了 Compose 的 Layout 阶段,以解说 修饰符链的次第 和 传入的父级捆绑 是怎样影响 它们所传递给的 可组合项的。
今日这一集,我们进一步集合 布局阶段 和 捆绑,并从另一个角度介绍它们——怎样使用它们的力气 在 Compose 中 构建 自定义布局。
为了建自定义布局,我们会介绍布局阶段 可以做 什么、怎样 进入它 以及怎样 运用其子阶段(测量和放置)来发挥您的优势,用于构建活络的自定义布局。
之后,我们将介绍两个重要的、违反规则的 Compose API:SubcomposeLayout
和 Intrinsic 测量 ,作为布局难题的终究两个缺失部分。这些概念将为您供应额外的知识,以在 Compose 中构建 具有非常详细要求的凌乱规划。
您也可以将本文作为 MAD 技能视频观看:
youtu.be/l6rAoph5UgI
Compose 的全部布局
在前几会合,我们谈论了 Compose 怎样通过其三个阶段 将数据转换为 UI:组合、布局和制造,或闪现 “什么”、放在 “哪里” 以及 “怎样” 出现它 .
但正如我们系列的称谓所暗示的那样,我们最感兴趣的是 布局阶段。
但是,Compose 中的术语 “布局(Layout)” 用于许多不同的事物,并且由于其意义众多,然后看起来或许令人困惑。到目前为止,在本系列中,我们现已学习了以下用法:
- 布局阶段:Compose 的三个阶段之一,其间父布局定义其子元素的大小和方位
- 布局:一个广泛的抽象术语,用于在 Compose 中快速定义任何 UI 元素
- 布局节点: 一个抽象概念,用作 UI 树中一个元素的可视化表示,被创建为 Compose 中的 组合 阶段的作用。
在这一会合,我们还将学习一些额外的意义来完结整个布局循环。让我们先快速分解它们——对这些术语的更深入解说,稍后将在帖子中进一步介绍:
-
Layout
可组合项: 用作 Compose UI 中心组件的可组合项。在 组合(Composition)期间调用时,它会在 Compose UI 树中创建并增加一个布局节点;全部更高等级布局的基础,如Column
、Row
等。 -
layout()
函数 – 放置的起点,这是布局阶段的第二个子进程,担任在测量的榜首个子进程之后,将子项放置在Layout
可组合项中 -
.layout()
modifier — 一种修饰符,它包裹一个布局节点,并容许独自调整它的大小和放置它,而不是由其父布局完结
现在我们知道什么(对应上文中的 “闪现 ‘什么’ ”)是什么了,让我们从 布局阶段 初步,同时 并扩大 它(布局阶段)。如前所述,在布局阶段,UI 树中的每个元素 测量其子元素(假设有),并 将它们放置 在可用的 2D 空间中。
Compose 中的每个开箱即用的布局,例如Row
、Column
等等,都会为您 自动地 处理全部这些。
但是,假设您的规划需求 非标准布局,那您需求自定义并构建自己的布局,例如来自我们的 JetLagged 示例 的TimeGraph
?
这正是您需求更多地了解布局阶段的时分——怎样 进入它(布局阶段) 以及怎样使用它的子元素 测量和放置 的子阶段,才会对您有利。那么,让我们来看看怎样依据给定的规划 在 Compose 中 构建 自定义布局!
进入布局阶段矩阵
让我们回顾一下构建自定义布局的最重要、最基本的进程。但是,假设您希望遵从 详细、分步的视频攻略,了解怎样以及何时为现实生活中凌乱的 app 规划创建自定义布局,请检查在 Compose 视频中的自定义布局和图形, 或直接从我们的 JetLagged 示例 ,探索TimeGraph
自定义布局。
调用 Layout
可组合项是布局阶段和构建自定义布局的起点:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout() { … }
}
Layout
可组合项是 Compose 中 布局 阶段的 主角,也是 Compose 布局系统的 中心组件:
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
// …
}
它接受一个可组合的content
作为其子项,并接受一个用于测量和定位其元素的 测量战略。全部更高等级的布局,例如 Column
和Row
,都在底层运用这个可组合项。
Layout
可组合项当时具有三个重载:
-
Layout
— 用于测量和放置 0 个或多个子项,它接受一个可组合项作为content
-
Layout
— 用于 UI 树的叶节点刚好有 0 个子节点,因此它没有content
参数 -
Layout
– 接受用于传递多个不同可组合项的contents
列表
一旦我们进入布局阶段,我们就会看到它由两个进程组成,测量和放置,按特定次第排列:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// 1. 测量 进程
// 承认组件的大小
layout(…) {
// 2. 放置 进程
// 承认组件的方位
}
}
)
}
子元素的 大小 在 测量进程 中核算,它们的方位在 放置进程 中核算。这些进程的次第是 通过 Kotlin DSL 作用域强制实行的,这些作用域的嵌套方法可以防止放置尚未预先测量的内容或在 测量作用域 内进行放置:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier
) {
Layout(
content = content,
modifier = modifier,
measurePolicy = { measurables, constraints ->
// 测量 作用域
// 1. 测量 进程
// 承认组件的大小
layout(…) {
// 放置 作用域
// 2. 放置 进程
// 承认组件的方位
}
}
)
}
在测量期间,布局的内容可以作为 measurables(可测量政策) 或 准备好被测量的 组件进行访问。在 布局 内部,measurables
默许以列表的方法出现:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
measurePolicy = { measurables: List<Measurable>, constraints: Constraints ->
// 测量 作用域
// 1. 测量 进程
// 承认组件的大小
}
)
}
依据自定义布局的要求,您可以选用此列表并测量具有 相同传入捆绑 的每个项目,以坚持其 原始的预定义大小:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 测量 作用域
// 1. 测量 进程
measurables.map { measurable ->
measurable.measure(constraints)
}
}
}
或许依据需求调整其测量值——通过 复制 您希望保存的捆绑并 掩盖 您希望更改的捆绑:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 测量 作用域
// 1. 测量 进程
measurables.map { measurable ->
measurable.measure(
constraints.copy(
minWidth = newWidth,
maxWidth = newWidth
)
)
}
}
}
在上一会合我们现已看到,在布局阶段,捆绑在 UI 树中 从父级传递到子级。当父节点测量其子节点时,它会向每个子节点供应这些捆绑,让他们知道 容许的最小和最大标准。
布局 阶段的一个非常重要的特征是 单遍测量。这意味着布局元素不能多次测量其任何子元素。单遍测量有利于功用,容许 Compose 有用地处理深层 UI 树。
测量一个measurables(可测量政策)
列表,将回来一个placeables(可放置政策)
列表,或许一个现在 准备好被放置的组件:
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 测量 作用域
// 1. 测量 进程
val placeables = measurables.map { measurable ->
// 回来一个 placeable(可放置政策)
measurable.measure(constraints)
}
}
}
放置进程从调用layout()
函数并进入放置作用域初步。此时,父布局将可以 决议自己的大小 (totalWidth
,totalHeight
),例如,将其子 可放置政策 的宽度和高度相加:
@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// totalWidth 可以是全部子项宽度的总和
// totalHeight 可以是全部子项高度的总和
layout(totalWidth, totalHeight) {
// 放置 作用域
// 2. 放置 进程
}
}
}
放置规模现在容许我们运用作为 测量作用 出现的全部 placeables(可放置政策)
:
@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// …
layout(totalWidth, totalHeight) {
// 放置 作用域
// 2. 放置 进程
placeables // 放置我们!
}
}
}
要初步放置子项,我们需求他们的开始 x 和 y 坐标。一旦我们定义了我们希望子项被放置的方位,我们就调用place()
来结束放置进程:
@Composable
fun CustomLayout(
// …
) {
Layout(
// …
) {
// …
layout(totalWidth, totalHeight) {
// 放置 作用域
// 2. 放置 进程
placeables.map { it.place(xPosition, yPosition) }
}
}
}
这样,我们就结束了放置进程,以及布局阶段!您的自定义布局现在可以运用和重用了。
.layout()
用于全部单个元素的修饰符
运用 Layout 可组合项创建自定义布局使您可以操作 全部子元素 并 手动控制 它们的大小和方位。但是,在某些情况下,创建自定义布局只是为了控制 一个特定元素,这是一种矫枉过正的做法,并且没必要。
在这些情况下,Compose 没有运用自定义布局,而是供应了一种更好、更简略的解决方案 — .layout()
修饰符,它容许您仅测量和布局 一个被包裹的元素。
让我们看一个示例,其间 UI 元素,被它的父级以我们不太喜爱的方法紧缩:
我们只希望这个简略 Column
中的一个Element
,可以通过为其移除周围的40.dp
的padding
,强制它拥有比父级的宽度 更大的宽度,例如,以完结边对边的外观:
@Composable
fun LayoutModifierExample() {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(40.dp)
) {
Element()
Element()
// 下面的 item 应该 “抵御” 强制的 padding,且遵从边对边(的规则)
Element()
Element()
}
}
为了让第三个元素控制自己并 移除 强制 padding,我们在其上设置了一个.layout()
修饰符。
它的作业方法与Layout
可组合项非常类似。它接受一个 lambda,该 lambda 容许您访问您正在测量的元素,(并)作为单个的 measurable(可测量政策)
,和可组合项(的)来自父级的 传入捆绑 来传递 。然后,您可以运用它来修正单个包装元素的测量和布局方法:
Modifier.layout { measurable, constraints ->
// 测量
val placeable = measurable.measure(...)
layout(placeable.width, placeable.height) {
// 放置
placeable.place(...)
}
}
回到我们的示例——然后我们在测量进程中更改此Element
的最大宽度,以增加额外的 80.dp
:
Element(modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(
constraints.copy(
// 通过将 DP 增加到传入捆绑来调整此 item 的 maxWidth
maxWidth = constraints.maxWidth + 80.dp.roundToPx()
)
)
layout(placeable.width, placeable.height) {
// 把这个 item 放在原来的方位
placeable.place(0, 0)
}
})
正如我们之前所说,Compose 的优势之一是您可以在解决问题时 挑选自己的途径,由于条条大路通罗马。假设您知道此元素所需的 切当静态大小,另一种方法或许是在其上设置 .requiredWidth()
修饰符,以便父布局中的传入捆绑 不会掩盖 其设置的宽度,而是 “尊重”它。相反,运用常规的 .width()
修饰符会使设置的宽度被父布局和测量阶段的传入捆绑掩盖。
SubcomposeLayout — 打破 Compose 各个阶段 的规则
在前面几会合,我们谈到了 Compose的各个阶段 以及它们精确排序的规则:1. 组合,2. 布局,3. 制造。布局阶段随后分解为 测量和放置 子阶段。尽管这适用于绝大多数Layout
可组合项,却有一个打破规则的布局不遵从此架构,但有一个很好的理由 —SubcomposeLayout
。
考虑以下用例——您正在构建一个包括一千个 item 的列表,而这些 item 根本 无法 同时 包容在屏幕上。在那种情况下,组合全部这些子 item 将是不必要的资源糟蹋——假设其间大部分甚至都看不到,那为什么要预先组合这么多 item 呢?
相反,更好的方法是1。丈量子项 以取得它们的大小,然后在此基础上,2。核算可用视口(可以理解为屏幕上的可见区域)中 可以包容的 item 数,终究只组合 可见 的 item。
这是SubcomposeLayout
反面的主要思维之一——它需求 首要 对部分或全部子可组合项进行 测量,然后运用该信息来 承认是否组合 部分或全部子项。
这正是 Lazy 组件 构建在SubcomposeLayout
之上的原因,这使它们可以在滚动时按需增加内容。
SubcomposeLayout
将组合阶段推延到布局阶段,因此可以推延某些子可组合项的组合或实行,直到父布局具有更多信息(例如,其子可组合项的大小)。也就是说,布局阶段的测量进程需求 先于 组合阶段进行。
BoxWithConstraints
也在底层运用 SubcomposeLayout
,但这个用例略有不同—— BoxWithConstraints
容许您 获取 父级传递的 捆绑,并在 推迟的组合阶段 运用它们,由于捆绑仅在布局阶段测量进程中已知:
BoxWithConstraints {
// maxHeight 是仅在 BoxWithConstraints 中可用的测量信息,
// 由于推迟的组合阶段发生在布局阶段测量【之后】
if (maxHeight < 300.dp) {
SmallImage()
} else {
BigImage()
}
}
(什么时分要)制止(运用)SubcompositionLayout
由于 SubcompositionLayout
改变了 Compose 各个阶段的 常规的流程 以容许动态实行,因此在功用方面存在一定的 本钱和捆绑。因此,了解何时应该运用 SubcompositionLayout
以及何时不该运用它非常重要。
了解何时或许需求 SubcomposeLayout 的一种快速的好方法是,至少 一个子可组合项的组合阶段取决于另一个子可组合项的测量作用。我们现已在 Lazy 组件和 BoxWithConstraints
中看到了有用的用例。
但是,假设您只需求 一个子项的测量值来测量其他子项,则可以运用常规 Layout
可组合项来完结。这样,您依然可以依据互相的作用别离测量 item ——您只是不能改变它们的组合。
Intrinsic 测量 — 打破单遍测量规则
我们之前提到的第二个 Compose 规则是 布局 阶段的 单遍测量,这对该进程和布局系统的整体功用有很大帮忙。想一想短时刻内或许发生的 重组数量,以及捆绑每次重组的整个 UI 树的测量,这些都将大幅前进整体速度!
为每次重组,遍历具有许多 UI 节点的树
但是,在某些用例中,父布局 需求 在测量子布局之前 了解 有关其子布局的 一些信息,以便它可以运用此信息来 定义和传递捆绑。而这正是 Intrinsic 测量的用途所在——让你 在子项被测量之前,可以事前查询子项(的相关信息)。
让我们看看下面的比方——我们希望这列Column
items 具有相同的宽度,或许更精确地说,让每个 item 的宽度与最宽的子项(在我们的比方中,指 “And Modifiers” 这个 item)的宽度相同。但是,我们也希望最宽子项按需取得尽或许多的宽度。所以我们的榜首步是:
@Composable
fun IntrinsicExample() {
Column() {
Text(text = "MAD")
Text(text = "Skills")
Text(text = "Layouts")
Text(text = "And Modifiers")
}
}
但是,我们可以看到,这还不可。每个 itme 只占用它需求的空间。我们可以测验以下方法:
@Composable
fun IntrinsicExample() {
Column() {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}
但是,这会将每个 item 和父 Column
扩展至屏幕上可用的最大宽度。请记住,我们想要全部 item 的都有 最宽 item 的宽度。所以,如你所知,我们的政策是在这里运用 Intrinsics:
@Composable
fun IntrinsicExample() {
Column(Modifier.width(IntrinsicSize.Max)) {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}
通过在父Column
上运用 IntrinsicSize.Max
,我们查询它的子项并询问 “想要恰当地闪现全部内容,所需的 最大宽度 是多少?”。由于我们正在闪现文本,并且短语 “And Modifiers” 最长,因此它将定义Column
的宽度。
一旦承认了固有大小,它就会用于 设置Column
的 大小(在本例中为宽度),然后其他子项就可以填满该宽度。
相反,假设我们运用 IntrinsicSize.Min
,问题将是“要恰当地闪现全部内容,所需的 最小宽度 是多少?” 在文本的情况下,最小固有宽度是每行有一个单词的宽度:
@Composable
fun IntrinsicExample() {
Column(Modifier.width(IntrinsicSize.Min) {
Text(text = "MAD", Modifier.fillMaxWidth())
Text(text = "Skills", Modifier.fillMaxWidth())
Text(text = "Layouts", Modifier.fillMaxWidth())
Text(text = "And Modifiers", Modifier.fillMaxWidth())
}
}
快速总结全部可用的 intrinsic 组合:
-
Modifier.width(IntrinsicSize.Min)
— “要正确闪现内容,所需的最小宽度是多少?” -
Modifier.width(IntrinsicSize.Max)
— “要正确闪现内容,所需的最大宽度是多少?” -
Modifier.height(IntrinsicSize.Min)
— “要正确闪现内容,所需的最小高度是多少?” -
Modifier.height(IntrinsicSize.Max)
— “要正确闪现内容,所需的最大高度是多少?”
但是,Intrinsic 测量 不会真实地测量 子项两次。相反,它进行了一种不同类型的核算——您可以将其视为不需求指数测量时刻的 预测量进程,由于它 更轻量、更简略。 因此,尽管这并没有 彻底 打破单一的测量规则,但它的确略微改变了一点,并闪现了一个超出常规要求的 Compose 要求。
创建自定义布局时,Intrinsics 供应依据 近似值 的 默许完结。但是,在某些情况下,默许核算或许无法按预期作业,因此 API 供应了一种 掩盖(重写) 这些默许值的方法。
要指定自定义布局的 Intrinsic 测量,您可以在测量进程中重写MeasurePolicy
接口的minIntrinsicWidth
、minIntrinsicHeight
、maxIntrinsicWidth
和 maxIntrinsicHeight
Layout(
modifier = modifier,
content = content,
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
// 在这儿进行 测量 和 布局
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurables: List<IntrinsicMeasurable>,
width: Int
): Int {
// 在这儿核算自定义 maxIntrinsicHeight 的逻辑
}
// 其他与 intrinsics 相关的方法都有默许值,
// 您可以只重写您需求的方法。
}
)
到这儿就结束了
我们今日涵盖了许多内容——术语 “Layout(布局)” 的全部 不同意义 以及它们之间的联络,在构建自定义布局时怎样 进入和控制 布局阶段才能对您有利,然后我们总结了SubcompositionLayout
和 Intrinsic 测量 作为附加的 API 来完结非常详细的布局行为。
至此,我们结束了 MAD 技能 组合布局 和 修饰符 系列!在只是几集内容中,就触及了从 布局 和 修饰符 的最基础的知识,到供应简略而健壮的 Compose 布局、Compose 的各个阶段,再到 修饰符链的次第 和 subcomposition(子组合) 等高级概念 – 祝贺,您现已取得了长足的前进!
我们希望您现已了解了有关 Compose 的新知识,并更新了旧知识,最重要的是 — 您感到更有准备和决心,将 全部内容 迁移到 Compose 。
这篇博文是系列博文的一部分:
第 1 集:Compose 布局 和 修饰符 的基础知识
第 2 集:Compose 的各个阶段
第 3 集:捆绑 和 修饰符 次第
第 4 集:高级布局概念