那曲檬骨新材料有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

詳解Jetpack Compose布局流程

谷歌開發者 ? 來源:AndroidPub ? 2025-02-05 13:38 ? 次閱讀

本文作者 / Android 谷歌開發者專家王鵬

前言 - 從 Compose 生命周期說起

ba236dbc-da36-11ef-9310-92fbcf53809c.png

Compose 繪制生命周期為三個階段:

Composition/組合: Composable 源碼經過運行后生成 LayoutNode 的節點樹,這棵樹被稱為 Composition。

Layout/布局: 對節點樹深度遍歷測量子節點的尺寸,并將其在父容器內擺放到合適的位置。

Drawing/繪制: 基于布局后拿到的尺寸和位置信息,繪制上屏。

我們與 Android 經典視圖系統的生命周期 (Measure,Layout,Drawing) 做一個對比: 組合是 Compose 的特有階段,是其能夠通過函數調用實現聲明式 UI 的核心,想要深入理解 Compose 第一課就是理解這個過程。

繪制階段與傳統視圖大同小異,都是通過 Android Cavas API,底層調用 skia 實現。

本文討論的重點是布局階段。Compose 的 Layout 把 Measure 也囊括了進來,相對于 Android View 有相似性,但也有其獨有的特點和優勢,接下來我們進入正題。

Compose 布局過程三步走

Compose 布局包括三個階段,從當前 Node 出發,需要依次經歷:

Measure children: 深度遍歷子節點,并測量它們的尺寸

Decide own size: 根據收集到的子節點尺寸,決定當前節點自己的尺寸

Place children: 將子節點擺放到合理的相對位置

wKgZO2ei-d-ASRvkAACju1W21oo095.png

上面代碼描述了一個卡片的布局,下面以這個布局的節點樹為例,看一下布局流程。

ba53930c-da36-11ef-9310-92fbcf53809c.png

Step1: 從 Row 開始發起測量,遵循三步走第一步,深度遍歷測量其子節點 Image 和 Column

Step2&3: Image 發起測量,因為沒有子節點需要測量了,所以只需要計算自己的尺寸,也因為沒有子節點需要擺放,空實現完成 place 即可

Step4: Column 發起測量,因其有子節點,繼續深度遍歷

Step5&6: 測量 Text,因為一個葉子節點,立即完成自己的 Size 和 Place 階段

Step7&8: 測量另一個 Text,同上

Step9: Column 拿到兩個子 Text 返回的 Size 后,計算出自己的 Size,不難猜到其計算邏輯應該是 width = maxOf(child1.w, child2.w),height = sumOf(child1.h, child2.h)。設置自己的 width 和 height 后,對兩個子 Text 進行 Place,垂直線性擺放。

看一下代碼是如何實現這三步。

所有的 Composable 最終都會調用一個公共 Layout Composable 方法,這里面創建 LayoutNode 存儲在 Composition 節點樹。

ba6b6c02-da36-11ef-9310-92fbcf53809c.png

以 Column 的實現為例,可以看到調用 Layout 時,傳入了三個參數:

@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,組合過后形成當前節點的子節點

measurePolicy: 這是定義了布局的三步走核心邏輯

modifier: 修飾符鏈,參與到布局或者繪制階段

measurePolicy 和 modifier 會存儲在當前 LayoutNode 上,等待 measure 的開始參與其中。下面重點分析 MeasurePolicy 了解三步走如何實現。

MeasurePolicy - 測量策略

fun interface MeasurePolicy {


    fun MeasureScope.measure(
        measurables: List,
        constraints: Constraints
    ): MeasureResult


}

MeasurePolicy 通過 measure 方法完成測量。這里有兩個重要參數:

measurables: 等待測量的對象,其實就是當前節點的子節點

constraints: 測量約束。節點需要基于當前的 Constaints 進行測量,它規定了節點尺寸的上限和下限,如下:

class Constraints {
    val minWidth: Int
    val maxWidth: Int
    val minHeight: Int
    val maxHeight: Int
    ...
}

