本文作者 / Android 谷歌開(kāi)發(fā)者專家王鵬
前言 - 從 Compose 生命周期說(shuō)起

Compose 繪制生命周期為三個(gè)階段:
Composition/組合: Composable 源碼經(jīng)過(guò)運(yùn)行后生成 LayoutNode 的節(jié)點(diǎn)樹(shù),這棵樹(shù)被稱為 Composition。
Layout/布局: 對(duì)節(jié)點(diǎn)樹(shù)深度遍歷測(cè)量子節(jié)點(diǎn)的尺寸,并將其在父容器內(nèi)擺放到合適的位置。
Drawing/繪制: 基于布局后拿到的尺寸和位置信息,繪制上屏。
我們與 Android 經(jīng)典視圖系統(tǒng)的生命周期 (Measure,Layout,Drawing) 做一個(gè)對(duì)比: 組合是 Compose 的特有階段,是其能夠通過(guò)函數(shù)調(diào)用實(shí)現(xiàn)聲明式 UI 的核心,想要深入理解 Compose 第一課就是理解這個(gè)過(guò)程。
繪制階段與傳統(tǒng)視圖大同小異,都是通過(guò) Android Cavas API,底層調(diào)用 skia 實(shí)現(xiàn)。
本文討論的重點(diǎn)是布局階段。Compose 的 Layout 把 Measure 也囊括了進(jìn)來(lái),相對(duì)于 Android View 有相似性,但也有其獨(dú)有的特點(diǎn)和優(yōu)勢(shì),接下來(lái)我們進(jìn)入正題。
Compose 布局過(guò)程三步走
Compose 布局包括三個(gè)階段,從當(dāng)前 Node 出發(fā),需要依次經(jīng)歷:
Measure children: 深度遍歷子節(jié)點(diǎn),并測(cè)量它們的尺寸
Decide own size: 根據(jù)收集到的子節(jié)點(diǎn)尺寸,決定當(dāng)前節(jié)點(diǎn)自己的尺寸
Place children: 將子節(jié)點(diǎn)擺放到合理的相對(duì)位置

上面代碼描述了一個(gè)卡片的布局,下面以這個(gè)布局的節(jié)點(diǎn)樹(shù)為例,看一下布局流程。

Step1: 從 Row 開(kāi)始發(fā)起測(cè)量,遵循三步走第一步,深度遍歷測(cè)量其子節(jié)點(diǎn) Image 和 Column
Step2&3: Image 發(fā)起測(cè)量,因?yàn)闆](méi)有子節(jié)點(diǎn)需要測(cè)量了,所以只需要計(jì)算自己的尺寸,也因?yàn)闆](méi)有子節(jié)點(diǎn)需要擺放,空實(shí)現(xiàn)完成 place 即可
Step4: Column 發(fā)起測(cè)量,因其有子節(jié)點(diǎn),繼續(xù)深度遍歷
Step5&6: 測(cè)量 Text,因?yàn)橐粋€(gè)葉子節(jié)點(diǎn),立即完成自己的 Size 和 Place 階段
Step7&8: 測(cè)量另一個(gè) Text,同上
Step9: Column 拿到兩個(gè)子 Text 返回的 Size 后,計(jì)算出自己的 Size,不難猜到其計(jì)算邏輯應(yīng)該是 width = maxOf(child1.w, child2.w),height = sumOf(child1.h, child2.h)。設(shè)置自己的 width 和 height 后,對(duì)兩個(gè)子 Text 進(jìn)行 Place,垂直線性擺放。
看一下代碼是如何實(shí)現(xiàn)這三步。
所有的 Composable 最終都會(huì)調(diào)用一個(gè)公共 Layout Composable 方法,這里面創(chuàng)建 LayoutNode 存儲(chǔ)在 Composition 節(jié)點(diǎn)樹(shù)。

