原文链接。

MAD 技能:Compose 布局 和 修饰符 第 4 集

Jetpack Compose:高级布局概念

欢迎回到 关于 Jetpack Compose 布局 和 修饰符 的 MAD 技能系列!在上一集,我们谈论了 Compose 的 Layout 阶段,以解说 修饰符链的次第传入的父级捆绑 是怎样影响 它们所传递给的 可组合项的。

今日这一集,我们进一步集合 布局阶段 和 捆绑,并从另一个角度介绍它们——怎样使用它们的力气 在 Compose 中 构建 自定义布局。

为了建自定义布局,我们会介绍布局阶段 可以做 什么、怎样 进入它 以及怎样 运用其子阶段(测量和放置)来发挥您的优势,用于构建活络的自定义布局。

之后,我们将介绍两个重要的、违反规则的 Compose API:SubcomposeLayoutIntrinsic 测量 ,作为布局难题的终究两个缺失部分。这些概念将为您供应额外的知识,以在 Compose 中构建 具有非常详细要求的凌乱规划

您也可以将本文作为 MAD 技能视频观看:

youtu.be/l6rAoph5UgI

Compose 的全部布局

在前几会合,我们谈论了 Compose 怎样通过其三个阶段 将数据转换为 UI:组合、布局和制造,或闪现 “什么”、放在 “哪里” 以及 “怎样” 出现它 .

Jetpack Compose:高级布局概念

但正如我们系列的称谓所暗示的那样,我们最感兴趣的是 布局阶段

但是,Compose 中的术语 “布局(Layout)” 用于许多不同的事物,并且由于其意义众多,然后看起来或许令人困惑。到目前为止,在本系列中,我们现已学习了以下用法:

  • 布局阶段:Compose 的三个阶段之一,其间父布局定义其子元素的大小和方位
  • 布局:一个广泛的抽象术语,用于在 Compose 中快速定义任何 UI 元素
  • 布局节点: 一个抽象概念,用作 UI 树中一个元素的可视化表示,被创建为 Compose 中的 组合 阶段的作用。

在这一会合,我们还将学习一些额外的意义来完结整个布局循环。让我们先快速分解它们——对这些术语的更深入解说,稍后将在帖子中进一步介绍:

  • Layout 可组合项: 用作 Compose UI 中心组件的可组合项。在 组合(Composition)期间调用时,它会在 Compose UI 树中创建并增加一个布局节点;全部更高等级布局的基础,如ColumnRow等。
  • layout()函数 – 放置的起点,这是布局阶段的第二个子进程,担任在测量的榜首个子进程之后,将子项放置在Layout 可组合项中
  • .layout()modifier — 一种修饰符,它包裹一个布局节点,并容许独自调整它的大小和放置它,而不是由其父布局完结

现在我们知道什么(对应上文中的 “闪现 ‘什么’ ”)是什么了,让我们从 布局阶段 初步,同时 并扩大 它(布局阶段)。如前所述,在布局阶段,UI 树中的每个元素 测量其子元素(假设有),并 将它们放置 在可用的 2D 空间中。

Jetpack Compose:高级布局概念

Compose 中的每个开箱即用的布局,例如RowColumn等等,都会为您 自动地 处理全部这些。

但是,假设您的规划需求 非标准布局,那您需求自定义并构建自己的布局,例如来自我们的 JetLagged 示例 的TimeGraph?

Jetpack Compose:高级布局概念

这正是您需求更多地了解布局阶段的时分——怎样 进入它(布局阶段) 以及怎样使用它的子元素 测量和放置 的子阶段,才会对您有利。那么,让我们来看看怎样依据给定的规划 在 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作为其子项,并接受一个用于测量和定位其元素的 测量战略。全部更高等级的布局,例如 ColumnRow,都在底层运用这个可组合项。

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()函数并进入放置作用域初步。此时,父布局将可以 决议自己的大小 (totalWidthtotalHeight),例如,将其子 可放置政策 的宽度和高度相加:

@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 元素,被它的父级以我们不太喜爱的方法紧缩:

Jetpack Compose:高级布局概念

我们只希望这个简略 Column 中的一个Element,可以通过为其移除周围的40.dppadding,强制它拥有比父级的宽度 更大的宽度,例如,以完结边对边的外观:

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

Jetpack Compose:高级布局概念

正如我们之前所说,Compose 的优势之一是您可以在解决问题时 挑选自己的途径,由于条条大路通罗马。假设您知道此元素所需的 切当静态大小,另一种方法或许是在其上设置 .requiredWidth() 修饰符,以便父布局中的传入捆绑 不会掩盖 其设置的宽度,而是 “尊重”它。相反,运用常规的 .width() 修饰符会使设置的宽度被父布局和测量阶段的传入捆绑掩盖。

SubcomposeLayout — 打破 Compose 各个阶段 的规则