Constraints - 測量約束

父節點通過 Constraints 約束子節點的測量。Constraints 非常重要,我們常說 Compose 不怕布局嵌套正是得益于它。反觀 Android 原生視圖,由于測量階段的約束不明確,子 View 需要再次請求父 View 給出清楚的 View.MeasureSpec,導致出現多次繪制。

舉幾個例子理解一下 Constraints 如何設置:

ba8aefbe-da36-11ef-9310-92fbcf53809c.png

對于頁面的根節點, Activity 的 Window 的長寬就是其 Constraints 的最大長寬。如果是一個垂直可滾動容器的節點,那么它的 Constraints 的 height 應該是 Infinity,因為它可以跨多個屏幕存在。

此外, Modifier 的裝飾能力本質也是通過修改 Constraints 完成的。例如 fillMaxWidth 要求被修飾的節點填充整個父容器,所以 Modifier 會在布局階段將 minHeight/minWidth 對齊 max 組值。關于 Modifier 參與布局的流程,稍后介紹。

三步走實現 - Kotlin 語法優勢的體現

舉例看一下三步走代碼如何實現。

baa449f0-da36-11ef-9310-92fbcf53809c.png

我們實現一個類似 Column 的布局效果,在 measurePolicy#measure 中實現三步走邏輯。

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
        }
    }
}

每個 measuable 提供了參與測量的 measure 方法,此處會傳入 Constraints,返回的 placeable 中已經存儲了測量后的 widht 和 height,等待 place

基于各個 placeable 的 w 和 h 計算當前節點的 Size,并通過 layout 方法設置。layout 方法內會真正的創建 LayoutNode

layout 方法的末參是一個 lambda,這里是第三步擺放子節點的邏輯,通過設置 y 軸的偏移量實現縱向布局,非常簡單

特別值得一提的是,通過 meause 一個方法就完成三步走,布局邏輯相對傳統的 View 系統更加高效,回想傳統自定義 View 你需要分別實現 onMeasure,onLayout,onDraw 等,邏輯分散,可讀性差。

但是這種集中式的寫法有一個弊端,需要人為保證代碼順序。試想如果把 layout 寫在 measure 前面怎么辦?幸好 Kotlin 強大的編譯期檢查能力,很好地指導大家寫出正確代碼:

measure 方法的返回值是 MeasureResult 類型,layout 方法也返回此類型,所以保證了尾部一定是調用 layout 完成三步走

Measuable#measure 調用后返回 Placeable 類型,然后才能調用 Placeable#place,這保證了 place 和 measure 的先后關系

Measuable#measure 只能在 MeasureScope 中調用,Placeable#place 只能在 Placeable.PlacementScope 中調用,這確保了 place 需要在 layout 的 lambda 中調用

通過各種返回值類型、作用域類型的約束,大家可以寫出安全又一氣呵成的代碼,這種 API 設計理念值得推崇。

Modifier Node

接下來介紹一下 Modifier 如何參與布局的。

bab8a40e-da36-11ef-9310-92fbcf53809c.png

Modifier 在組合之后也會成為 Node 存儲在節點樹上,Modifier 的調用鏈生成一條單向繼承的子節點樹,而被修飾的 Composable 會成為這條樹枝的葉子結點。

比如上面例子中,Image 最終成為 clip->size 的子節點。實際上 Image 內部有一些內置的 Modifier,所以全部展開后 Image 所在的樹枝上有一連串 ModifierNode。

掛在節點樹上的 ModifierNode 可以參與到深度遍歷的繪制流程中,在 Image 之前對 Constraints 做出調整,完成對末端 Image 的裝飾。

以 Padding 修飾符為例,看一下源碼:

