Jetpack Compose自定义Layout详解
本文字数:10105字
预计阅读时间:26分钟
前言
最近在使用Jetpack Compose进行应用开发,不得不说相比较命令式布局方式Xml,Compose声明式UI真香。然而仅使用基础的Compose控件在面对UI大佬的设计图时,总会出现一些“小问题”,包括部分页面不能通过标准布局实现、需要特别精准的控制测量和放置的场景、需要实现布局动画等,这个时候往往需要通过自定义布局去解决这些“小问题”,来达到项目验收标准。既然是要自定义组件,那么首先了解一下Compose的布局模型吧,从布局模型中找到自定义组件的切入点。
布局模型
Compose布局模型可以分为三个部分 组合、布局、绘制
首先组合阶段会执行可组合函数,从而创建界面树,举个例子
上图就是按照 SearchResult 组合函数内部子组件顺序生成界面树的示例。日常开发中可组合函数内部可能会包含各种状态逻辑和控制流,反映到组合阶段就是根据不同的逻辑控制来生成不同的界面树。
接下来的 Layout(布局) 阶段会遍历界面树,通过测量组件以确定自身大小以及在手机上应该摆放的位置,这点和View视图的 onMeasure 和 onLayout 类似,都是先测量然后放置,但是也有不一样的地方,View视图可以经过多次测量来确定子视图的大小和位置信息,但是在Compose中每个组件只允许测量一次,多次测量会报错,正是这一特性可以让我们在使用过程中任意嵌套组件而不会造成很大的性能问题。
最后的 Draw(绘制) 阶段会再次遍历界面树,这个阶段主要做的事情就是把组件绘制到界面上。主要涉及 Canvas 操作,Compose 中画布操作和Android中画布操作大同小异,函数式风格的API使用起来会更方便,虽然有些画布操作 Compose 没有支持,但我们也可以通过 drawIntoCanvas 方法拿到原生Android 环境下的 Canvas 和 Paint 对象进行绘制。这个阶段本文不做深入探讨。
在了解了Compose的布局三个阶段之后,要想自定义组件,切入点还是得从 Layout(布局) 阶段开始,本文主要对该阶段进行深入探讨。
Layout
阶段主要涉及了三个步骤:
测量所有子项 确定自己的尺寸 放置其子项
这个看起来跟Android中onMeasure和onLayout的合并,其实在Compose中任何组件都是基于Layout(...)
可组合函数进行搭建的,接下来通过源码剖析来探究其内部做了什么? 如果要自定义需要如何做?
Layout可组合函数
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
...
}
Layout()
可组合函数构成很简单
content
里面可以放置子组件布局modifier
修饰符设置大小、背景等修饰都靠它,其实还可以做更多事后面专门分析。measurePolicy
负责测量和放置逻辑。
MeasurePolucy
是一个接口,需要组件自己实现,像我们经常使用的Column
、Row
等组件都是通过实现自己的逻辑来达到不同的约束效果。我们如果要自己实现一个Column
怎么做呢? 首先需要声明我们自己的可组合函数,内部调用Layout()
然后在measurePolicy
中实现测量和放置逻辑,代码如下:
@Composable
fun CustomColumnLayout(
modifier: Modifier,
content: @Composable () -> Unit
){
Layout(
content = content,
modifier = modifier,
//这个地方为了更加直观使用了 object 匿名函数声明方式,当然也可以简化成lambda表达式
//measurePolicy = MeasurePolicy { measurables, constraints ->
//
//})
measurePolicy = object : MeasurePolicy {
override fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult {
//测量和放置逻辑
...
}
})
}
constraints
表示父元素允许子元素的尺寸范围,如果子元素从该范围内选择了尺寸,那么父元素必须接受并处理子元素。尺寸范围包含最大最小高度和最大最小宽度,当最大最小高度/宽度相等的时候表示高度/宽度为确定值。
class Constraints {
val minWidth: Int
val maxWidth: Int
val minHeight: Int
val maxHeight: Int
}
measurables
表示子元素布局列表, Measurable
可以通过measure(constraints: Constraints)
方法进行测量并得到一个新的可放置的布局Placeable
。所有子元素测量完成后,在根据得到的测量结果进行具体的放置工作,用代码体现出来如下:
@Composable
fun CustomColumnLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>,
constraints: Constraints ->
// placeables 是经过测量的子元素,它拥有自身的尺寸值
val placeables = measurables.map { measurable ->
// 测量所有子元素,这里不编写任何自定义测量逻辑,只是简单地
// 调用 Measurable 的 measure 函数并传入 constraints
measurable.measure(constraints)
}
val width = placeables.sumOf { it.height }// 根据 placeables 计算得出
val height = placeables.maxOf { it.width }// 根据 placeables 计算得出
// 报告所需的尺寸
layout (width, height) {
var y = 0
placeables.foreach { placeable ->
//通过遍历将每个项目放置到最终的预期位置
placeable.placeRelative(x = 0, y = y)
// 按照所放置项目的高度增加 y 坐标值
y += placeable.height
}
}
}
}
从代码中可以看到,测量得到的placebles
提供了placeRelative()
方法,我们可以指定x
和y
值来确定位置,此方法还有额外的作用,如果布局方向为从右到左,则给定的位置将被水平镜像。相对应的如果不想对布局从右到左还是从左到右作出反应可以使用place()
方法,该方法只通过指定x
和y
值来确定位置。到这里一个属于自己的Column
组件就搞定了,使用起来也很简单
@Composable
fun Test(){
CustomColumnLayout{
Text(text = "test1")
Text(text = "text2")
Text(text = "text3")
}
}
接下来我们将分析Layout()
可组合函数的另一个参数Modifier
,在Compose开发中Modifier
起着非常重要的作用,我们使用标准布局也好,使用自定义布局也好都离不开Modifier
,那么它究竟是如何运作的呢?我们可不可以通过Modifier
来对已有的组件进行自定义布局信息呢?带着这两个疑问让我们进入Modifier
的世界去寻找答案。
修饰符
Compose中修饰符虽然有很多种,但它们的基类Modifier
其实没有多复杂
interface Modifier {
fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
fun any(predicate: (Element) -> Boolean): Boolean
fun all(predicate: (Element) -> Boolean): Boolean
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
interface Element : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
operation(this, initial)
override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)
override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
}
companion object : Modifier {
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = initial
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
override fun any(predicate: (Element) -> Boolean): Boolean = false
override fun all(predicate: (Element) -> Boolean): Boolean = true
override infix fun then(other: Modifier): Modifier = other
override fun toString() = "Modifier"
}
}
如果我们把没有默认实现的foldIn()
、foldOut()
、any()
、all()
方法屏蔽掉
interface Modifier {
...
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
interface Element : Modifier {
...
}
companion object : Modifier {
...
override infix fun then(other: Modifier): Modifier = other
override fun toString() = "Modifier"
}
}
可以很明显的看到Element
以及Modifier
的伴生对象都是实现了Modifier
接口,其实还有一个类也实现了,就是CombineModifier
这个类,它在"合并"不同的Modifier
以及遍历的时候很关键。总结一下直接实现Modifier
接口的三个类:
Element
接口Modifier
接口伴生对象CombineModifier
类
我们通过一段代码来更加深入的了解Modifier
的运作原理
Column(
modifier = Modifier
.fillMaxSize()
.padding(5.dp)
.background(Color.White)
) {
...
}
上面代码通过Modifier
设置了大小、边距和背景色。代码是链式调用,通过深入源码看下究竟是如何串起来的,从调用链第一个fillMaxSize()
着手分析
fun Modifier.fillMaxSize(fraction: Float = 1f) =
this.then(if (fraction == 1f) FillWholeMaxSize else createFillSizeModifier(fraction))
fillMaxSize()
是以扩展函数的方式声明并返回Modifier
实例,this.then(...)
因为是链式调用第一个所以调用的应该是Modifier
接口伴生对象内部默认实现的then(...)
方法
interface Modifier {
...
companion object : Modifier {
...
override infix fun then(other: Modifier): Modifier = other
...
}
}
then(...)
方法非常简单直接返回传入的other
,在这里应该是FillModifier
,所以当前的Modifier
就是FillModifier
接下来是padding(5.dp)
fun Modifier.padding(all: Dp) =
this.then(
PaddingModifier(
...
)
)
这里this.then(...)
调用的是FillModifier
中实现的方法,FillModifier
内部没有重写该方法我们向上查找,根据继承链 FillModifier
->LayoutModifier
->Element
->Modifier
,最终找到then()
方法的实现是在顶层Modifier
接口中
interface Modifier {
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
}
class CombinedModifier(
private val outer: Modifier,
private val inner: Modifier
) : Modifier
if(other === Modifier)
这个条件反编译成java后查看是等同于if(other == Modifier.Companion)
,因为伴生对象也是实现Modifier
接口的。所以这里可以直接把传入的other
实例和接口的伴生对象对比,如果相等直接返回,如果不等则使用CombinedModifier
进行处理。CombinedModifier
的处理也比较简单,接收两个Modifier
类型参数分别定义为outer
和inner
,经过链式调用的第二步我们的当前的Modifier
结构如下:
链式调用最后一个background(Color.White)
fun Modifier.background(
color: Color,
shape: Shape = RectangleShape
) = this.then(
Background(
...
)
)
这里跟上面一样都是通过调用then()
方法将Background
与之前的Modifier
合并,这里Background
是实现DrawModifier
接口的,那么经过这一轮调用我们可以得到类似上图的结构,只不过多了一层,以此类推如果我们通过Modifier
创建了很长的调用链都可以转换为下图:
最后生成的Modifier
链会被传入到Layout(...)
可组合函数中,在函数中通过materializerOf(modifier)
方法进行处理
@Composable inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
...
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
...
},
skippableUpdate = materializerOf(modifier), //这里处理modifier
content = content
)
}
materializerOf
方法中使用到了两个方法foldIn()
和foldOut()
,前面提到我们在组合函数中声明的Modifier
修饰都会转成对应的Modifier
链,foldIn()
和foldOut()
就是Modifier
提供的遍历方法。
//Element中实现
override fun <R> foldIn(initial: R, operation: (R, Element) -> R): R = operation(initial, this)
override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = operation(this, initial)
//CombinedModifier中实现
override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
inner.foldIn(outer.foldIn(initial, operation), operation)
override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
outer.foldOut(inner.foldOut(initial, operation), operation)
从代码中我们可以看出foldIn()
和foldOut()
正好相反,拿之前示例的代码进行测试,比如打印出Modifier
链的长度
val modifier = Modifier
.fillMaxSize()
.padding(5.dp)
.background(Color.White)
val result = modifier.foldIn(0) { index, element ->
Log.d("Modifier", "index = $index element = $element")
index + 1
}
使用foldIn()
输出结果
使用foldOut()
输出结果
对比结果,我们发现CombinedModifier
并没有输出,看来CombinedModier`仅仅是作为内部数据结构在使用,官方如此设计应该是希望对上层开发者无感知。
回到materializerOf
中继续分析,最终调用Composer的materialize
方法,在这个方法内判断如果Modifier
链中没有ComposedModifier
则不做处理并返回原Modifier链,否则通过foldIn()
进行遍历,遇到ComposeModifer
类型直接通过它提供的工厂方法构造新的Modifier
,生成新的Modifier
也有可能还是ComposerModifer
继续重复执行,最终得到Modifier
链是不包含ComposerModifier
的,即是完全摊开的Modifier链。
fun Composer.materialize(modifier: Modifier): Modifier {
if (modifier.all { it !is ComposedModifier }) return modifier
...
val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
acc.then(
if (element is ComposedModifier) {
...
val factory = element.factory as Modifier.(Composer, Int) -> Modifier
val composedMod = factory(Modifier, this, 0)
materialize(composedMod)
} else element
)
}
...
return result
}
其实这仅仅是开始,Modifier链的核心处理是通过LayoutNode来实现的,在LayoutNode
中进一步生成LayoutNodeWrapper
链,节点转换有很多种,我们拿LayoutModifier
来分析,在节点转换时候会包装成ModifiedLayoutNode
,然后在Layout()
可组合函数中通过Measurable
调用measure
方法时会触发ModifiedLayoutNode
中的measure
方法,在内部会触发LayoutModifier
的measure()
方法,该方法的实现都是在LayoutModifier
的子类中,我们拿PaddingModifier
来具体分析:
private class PaddingModifier(
...
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
...
override fun MeasureScope.measure(
measurable: Measurable,//下一个 LayoutNodeWrapper 节点
constraints: Constraints//来自父容器或者来自上一个节点的约束
): MeasureResult {
val horizontal = start.roundToPx() + end.roundToPx()
val vertical = top.roundToPx() + bottom.roundToPx()
//测量下一个节点
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
if (rtlAware) {
placeable.placeRelative(start.roundToPx(), top.roundToPx())
} else {
placeable.place(start.roundToPx(), top.roundToPx())
}
}
}
}
一个元素的所有LayoutNodeWrapper
节点测量完成后,会通过Layout()
可组合函数中的MeasurePolicy
实现继续测量子元素,重复流程直到所有的子元素测量完成。
分析到这里我们就可以发现自定义Layout除了通过自定义实现measurePolicy
之外,通过自定义Modifier
也可以达到修改布局的目的。这两者就类似于自定义ViewGroup和自定义View,通过Layout()
可组合函数自定义相当于自定义ViewGroup,通过Modifier
自定义相当于自定义View。对于第二种自定义方式可以通过Modifier
提供的layout
方法去实现
fun Modifier.customLayout() = layout { measurable, constraints ->
val placeable = measurable.measure(constraints) //测量
//在这里可以增加逻辑代码 如果需要的话
...
layout(constraints.maxWidth, constraints.maxHeight) {
//位置放置 可以实现自己的放置逻辑
placeable.placeRelative(0, 0)
}
}
另外官方文档实现的firstBaselineToTop就是一个很不错的例子值得一看。
总结
通过Compose布局模型,着重分析了组合->布局->绘制三个阶段中的布局阶段,以实例和源码结合作为切入点总结出了 Compose中自定义Layout的两种方式
通过调用 Layout
可组合函数重写measurePolicy
,内部实现自己的测量和放置逻辑,跟Android中自定义ViewGroup类似。实现 Modifier
的layout
方法,内部实现自己的测量和放置逻辑,跟Android中自定义View类似。
这两种布局方式可以根据具体需求灵活应用。另外Compose中组件都是可组合函数,自定义属性可以通过可组合函数参数传递,代码更加直观,使用起来更方便。
以上就是本文的全部内容啦,谢谢阅读。
引用
官方文档:自定义布局 | Jetpack Compose | Android Developers https://developer.android.com/jetpack/compose/layouts/custom?hl=zh-cn
也许你还想看
(▼点击文章标题或封面查看)
Kotlin攻略(深度解读版)
2021-04-22
源码分析-Kotlin中协程的挂起和恢复
2022-01-06
try catch finally机制分析
2022-08-04
子线程更新UI全解
2022-04-07
Flutter状态管理-Provider的使用和源码解析
2022-02-10