以 Column 的實(shí)現(xiàn)為例,可以看到調(diào)用 Layout 時(shí),傳入了三個(gè)參數(shù):
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
content: 在這里定義子 Composable,組合過(guò)后形成當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)
measurePolicy: 這是定義了布局的三步走核心邏輯
modifier: 修飾符鏈,參與到布局或者繪制階段
measurePolicy 和 modifier 會(huì)存儲(chǔ)在當(dāng)前 LayoutNode 上,等待 measure 的開(kāi)始參與其中。下面重點(diǎn)分析 MeasurePolicy 了解三步走如何實(shí)現(xiàn)。
MeasurePolicy - 測(cè)量策略
fun interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List,
constraints: Constraints
): MeasureResult
}
MeasurePolicy 通過(guò) measure 方法完成測(cè)量。這里有兩個(gè)重要參數(shù):
measurables: 等待測(cè)量的對(duì)象,其實(shí)就是當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn)
constraints: 測(cè)量約束。節(jié)點(diǎn)需要基于當(dāng)前的 Constaints 進(jìn)行測(cè)量,它規(guī)定了節(jié)點(diǎn)尺寸的上限和下限,如下:
class Constraints {
val minWidth: Int
val maxWidth: Int
val minHeight: Int
val maxHeight: Int
...
}
Constraints - 測(cè)量約束
父節(jié)點(diǎn)通過(guò) Constraints 約束子節(jié)點(diǎn)的測(cè)量。Constraints 非常重要,我們常說(shuō) Compose 不怕布局嵌套正是得益于它。反觀 Android 原生視圖,由于測(cè)量階段的約束不明確,子 View 需要再次請(qǐng)求父 View 給出清楚的 View.MeasureSpec,導(dǎo)致出現(xiàn)多次繪制。
舉幾個(gè)例子理解一下 Constraints 如何設(shè)置:

對(duì)于頁(yè)面的根節(jié)點(diǎn), Activity 的 Window 的長(zhǎng)寬就是其 Constraints 的最大長(zhǎng)寬。如果是一個(gè)垂直可滾動(dòng)容器的節(jié)點(diǎn),那么它的 Constraints 的 height 應(yīng)該是 Infinity,因?yàn)樗梢钥缍鄠€(gè)屏幕存在。
此外, Modifier 的裝飾能力本質(zhì)也是通過(guò)修改 Constraints 完成的。例如 fillMaxWidth 要求被修飾的節(jié)點(diǎn)填充整個(gè)父容器,所以 Modifier 會(huì)在布局階段將 minHeight/minWidth 對(duì)齊 max 組值。關(guān)于 Modifier 參與布局的流程,稍后介紹。
三步走實(shí)現(xiàn) - Kotlin 語(yǔ)法優(yōu)勢(shì)的體現(xiàn)
舉例看一下三步走代碼如何實(shí)現(xiàn)。