//組合中調用 paddiung 會
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 存儲到鏈上,創建 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 創建 Element 加入 Modifier chain 中。Element 是無狀態的,重組中會重新生成,Element 會在組合中創建有狀態的 ModifierNode。ModifierNode 有狀態,重組中僅當狀態發生變化時被更新,否則不會重新生成。Modifier Node 是 Compose 1.5 引入的新優化,目的就是通過存儲 Modifier 狀態參與比較,提升重組性能。

ModifierNode 按照參與的階段不同,分為 LayoutModifierNode 和 DrawModifierNode。對于前者,布局邏輯就是現在 LayoutModifierNode#measure 中,和 MeasurePolicy#measure 的功能一樣,唯一的區別是接受單個 measurable 參數而不是 List。因為我們知道了 ModifierNode 是單向繼承,所以只會有一個后續子節點。如果把LayoutNode 的 measure 看做是自定義 ViewGroup 需要針對多個子 View 布局,那么 LayoutModifierNode 的 measure 更像是自定義 View,只對自身負責。

Modifier.layout {}

除了自定義一個 Modifier 來改變當前節點的布局,還有一個簡單的方法就是使用 Modifier.layout {} 方法。

fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
)
我們可以在 Modifier 調用鏈的任意位置插入 measure 自定義代碼,對當前節點做裝飾。例如下面代碼中添加了一個自定義 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 布局流程

bade81ec-da36-11ef-9310-92fbcf53809c.png

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

bafa741a-da36-11ef-9310-92fbcf53809c.png

先看一下自頂向下的測量流程: 從 fillMaxSize 對應的 LayoutModifierNode 出發,假設當前的 Constraints 是 w:0-200,h:0-300。fillMaxSize 的功能是讓子節點填滿當前全部剩余空間,會為子節點創建以下 childConstraints:

val childConstraints = Constraints (
    minWidth = outerConstraints.maxWidth,
    maxWidth = outerConstraints.maxWidth,
    minHeight = outerConstraints.maxHeight,
    maxHeight = outerConstraints.maxHeight,
)
來到 warpContentSize,它會讓子自己決定 size 不設限,min 值再次回歸 0,childConstraints 如下:
val childConstraints = Constraints (
    minWidth = 0,
    maxWidth = outerConstraints.maxWidth,
    minHeight = 0,
    maxHeight = outerConstraints.maxHeight,
)
來到 size(50),這里自然要給一個具體的 size 約束,如下:
val childConstraints = Constraints (
    minWidth = 50,
    maxWidth = 50,
    minHeight = 50,
    maxHeight = 50,
)
以此類推 Constraints 經過不斷調整傳入到葉子節點 Box 對應的 LayoutNode,完成三步走。 第一步測量

bb16c8cc-da36-11ef-9310-92fbcf53809c.png

葉子節點測量完后,再自底向上進行第二三步,整個流程不做贅述了,只提一點: wrapContentSize 從語義上是應該跟隨子節點的大小,即 5050,為什么實際尺寸設置了 200300 呢?

因為其父節點 fillMaxSize 傳入的 Constraints 是 200300,rwapContentSize 必須填滿這個空間,而由于它有一個默認參數 align = Alignment.Center,所以才能出現 5050 矩形塊居中的效果。

Intrinsic Measurements - 固有特性測量

中文將其翻譯成 "固有特性",很多人不理解 "固有" 到底指什么?所以放在本文最后討論一下。

Compose 要求布局過程中每個節點只被測量一次,測量總耗時只與節點數正相關,與層級無關,所以 ComopseUI 不怕嵌套過深,而傳統 Android 視圖系統中,某個 View 存在多次測量的情況,隨著層級變多測量次數會指數級增長,所以傳圖視圖下我們需要通過優化 View 的層級提升性能。

Compose 為了保證 "每個節點只測量一次" 的原則,甚至增加了編譯期檢查:

val constraints1 = ...
val constraints2 = ...
val placeable1 = measurable.measure(constraints1
val placeable2 = measurable.measure(constraints2)

bb280286-da36-11ef-9310-92fbcf53809c.png

"每個節點只測量一次"在提升性能的同時也帶來了問題。來自官方文檔的例子:

@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
        )
    }
}
上面代碼的本意是希望打造以下的布局效果:

但發現實際效果不符合預期: Divider 的高度沒有對齊左右的 Text,而是撐滿了容器高度:

Row 為測量 Divider 傳入Constraints 時,不知道對齊 Text 高度應該設置怎樣的 maxHeight。傳入的 maxHeight 值比較大導致 Divider 的 fillMaxSize 撐滿了整個容器。 傳統視圖體系中類似的情況,Row 在測量了 Text 的高度后,會再測量一次 Divider 并給出更合適的 View.MeasureSpec,但 Compose 中不可以,因為這樣違反了 "每個節點只測量一次"的原則。

為此, Compose 引入了 "固有特性測量" 的機制。在當前節點正式發起深度遍歷子測量節點之前的一次 "預處理",從子節點提前獲取必要信息,設置更合理的 Constraints,然后再發起正式測量。 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 是文本內容單行展示的高度;Divider 的 minIntrinsicHeight 是 0,當我們改一下例子中的代碼,在 Row 的Modifier.height 增加 IntrinsicSize.Min。
Row(modifier = modifier.height(IntrinsicSize.Min)) {...}
Row 在發起子節點測量前,通過 MeasurePolicy 提供的固有特性相關方法,獲取所有子節點的minIntrinsicHeight,取最大的一個設為 Constraints.maxHeight 后發起正式測量。這樣,Divider 的 fillMaxSize 就會跟 Text 兩邊高度對齊了。

看到這里相信大家理解 "固有"的含義了,其本質代表 "不依賴 Constraints"就可以獲取的值,基于這些值更新 Constraints,后續測量只有一次也能正確約束。

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Android
    +關注

    關注

    12

    文章

    3945

    瀏覽量

    127924
  • 函數
    +關注

    關注

    3

    文章

    4346

    瀏覽量

    62968
  • 代碼
    +關注

    關注

    30

    文章

    4825

    瀏覽量

    69035

原文標題:【GDE 分享】一文看懂 Jetpack Compose 布局流程

