本文字数:10105字
预计阅读时间:26分钟
前言
最近在使用Jetpack Compose进行应用开发,不得不说相比较命令式布局方式Xml,Compose声明式UI真香。然而仅使用基础的Compose控件在面对UI大佬的设计图时,总会出现一些“小问题”,包括部分页面不能通过标准布局实现、需要特别精准的控制测量和放置的场景、需要实现布局动画等,这个时候往往需要通过自定义布局去解决这些“小问题”,来达到项目验收标准。既然是要自定义组件,那么首先了解一下Compose的布局模型吧,从布局模型中找到自定义组件的切入点。
布局模型
Compose布局模型可以分为三个部分 组合、布局、绘制
![f2b68b948da13d65ec7d6f8d7635d52d.jpeg](https://i-blog.csdnimg.cn/blog_migrate/db9167b14215d4856c3fb68645ba8818.jpeg)
首先组合阶段会执行可组合函数,从而创建界面树,举个例子
![10e8a94473b676c1466955f65b1ad36b.jpeg](https://i-blog.csdnimg.cn/blog_migrate/b2c1da1befc45ce2fd4fad156a01e144.jpeg)
上图就是按照 SearchResult 组合函数内部子组件顺序生成界面树的示例。日常开发中可组合函数内部可能会包含各种状态逻辑和控制流,反映到组合阶段就是根据不同的逻辑控制来生成不同的界面树。
接下来的 Layout(布局) 阶段会遍历界面树,通过测量组件以确定自身大小以及在手机上应该摆放的位置,这点和View视图的 onMeasure 和 onLayout 类似,都是先测量然后放置,但是也有不一样的地方,View视图可以经过多次测量来确定子视图的大小和位置信息,但是在Compose中每个组件只允许测量一次,多次测量会报错,正是这一特性可以让我们在使用过程中任意嵌套组件而不会造成很大的性能问题。
最后的 Draw(绘制) 阶段会再次遍历界面树,这个阶段主要做的事情就是把组件绘制到界面上。主要涉及 Canvas 操作,Compose 中画布操作和Android中画布操作大同小异,函数式风格的API使用起来会更方便,虽然有些画布操作 Compose 没有支持,但我们也可以通过 drawIntoCanvas 方法拿到原生Android 环境下的 Canvas 和 Paint 对象进行绘制。这个阶段本文不做深入探讨。
在了解了Compose的布局三个阶段之后,要想自定义组件,切入点还是得从 Layout(布局) 阶段开始,本文主要对该阶段进行深入探讨。
Layout
阶段主要涉及了三个步骤:
测量所有子项
确定自己的尺寸
放置其子项
![382f7acb3b8283e1ec660ad50a156ec1.jpeg](https://i-blog.csdnimg.cn/blog_migrate/deb11c55e3565c6a5404ee4e6055c82f.jpeg)
这个看起来跟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 max