我們實(shí)現(xiàn)一個(gè)類似 Column 的布局效果,在 measurePolicy#measure 中實(shí)現(xiàn)三步走邏輯。
measurePolicy = { // this: MeasureScope
// Step1:Measure each children
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// Step2: Deciee own size
val height = placeables.sumOf { it.height }
val width = placeables.maxOf { it.width }
layout(width, height) { //this: Placeable.PlacementScope
// Step3: Place children by changing the offset of y co-ord
var yPosition = 0
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
每個(gè) measuable 提供了參與測(cè)量的 measure 方法,此處會(huì)傳入 Constraints,返回的 placeable 中已經(jīng)存儲(chǔ)了測(cè)量后的 widht 和 height,等待 place
基于各個(gè) placeable 的 w 和 h 計(jì)算當(dāng)前節(jié)點(diǎn)的 Size,并通過(guò) layout 方法設(shè)置。layout 方法內(nèi)會(huì)真正的創(chuàng)建 LayoutNode
layout 方法的末參是一個(gè) lambda,這里是第三步擺放子節(jié)點(diǎn)的邏輯,通過(guò)設(shè)置 y 軸的偏移量實(shí)現(xiàn)縱向布局,非常簡(jiǎn)單
特別值得一提的是,通過(guò) meause 一個(gè)方法就完成三步走,布局邏輯相對(duì)傳統(tǒng)的 View 系統(tǒng)更加高效,回想傳統(tǒng)自定義 View 你需要分別實(shí)現(xiàn) onMeasure,onLayout,onDraw 等,邏輯分散,可讀性差。
但是這種集中式的寫(xiě)法有一個(gè)弊端,需要人為保證代碼順序。試想如果把 layout 寫(xiě)在 measure 前面怎么辦?幸好 Kotlin 強(qiáng)大的編譯期檢查能力,很好地指導(dǎo)大家寫(xiě)出正確代碼:
measure 方法的返回值是 MeasureResult 類型,layout 方法也返回此類型,所以保證了尾部一定是調(diào)用 layout 完成三步走
Measuable#measure 調(diào)用后返回 Placeable 類型,然后才能調(diào)用 Placeable#place,這保證了 place 和 measure 的先后關(guān)系
Measuable#measure 只能在 MeasureScope 中調(diào)用,Placeable#place 只能在 Placeable.PlacementScope 中調(diào)用,這確保了 place 需要在 layout 的 lambda 中調(diào)用
通過(guò)各種返回值類型、作用域類型的約束,大家可以寫(xiě)出安全又一氣呵成的代碼,這種 API 設(shè)計(jì)理念值得推崇。
Modifier Node
接下來(lái)介紹一下 Modifier 如何參與布局的。

Modifier 在組合之后也會(huì)成為 Node 存儲(chǔ)在節(jié)點(diǎn)樹(shù)上,Modifier 的調(diào)用鏈生成一條單向繼承的子節(jié)點(diǎn)樹(shù),而被修飾的 Composable 會(huì)成為這條樹(shù)枝的葉子結(jié)點(diǎn)。
比如上面例子中,Image 最終成為 clip->size 的子節(jié)點(diǎn)。實(shí)際上 Image 內(nèi)部有一些內(nèi)置的 Modifier,所以全部展開(kāi)后 Image 所在的樹(shù)枝上有一連串 ModifierNode。
掛在節(jié)點(diǎn)樹(shù)上的 ModifierNode 可以參與到深度遍歷的繪制流程中,在 Image 之前對(duì) Constraints 做出調(diào)整,完成對(duì)末端 Image 的裝飾。
以 Padding 修飾符為例,看一下源碼:
//組合中調(diào)用 paddiung 會(huì)
fun Modifier.padding(
start: Dp = 0.dp,
top: Dp = 0.dp,
end: Dp = 0.dp,
bottom: Dp = 0.dp
) = this then PaddingElement(
start = start,
top = top,
end = end,
bottom = bottom
)
//Element 存儲(chǔ)到鏈上,創(chuàng)建 PaddingNode
private class PaddingElement(
...
) : ModifierNodeElement()
//PaddingNode 定義 measure 邏輯
private class PaddingNode(
overide fun MeasureScope.measure(
measurable: Measurable, // 注意不是list
constraints: Constraints
): MeasureResult {
...
}
):LayoutModifierNode,Modifier.Node()
組合階段,Modifier#then 創(chuàng)建 Element 加入 Modifier chain 中。Element 是無(wú)狀態(tài)的,重組中會(huì)重新生成,Element 會(huì)在組合中創(chuàng)建有狀態(tài)的 ModifierNode。ModifierNode 有狀態(tài),重組中僅當(dāng)狀態(tài)發(fā)生變化時(shí)被更新,否則不會(huì)重新生成。Modifier Node 是 Compose 1.5 引入的新優(yōu)化,目的就是通過(guò)存儲(chǔ) Modifier 狀態(tài)參與比較,提升重組性能。
ModifierNode 按照參與的階段不同,分為 LayoutModifierNode 和 DrawModifierNode。對(duì)于前者,布局邏輯就是現(xiàn)在 LayoutModifierNode#measure 中,和 MeasurePolicy#measure 的功能一樣,唯一的區(qū)別是接受單個(gè) measurable 參數(shù)而不是 List。因?yàn)槲覀冎懒?ModifierNode 是單向繼承,所以只會(huì)有一個(gè)后續(xù)子節(jié)點(diǎn)。如果把LayoutNode 的 measure 看做是自定義 ViewGroup 需要針對(duì)多個(gè)子 View 布局,那么 LayoutModifierNode 的 measure 更像是自定義 View,只對(duì)自身負(fù)責(zé)。
Modifier.layout {}
除了自定義一個(gè) Modifier 來(lái)改變當(dāng)前節(jié)點(diǎn)的布局,還有一個(gè)簡(jiǎn)單的方法就是使用 Modifier.layout {} 方法。
fun Modifier.layout(
measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
)
我們可以在 Modifier 調(diào)用鏈的任意位置插入 measure 自定義代碼,對(duì)當(dāng)前節(jié)點(diǎn)做裝飾。例如下面代碼中添加了一個(gè)自定義 50px 的 padding。
Box(Modifier
.background(Color.Gray)
.layout { measurable, constraints ->
// an example modifier that adds 50 pixels of vertical padding
val padding = 50
val placeable = measurable.measure(constraints.offset(vertical = -padding))
layout(placeable.width, placeable.height + padding) {
placeable.placeRelative(0, padding)
}
}){ ... }
Modifier 布局流程

上面代碼繪制一個(gè)居中擺放 50*50 的矩形。我們通常不會(huì)同時(shí)設(shè)置這么多 size 相關(guān)的 modifier,這個(gè)例子只是為了展示 Modifier 的布局流程:

先看一下自頂向下的測(cè)量流程: 從 fillMaxSize 對(duì)應(yīng)的 LayoutModifierNode 出發(fā),假設(shè)當(dāng)前的 Constraints 是 w:0-200,h:0-300。fillMaxSize 的功能是讓子節(jié)點(diǎn)填滿當(dāng)前全部剩余空間,會(huì)為子節(jié)點(diǎn)創(chuàng)建以下 childConstraints:
val childConstraints = Constraints (
minWidth = outerConstraints.maxWidth,
maxWidth = outerConstraints.maxWidth,
minHeight = outerConstraints.maxHeight,
maxHeight = outerConstraints.maxHeight,
)
來(lái)到 warpContentSize,它會(huì)讓子自己決定 size 不設(shè)限,min 值再次回歸 0,childConstraints 如下:
val childConstraints = Constraints (
minWidth = 0,
maxWidth = outerConstraints.maxWidth,
minHeight = 0,
maxHeight = outerConstraints.maxHeight,
)
來(lái)到 size(50),這里自然要給一個(gè)具體的 size 約束,如下:
val childConstraints = Constraints (
minWidth = 50,
maxWidth = 50,
minHeight = 50,
maxHeight = 50,
)
以此類推 Constraints 經(jīng)過(guò)不斷調(diào)整傳入到葉子節(jié)點(diǎn) Box 對(duì)應(yīng)的 LayoutNode,完成三步走。 第一步測(cè)量

葉子節(jié)點(diǎn)測(cè)量完后,再自底向上進(jìn)行第二三步,整個(gè)流程不做贅述了,只提一點(diǎn): wrapContentSize 從語(yǔ)義上是應(yīng)該跟隨子節(jié)點(diǎn)的大小,即 5050,為什么實(shí)際尺寸設(shè)置了 200300 呢?
因?yàn)槠涓腹?jié)點(diǎn) fillMaxSize 傳入的 Constraints 是 200300,rwapContentSize 必須填滿這個(gè)空間,而由于它有一個(gè)默認(rèn)參數(shù) align = Alignment.Center,所以才能出現(xiàn) 5050 矩形塊居中的效果。
Intrinsic Measurements - 固有特性測(cè)量
中文將其翻譯成 "固有特性",很多人不理解 "固有" 到底指什么?所以放在本文最后討論一下。
Compose 要求布局過(guò)程中每個(gè)節(jié)點(diǎn)只被測(cè)量一次,測(cè)量總耗時(shí)只與節(jié)點(diǎn)數(shù)正相關(guān),與層級(jí)無(wú)關(guān),所以 ComopseUI 不怕嵌套過(guò)深,而傳統(tǒng) Android 視圖系統(tǒng)中,某個(gè) View 存在多次測(cè)量的情況,隨著層級(jí)變多測(cè)量次數(shù)會(huì)指數(shù)級(jí)增長(zhǎng),所以傳圖視圖下我們需要通過(guò)優(yōu)化 View 的層級(jí)提升性能。
Compose 為了保證 "每個(gè)節(jié)點(diǎn)只測(cè)量一次" 的原則,甚至增加了編譯期檢查:
val constraints1 = ... val constraints2 = ... val placeable1 = measurable.measure(constraints1 val placeable2 = measurable.measure(constraints2)

"每個(gè)節(jié)點(diǎn)只測(cè)量一次"在提升性能的同時(shí)也帶來(lái)了問(wèn)題。來(lái)自官方文檔的例子:
@Composable
fun TwoTexts(modifier: Modifier = Modifier, text1: String, text2: String) {
Row(modifier = modifier) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp)
.wrapContentWidth(Alignment.Start),
text = text1
)
Divider(
color = Color.Black,
modifier = Modifier.fillMaxHeight().width(1.dp)
)
Text(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp)
.wrapContentWidth(Alignment.End),
text = text2
)
}
}
上面代碼的本意是希望打造以下的布局效果:
但發(fā)現(xiàn)實(shí)際效果不符合預(yù)期: Divider 的高度沒(méi)有對(duì)齊左右的 Text,而是撐滿了容器高度:
Row 為測(cè)量 Divider 傳入Constraints 時(shí),不知道對(duì)齊 Text 高度應(yīng)該設(shè)置怎樣的 maxHeight。傳入的 maxHeight 值比較大導(dǎo)致 Divider 的 fillMaxSize 撐滿了整個(gè)容器。 傳統(tǒng)視圖體系中類似的情況,Row 在測(cè)量了 Text 的高度后,會(huì)再測(cè)量一次 Divider 并給出更合適的 View.MeasureSpec,但 Compose 中不可以,因?yàn)檫@樣違反了 "每個(gè)節(jié)點(diǎn)只測(cè)量一次"的原則。
為此, Compose 引入了 "固有特性測(cè)量" 的機(jī)制。在當(dāng)前節(jié)點(diǎn)正式發(fā)起深度遍歷子測(cè)量節(jié)點(diǎn)之前的一次 "預(yù)處理",從子節(jié)點(diǎn)提前獲取必要信息,設(shè)置更合理的 Constraints,然后再發(fā)起正式測(cè)量。 MeasurePolicy 中提供了獲取 "固有特性" 尺寸的方法: IntrinsicMeasureScope.minIntrinsicXXX
fun interface MeasurePolicy {
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List,
height: Int
): Int
fun IntrinsicMeasureScope.minIntrinsicHeight
fun IntrinsicMeasureScope.maxIntrinsicWidth
fun IntrinsicMeasureScope.maxIntrinsicHeight
}
Text 的固有特性的 minIntrinsicHeight 是文本內(nèi)容單行展示的高度;Divider 的 minIntrinsicHeight 是 0,當(dāng)我們改一下例子中的代碼,在 Row 的Modifier.height 增加 IntrinsicSize.Min。
Row(modifier = modifier.height(IntrinsicSize.Min)) {...}
Row 在發(fā)起子節(jié)點(diǎn)測(cè)量前,通過(guò) MeasurePolicy 提供的固有特性相關(guān)方法,獲取所有子節(jié)點(diǎn)的minIntrinsicHeight,取最大的一個(gè)設(shè)為 Constraints.maxHeight 后發(fā)起正式測(cè)量。這樣,Divider 的 fillMaxSize 就會(huì)跟 Text 兩邊高度對(duì)齊了。
看到這里相信大家理解 "固有"的含義了,其本質(zhì)代表 "不依賴 Constraints"就可以獲取的值,基于這些值更新 Constraints,后續(xù)測(cè)量只有一次也能正確約束。
-
Android
+關(guān)注
關(guān)注
12文章
4028瀏覽量
134058 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4417瀏覽量
67568 -
代碼
+關(guān)注
關(guān)注
30文章
4970瀏覽量
74019
原文標(biāo)題:【GDE 分享】一文看懂 Jetpack Compose 布局流程
文章出處:【微信號(hào):Google_Developers,微信公眾號(hào):谷歌開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
詳解Jetpack Compose 1.1版本的新功能
如何使用 Compose 進(jìn)行構(gòu)建
Jetpack Compose基礎(chǔ)知識(shí)科普
Android Studio Dolphin穩(wěn)定版正式發(fā)布
Compose Material 3 穩(wěn)定版現(xiàn)已發(fā)布 | 2022 Android 開(kāi)發(fā)者峰會(huì)
Jetpack Compose 更新一覽 | 2022 Android 開(kāi)發(fā)者峰會(huì)
Google計(jì)劃用Jetpack Compose來(lái)重建Android系統(tǒng)中的設(shè)置應(yīng)用
Compose for Wear OS 1.1 推出穩(wěn)定版: 了解新功能!
Kotlin聲明式UI框架Compose Multiplatform支持iOS
詳解Jetpack Compose布局流程
評(píng)論