文章出處:【微信號:Google_Developers,微信公眾號:谷歌開發者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    Jetpack Compose 的基本布局

    mcu
    橙群微電子
    發布于 :2024年05月21日 15:48:29

    PCB工藝流程詳解

    PCB工藝流程詳解PCB工藝流程詳解
    發表于 05-22 14:46

    compose的使用技巧是什么?

    compose的使用技巧是什么?
    發表于 11-15 07:27

    主板的走線和布局設計詳解

    主板的走線和布局設計詳解
    發表于 01-17 19:47 ?0次下載

    詳解Jetpack Compose 1.1版本的新功能

    我們一如既往地搭建產品路線圖,現在已經發布了 Jetpack Compose 的 1.1 版本,這是 Android 的現代原生界面工具包。此版本新增了一些功能,比如經過優化的焦點處理、觸摸目標值
    的頭像 發表于 03-11 10:14 ?1475次閱讀

    如何使用 Compose 進行構建

    適用于 Wear OS 的 Compose 已推出了開發者預覽版,使用 Compose 構建 Wear OS 應用,不僅可以輕松遵循 Material You 指南,同時可以將 Compose 的優點發揮出來。
    的頭像 發表于 03-17 13:44 ?1806次閱讀

    Jetpack Compose基礎知識科普

    Jetpack Compose 是用于構建原生 Android 界面的新工具包。它可簡化并加快 Android 上的界面開發,使用更少的代碼、強大的工具和直觀的 Kotlin API,快速讓應用生動
    的頭像 發表于 04-02 13:38 ?3027次閱讀

    Android Studio Dolphin穩定版正式發布

    預覽動畫。此外,針對應用界面調試,我們還在布局檢查器 (Layout Inspector) 中引入了一個很好用的 Compose 界面計數工具,用以跟蹤界面重新組合的次數。 Jetpack C
    的頭像 發表于 10-12 19:37 ?2539次閱讀

    Compose Material 3 穩定版現已發布 | 2022 Android 開發者峰會

    (新一代 Material Design ) 構建 Jetpack Compose 界面。立即開始在應用中使用 Material Design 3 吧! Compose Material 3
    的頭像 發表于 11-21 18:10 ?1262次閱讀

    Jetpack Compose 更新一覽 | 2022 Android 開發者峰會

    作者 /?Android 開發者關系工程師 Jolanda Verhoef 去年我們發布了 Jetpack Compose ,此后一直在進行優化。我們已添加了新的功能并創造出功能更強大的工具,幫助
    的頭像 發表于 11-23 17:55 ?1182次閱讀

    Google計劃用Jetpack Compose來重建Android系統中的設置應用

    上周,Google 發布了 Android 14 的首個開發者預覽版,除了那些最新的功能以外,Google 似乎還正在默默醞釀一個新的計劃 —— 用更現代的 Jetpack Compose 來逐步
    的頭像 發表于 02-18 11:16 ?1699次閱讀

    Compose for Wear OS 1.1 推出穩定版: 了解新功能!

    為 Wear OS 構建出色的響應式應用。 ? Compose for Wear OS?1.1 版本 https://developer.android.google.cn/jetpack
    的頭像 發表于 02-22 01:30 ?981次閱讀

    Kotlin聲明式UI框架Compose Multiplatform支持iOS

    ,基于 Kotlin 和?Jetpack Compose?打造,由 JetBrains 和開源貢獻者開發。 Jetpack Compose 是 Google 為構建原生 UI 打造的
    的頭像 發表于 04-24 09:12 ?1350次閱讀
    Kotlin聲明式UI框架<b class='flag-5'>Compose</b> Multiplatform支持iOS

    Jetpack Compose和設備類型的三大重要更新

    2024 年 Google I/O 大會上我們分享了大量更新和公告,幫助開發者提升工作效率。了解 2024 年 Google I/O 大會上有關 Jetpack Compose 和設備類型的三大重要更新。
    的頭像 發表于 08-09 17:07 ?742次閱讀

    docker-compose配置文件內容詳解以及常用命令介紹

    一、Docker Compose 簡介 Docker Compose是一種用于定義和運行多容器Docker應用程序的工具。通過一個? docker-compose.yml ?文件,您可以配置應用程序
    的頭像 發表于 12-02 09:29 ?1096次閱讀
    docker-<b class='flag-5'>compose</b>配置文件內容<b class='flag-5'>詳解</b>以及常用命令介紹
    百家乐官网赌场代理| 皇廷国际| 百家乐平注法是什么| 百家乐送彩金平台| 百家乐赢钱公式冯耕| 做百家乐网上投注| 百家乐赌场公司| 钱柜百家乐的玩法技巧和规则| 金钱豹百家乐的玩法技巧和规则 | 百家乐在线怎么玩| 百家乐太阳城 | 合乐8百家乐娱乐城| 百家乐投注秘笈| 博之道百家乐的玩法技巧和规则| 东京太阳城王子酒店| 新2娱乐城| 百家乐官网视频看不到| 大世界百家乐官网娱乐城| 游戏百家乐官网押金| 百家乐最常见的路子| 百家乐建材| 大发888赌场娱乐网规则| 大发888新址| 波音代理| 玩百家乐官网凤凰娱乐城| 百佬汇百家乐官网的玩法技巧和规则| 赌场百家乐官网图片| 利高百家乐娱乐城| 百家乐注册优惠平台| 百家乐筹码防伪| e世博官网| 开心8百家乐官网现金网| 互联网百家乐官网的玩法技巧和规则 | 大发888官方下载168| 大发娱乐城| 百盛百家乐官网软件| 百家乐视频麻将游戏| 大发888开户博彩吧| 凯旋门百家乐娱乐城| 德州扑克的玩法| 百家乐官网网投注|