在前面几会合,我们谈到了 Compose的各个阶段 以及它们精确排序的规则:1. 组合,2. 布局,3. 制造。布局阶段随后分解为 测量和放置 子阶段。尽管这适用于绝大多数Layout可组合项,却有一个打破规则的布局不遵从此架构,但有一个很好的理由 —SubcomposeLayout

考虑以下用例——您正在构建一个包括一千个 item 的列表,而这些 item 根本 无法 同时 包容在屏幕上。在那种情况下,组合全部这些子 item 将是不必要的资源糟蹋——假设其间大部分甚至都看不到,那为什么要预先组合这么多 item 呢?

Jetpack Compose:高级布局概念

相反,更好的方法是1。量子 以取得它们的大小,然后在此基础上,2。核算可用视口(可以理解为屏幕上的可见区域)中 可以包容的 item 数,终究只组合 可见 的 item。

Jetpack Compose:高级布局概念

这是SubcomposeLayout反面的主要思维之一——它需求 首要 对部分或全部子可组合项进行 测量,然后运用该信息来 承认是否组合 部分或全部子项。

Jetpack Compose:高级布局概念

这正是 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 ——您只是不能改变它们的组合。

Jetpack Compose:高级布局概念

Intrinsic 测量 — 打破单遍测量规则

我们之前提到的第二个 Compose 规则是 布局 阶段的 单遍测量,这对该进程和布局系统的整体功用有很大帮忙。想一想短时刻内或许发生的 重组数量,以及捆绑每次重组的整个 UI 树的测量,这些都将大幅前进整体速度!

Jetpack Compose:高级布局概念

为每次重组,遍历具有许多 UI 节点的树

但是,在某些用例中,父布局 需求 在测量子布局之前 了解 有关其子布局的 一些信息,以便它可以运用此信息来 定义和传递捆绑。而这正是 Intrinsic 测量的用途所在——让你 在子项被测量之前,可以事前查询子项(的相关信息)

Jetpack Compose:高级布局概念

让我们看看下面的比方——我们希望这列Columnitems 具有相同的宽度,或许更精确地说,让每个 item 的宽度与最宽的子项(在我们的比方中,指 “And Modifiers” 这个 item)的宽度相同。但是,我们也希望最宽子项按需取得尽或许多的宽度。所以我们的榜首步是:

@Composable
fun IntrinsicExample() {
    Column() {
        Text(text = "MAD")
        Text(text = "Skills")
        Text(text = "Layouts")
        Text(text = "And Modifiers")
    }
}

Jetpack Compose:高级布局概念

但是,我们可以看到,这还不可。每个 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())
    }
}

Jetpack Compose:高级布局概念

但是,这会将每个 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())
    }
}

Jetpack Compose:高级布局概念

通过在父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())
    }
}

Jetpack Compose:高级布局概念

快速总结全部可用的 intrinsic 组合:

  • Modifier.width(IntrinsicSize.Min)— “要正确闪现内容,所需的最小宽度是多少?”
  • Modifier.width(IntrinsicSize.Max)— “要正确闪现内容,所需的最大宽度是多少?”
  • Modifier.height(IntrinsicSize.Min)— “要正确闪现内容,所需的最小高度是多少?”
  • Modifier.height(IntrinsicSize.Max)— “要正确闪现内容,所需的最大高度是多少?”

但是,Intrinsic 测量 不会真实地测量 子项两次。相反,它进行了一种不同类型的核算——您可以将其视为不需求指数测量时刻的 预测量进程,由于它 更轻量更简略。 因此,尽管这并没有 彻底 打破单一的测量规则,但它的确略微改变了一点,并闪现了一个超出常规要求的 Compose 要求。

创建自定义布局时,Intrinsics 供应依据 近似值默许完结。但是,在某些情况下,默许核算或许无法按预期作业,因此 API 供应了一种 掩盖(重写) 这些默许值的方法。

要指定自定义布局的 Intrinsic 测量,您可以在测量进程中重写MeasurePolicy接口的minIntrinsicWidthminIntrinsicHeightmaxIntrinsicWidthmaxIntrinsicHeight

    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(布局)” 的全部 不同意义 以及它们之间的联络,在构建自定义布局时怎样 进入和控制 布局阶段才能对您有利,然后我们总结了SubcompositionLayoutIntrinsic 测量 作为附加的 API 来完结非常详细的布局行为。

至此,我们结束了 MAD 技能 组合布局 和 修饰符 系列!在只是几集内容中,就触及了从 布局 和 修饰符 的最基础的知识,到供应简略而健壮的 Compose 布局、Compose 的各个阶段,再到 修饰符链的次第 和 subcomposition(子组合) 等高级概念 – 祝贺,您现已取得了长足的前进!

我们希望您现已了解了有关 Compose 的新知识,并更新了旧知识,最重要的是 — 您感到更有准备和决心,将 全部内容 迁移到 Compose 。

这篇博文是系列博文的一部分:

第 1 集:Compose 布局 和 修饰符 的基础知识
第 2 集:Compose 的各个阶段
第 3 集:捆绑 和 修饰符 次第
第 4 集:高级布局概念