本文依据自己的了解翻译自 MAD Skills Compose Layout and Modifier 系列文章的第四篇 # Advanced Layout concepts ,适合有Compose根底的同学阅读,另外建议有英语根底的同学直接看原文哈,附上原文链接


Compose布局进阶


欢迎来到 “Jetpack Compose布局与modifiers”,在之前的文章,咱们讨论了 Compose 的布局阶段,详细来说,便是 modifier 链式调用和自上而下的 Constraints 传递对 Composable 的影响。

在今天的文章,咱们的要点依旧在布局阶段和 constraints, 可是是从另一个角度来探讨———怎么运用它们来打造自界说 layout.

为了打造自界说 layout,咱们会去了解布局阶段能做什么、布局阶段的入口在哪里、怎么运用布局的两个子阶段(丈量和摆放),而且最重要的,怎么运用这些去构造出灵敏的自界说 layout。

然后咱们会再了解两个很重要可是不走惯例布局流程的API来作为布局难题的最终两块拼图:SubComposeLayout 和Intrinsic measurements(固有特性丈量)。了解这些概念会协助你构建愈加杂乱和特别的自界说Composable


Compose里的一切layouts概念


在之前的文章,咱们讨论了 Compose 经过三个阶段把数据转化为UI:组合(要显现什么)、布局(要显现在哪里)、制作(怎么烘托)

Compose布局进阶

但就如这系列文章的标题所示,咱们主要关心布局阶段。

但是,”Layout”在 Compose 里有许多种概念,并可能带来歧义造成不必要的困惑,咱们先整理一下之前用到的一些”Layout”:

  • Layout phase(布局阶段):用于父layout决议自己的尺度和摆放子元素

  • layout:一个广泛的抽象术语,用于快速界说Compose中的任何UI元素

  • layout node:用于表示 Compose UI树上的节点,组合阶段的责任便是把 Composable 代码转化成 layout node


在这篇文章,咱们会进一步学习更多的”Layout”概念来完善布局认知。先快速简略地提及一下,更多的解说会在后面的下文中:

  • Composable 的 Layout: 中心 Composable 函数,组合阶段时运用会在 Compose UI树中创立 layout node;咱们常用的 Column、Row 等都是依据 Layout 封装而来

  • layout()办法: 布局阶段摆放子 Composable 的入口,在丈量完子 Composable 后进行

  • .layout() modifier: 用于修正单个 Composable 的丈量和摆放,(会取代父Composable原本对它的丈量和摆放)

在弄清楚上面的概念后,咱们先从布局阶段说起。如之前提及,在布局阶段,UI树上的一切元素都会丈量自己的子元素并在二维空间里摆放他们。

Compose布局进阶

Compose 里供给的一切开箱即用 Layout,如 Row,Column 等,都会自动帮你处理这个进程。

可是假如你的规划想要一些非惯例的布局,像JetLaggeg sample里的TimeGraph,就需求你自己完成

Compose布局进阶

这正是你需求了解有关布局阶段的更多信息的时分:它的入口在哪?怎么运用它的两个子阶段(丈量、摆放)完成你的需求?现在就让咱们看看怎么运用 Compose 的自界说布局去完成给定的规划需求


进入布局阶段

让咱们先去搞清楚构建自界说布局最重要也是最根底的一步。当然,假如你想要一个详细、手把手教育的视频攻略了解“何时需求创立自界说布局和杂乱的UI规划并怎么完成”,这里是视频地址: Custom layouts and graphics in Compose video 又或许在JetLaggeg sample 源码仓库 里直接自行探究TimeGraph的源码。

调用 Layout Composable 函数是创立自界说布局和进入布局阶段的入口:

@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
) {
  // …
}

它承受一个 Composable 参数作为子 Composable; measure policy 的的参数界说详细丈量和摆放。一切的上层布局都是依据这个 Composable.

一旦进入布局阶段,咱们就会看到它由两个过程组成,即丈量和放置,次序履行:

@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 被传入,在 Layout 办法里,measurables 默认为列表方式:

@Composable
fun CustomLayout(
  content: @Composable () -> Unit,
  // …
) {
  Layout(
    content = content,
    modifier = modifier
    measurePolicy = { measurables: List<Measurable>, constraints: Constraints ->
      // 丈量效果域
      // 1. 丈量阶段
      // 子元素各自丈量并回来自己的丈量成果
    }
  )
}

依据你自己的布局需求,你既可以运用传递进来的constraints持续在传递给子元素去丈量列表里的每一个子元素,让子元素保持自己预界说的巨细:

@Composable
fun CustomLayout(
  content: @Composable () -> Unit,
  // …
) {
    Layout(
      content = content,
      modifier = modifier
    ) { measurables, constraints ->
      // 丈量效果域
      // 1. 丈量阶段
      measurables.map { measurable ->
        measurable.measure(constraints)
      }
    } 
}

又或许依据自己的需求调整 constraints(经过修正传递给子元素的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
        )
      )
    }
  }
}

在之前的文章咱们已经了解到布局阶段里 constraints 在UI树里是自上而下传递的,当一个元素丈量它的子元素的时分,会供给 constriants 让子元素了解自己可以运用的尺度巨细规模。

布局阶段一个十分重要的特性是单向传递丈量成果。这意味着每个元素不会丈量两遍。这个特性让 Compose 可以高效地丈量大规模的UI树。

丈量办法履行后会回来一个 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() 办法进入。这时分当时元素就可以决议自己的尺度了(经过layout办法的参数传入),比如说下面的比如运用子元素的宽度和作为自己的宽度,子元素的高度和作为自己的高度:

@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() modifier

Layout composable 用于操控一切子元素的尺度、摆放,可是假如你只想操控单个元素,那这多少有点杀鸡用牛刀了。

在这种情况下,Compose 结构供给了更好、更简略的解决方案:.layout() modifier ———用于操控单个元素的尺度和摆放

让咱们看一个详细的比如:一个Column包着4个条状元素

Compose布局进阶

咱们想要其间一个元素的宽度表现得不受父元素里的40dp内边距影响,让它看起来撑满整个父元素:

@Composable
fun LayoutModifierExample() {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color.LightGray)
            .padding(40.dp)
    ) {
        Element()
        Element()
        // 想要改动下面这个元素的宽度
        Element()
        Element()
    }
}

为了完成这样的效果,咱们给第三个元素设置 .layout() modifier

事实上运用 .layout() modifier 和刚刚提及的 Layout composable 十分类似,它承受一个 lambda 参数, lambda 里供给了代表当时元素的 measurable 和外面传递进来的 constraints,有了这些你可以独自完成这个元素的丈量和摆放:

Modifier.layout { measurable, constraints ->
    // 丈量
    val placeable = measurable.measure(...)
    layout(placeable.width, placeable.height) {
        // 摆放
        placeable.place(...)
    }
}

回到上面的比如,咱们经过让constraints的最大宽度增加80dp来改动子元素的宽度:

Element(modifier = Modifier.layout { measurable, constraints ->
  val placeable = measurable.measure(
    constraints.copy(
      // 修正constraints的最大宽度
      maxWidth = constraints.maxWidth + 80.dp.roundToPx()
    )
  )
  layout(placeable.width, placeable.height) {
    // 还原元素的摆放偏移
    placeable.place(0, 0)
  }
})

Compose布局进阶

Compose结构的灵敏性让你可以有多种方式解决问题。假如你知道这个元素的切当宽度,完成上面的需求的另一种方式是运用 .requiredWidth() modifier, 这样元素就会无视父元素由 padding modifier 带来的 constraints 约束了。

SubcomposeLayout———打破惯例Compose三阶


在之前的文章,咱们提及了Compose把数据到UI转化分为三个阶段:1.组合2.布局3.制作。且次序进行。其间布局阶段又可以细分为丈量和摆放两个子阶段。这样的规矩适用于绝大部分 Composable 布局,其实还存在不遵从这种形式且有充分理由这样做的布局———SubcomposeLayout.

试想一下下面的场景:你在构建一个含有不计其数元素的列表UI,显然屏幕是不能一次显现完的,这种情况下把一切的元素都组合、布局、烘托出来显然是功能价值高昂且没有必要的。

Compose布局进阶

相反,更好的办法应该是:

  1. 丈量子元素获取他们的尺度

  2. 依据该尺度核算当时屏幕可以显现多少元素

  3. 最终只把适宜数量的UI转化出来

Compose布局进阶

这便是SubcomposeLayout背面的主要思想,它需求先知道子元素的丈量成果然后依据该成果决议是转化其间的一些还是一切元素为UI.

Compose布局进阶

Compose 中的 Lazy 组件依据此构建,而且可以在滚动时按需追加内容。

SubcomposeLayout 把组合阶段延迟到布局阶段中,更准确地说,组合阶段需求在布局阶段里的丈量阶段之后,这样在组合前父元素就得到了子元素的尺度。

BoxWithConstraints 底层也是运用 SubcomposeLayout, 可是这个组件依据 SubcomposeLayout 封装的动机和 Lazy 组件稍微有些不同,BoxWithConstraints 是为了把上层传递来的 constraint 露出给了调用者,咱们都已经知道 constraints 原本只在 Layout 阶段才会知道:

BoxWithConstraints {
  // maxHeight这个在constraints里的信息只在BoxWithConstraints可用,
  // 为了拿到这个信息相同需求延迟组合阶段到布局里丈量阶段之后
  if (maxHeight < 300.dp) {
    SmallImage()
  } else {
    BigImage()
  }
}

不应该乱用 SubcompositionLayout


SubcompositionLayout 为了完成一些更灵敏的动态需求,改动了 Compose 的惯例流程,但这相同是有功能价值和限制的。因而理解什么时分才应该运用 SubcompositionLayout 是十分重要的。

一个简易的辨别是否需求运用 SubcompositionLayout 的办法便是 一个子元素在编写时是否需求依靠其他子元素的丈量成果,像刚刚提及的 Lazy 组件和 BoxWithConstraints 都是需求的。

假如你仅仅需求一个子元素的丈量成果去丈量另一个子元素,你可以运用惯例的 Layout composable 完成,你依然可以依据彼此的成果分别丈量子元素,仅仅无法更改它们的组合。

Compose布局进阶

打破单向传递丈量成果———固有特性丈量(Intrinsic measurements)


在布局阶段单向传递丈量成果是咱们之前提及的一个规矩,因为这样有利于丈量体系的功能表现。咱们都知道重组会十分频繁地发生,试想一下一个杂乱的UI树假如丈量阶段不能高效履行,那么重组的功能就会十分差。

Compose布局进阶

但是有些时分父布局确实在丈量子元素前需求一些子元素的信息,比如说依据这些信息去修正传递给子元素的constraints.这便是固有特性丈量的责任,让你在丈量前知道子元素的信息。

Compose布局进阶


让咱们看看下面的比如,咱们想要 Column 的子元素宽度一样而且等同于子元素里最宽的那一个(下面的比如便是”And Modifier”这个Text)。咱们先写成这样:

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

Compose布局进阶

可以看到这并不满意咱们的需求,每个子元素的宽度都仅仅各自所需求的宽度。咱们再试试这样:

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

Compose布局进阶

em… 现在每个子元素的宽度都变成了 Column 的最大宽度,相同不满意需求。这种情况下,咱们就需求运用固有尺度了:

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

Compose布局进阶

经过运用 IntrinsicSize.Max 作为 Column 的宽度,它会查询一切子元素正确显现一切内容的最大值来最为 Column 的宽度。这样也就完成需求了。

反之,假如咱们运用 IntrinsicSize.Min, 那 Column 会查询一切子元素正确显现一切内容的最小值来最为 Column的宽度,在这个比如中,便是每行刚好显现一个单词

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

Compose布局进阶

固有特性的快速总结:

  • Modifier.width(IntrinsicSize.Min):正确显现内容所需求的最小宽度

  • Modifier.width(IntrinsicSize.Max):正确显现内容所需求的最大宽度

  • Modifier.height(IntrinsicSize.Min):正确显现内容所需求的最小高度

  • Modifier.height(IntrinsicSize.Max):正确显现内容所需求的最大高度

但是固有特性丈量并不是丈量子元素两遍,它经过其他核算,你可以把它幻想成不需求指数丈量时刻的预丈量,比惯例丈量愈加高效。因而,这并没有完全打破单次丈量这个规矩(应该是指没有改动丈量阶段的时刻杂乱度)。

当咱们创立一个自界说 layout, 固有特性丈量供给了一个依据近似值的默认完成。当然这并不是什么时分都如你预期,所以API也预留了重写他们的办法。

    Layout(
        modifier = modifier,
        content = content,
        measurePolicy = object : MeasurePolicy {
            override fun MeasureScope.measure(
                measurables: List<Measurable>,
                constraints: Constraints
            ): MeasureResult {
                // Measure and layout here
            }
            override fun IntrinsicMeasureScope.maxIntrinsicHeight(
                measurables: List<IntrinsicMeasurable>,
                width: Int
            ): Int {
                // Logic for calculating custom maxIntrinsicHeight here
            }
            // Other intrinsics related methods have a default value,
            // you can override only the methods that you need.
        }
    )

结语


今天咱们了解了很多东西,Compose 中的各种不同意义的”Layout”以及它们之间的联络;怎么运用布局阶段完成自界说布局;经过 SubcompositionLayout 和固有特性丈量完成非惯例需求。

至此,MAD Skills Compose Layouts and Modifiers系列文章就结束了!这几篇文章从最根底的布局和 Modifiers,简略强大的 Compose 布局,Compose 的三个阶段,再到进阶的 modifier 链式调用和 subcomposition. 我相信你收获颇丰!

咱们期望你学习到Compose的新常识,也更新老常识,当然最重要的是你会感觉更有决心和预备好把应用迁移至Compose了!