Compose 是一个声明式的 UI 框架,提供了很多开箱即用的组件,比如 Text()、Button、Image() 等等,Compose 会经过几个不同的阶段,最终渲染出 UI 界面。
此转换过程分为【三个阶段】:
- 组合: 显示什么
- 布局: 放在哪里
- 绘制: 如何渲染
这三个阶段是逐一执行的,流程如下:
组合阶段
在组合阶段,Compose 运行时会执行代码中定义的可组合函数,最终会生成一棵视图树。这个视图树由一个个布局节点(LayoutNode)组成。比如 Text()、Button() 都对应一个 LayoutNode,这些 LayoutNode 持有组件的所有信息。
更形象一点的结构如下:
这是一个非常简单的示例,但有时候我们定义的可组合项包含逻辑和控制流,而 Compose 会在不同状态的情况下生成不同的树。
布局阶段
布局阶段,对于视图树中的每个 LayoutNode 节点进行宽高尺寸测量并完成位置摆放,布局元素都会根据 2D 坐标来测量并放置自己及其所有子元素。
其实 Compose 的布局阶段和传统的 View 系统很像(测量、布局、绘制),唯独多了一个“组合阶段”,而 Compose 把 测量和布局 统一放入了“布局阶段”。
在布局阶段,使用以下 3 步算法遍历 LayoutNode 树:
- 测量子节点: 每一个节点会测量它的子节点,如果有的话。
- 决定自己的大小: 基于这些测量,节点决定自己的大小。
- 放置子节点: 每个子节点都相对于节点自身的位置进行放置。
布局阶段结束后,每个 LayoutNode 都将分配一个 宽度 和 高度,以及一个应该绘制的 x、y 坐标。
现在我们分析下面这个简单示例的布局流程。
🔵 Row 测量其子项。
🟢 首先,测量 Image。它没有任何子项,因此它决定自己的大小并将其报告给 Row。
🟢 其次,测量 Column。它需要先测量自己的子项。
🟠 第一个 Text 被测量。它没有任何子项,因此它决定自己的大小并将其报告给 Column。
🟠 第二个 Text 被测量。它没有任何子项,因此它决定自己的大小并将其报告给 Column。
🟢 Column 使用子测量值来决定自己的大小。它使用最大子宽度及其子项的高度之和。
🟢 Column 将其子项放置在相对于自身的位置,将它们垂直放置在彼此下方。
🔵 Row 使用子尺寸来决定自己的尺寸。它使用最大子高度和子宽度的总和。然后它放置它的子项。
看到这,是不是发现个很牛逼的事? 我们只访问了每个节点一次。通过视图树的一次遍历,我们就可以测量和放置所有节点。这对性能来说就很重要了!当树中节点的数量增加时,遍历它所花费的时间只会以线性方式增加。相比之下,如果我们多次访问每个节点,遍历时间则会以呈指数级增加。
绘制阶段
绘制阶段,树中的每个节点都在屏幕上绘制其像素。
上面我们说过,在布局阶段结束后,所有布局节点会得到它们的 宽度 和 高度,以及 x、y 坐标。所以现在就可以进入绘制阶段了。
绘制阶段会从上到下再次遍历树,每个节点依次在屏幕上绘制自己。
首先 Row 将绘制它可能具有的任何内容,例如背景色。然后 Image 将绘制自己,然后是 Column,然后是第一个和第二个 Text。
Modifier 修饰符
上面我们给的简单代码示例都只是用了一些 Compose 提供给我们的现场的组件,实际开发过程中,会有一个大神级别一样的修饰符随处可见,它就是:Modifier 修饰符。
比如:Modifier.padding 是用来给组件设置边距的,它本质上是一个 LayoutModifier,而 LayoutModifier 会影响组件的测量和布局效果,会影响到组合项的整体 UI 效果。具体如何影响,这篇文章我们不讲它的深层次原理,而只是探讨思维模型。
所以,如果你想了解 LayoutModifier 的原理,可以阅读 【 聊聊 Jetpack Compose 原理 – LayoutModifier 和 Modifier.layout 】 这篇文章。
回到正题,如果我们加了 Modifier 修饰符,那么在最终生成的视图树中,可以将 Modifier 修饰符可视化为布局节点的包装节点:
当我们链接多个修饰符时,每个修饰符节点包裹链的其余部分和其中的布局节点。 例如,当我们链接一个 clip 和一个 size 修饰符时,clip 修饰符节点包裹 size 修饰符节点,然后包裹 Image 布局节点。
这里你可能有疑问,为什么不是先 clip 然后 size,而是先 size 然后在 clip?看完 【 聊聊 Jetpack Compose 原理 – LayoutModifier 和 Modifier.layout 】 这篇文章你就懂了。
接着说,在布局阶段,我们用来遍历树的算法保持不变,但 每个修饰符节点也会被访问。这样,修饰符可以更改其包裹的 修饰符 或 布局节点 的 大小要求 和 位置。
如果我们看一下 Image 可组合项的实现(底层更细的实现),实际上可以看到它本身由包裹单个布局节点的修饰符链组成。类似地,Text 可组合项也是通过包含布局节点的修饰符链实现的。最后,Row 和 Column 的实现只是描述如何布置其子节点的布局节点:
看不懂这张图?没事,这些细节原理都会有文章输出,比如我刚刚提到了两次的原理文章。