0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Jetpack Compose基礎(chǔ)知識科普

谷歌開發(fā)者 ? 來源:Android 開發(fā)者 ? 作者:Android 開發(fā)者 ? 2022-04-02 13:38 ? 次閱讀

Jetpack Compose 是用于構(gòu)建原生 Android 界面的新工具包。它可簡化并加快 Android 上的界面開發(fā),使用更少的代碼、強(qiáng)大的工具和直觀的 Kotlin API,快速讓應(yīng)用生動而精彩。Compose 使用全新的組件——可組合項(xiàng) (Composable) 來布局界面,使用修飾符 (Modifier) 來配置可組合項(xiàng)。

本文會為您講解由可組合項(xiàng)和修飾符提供支持的組合布局模型,并深入探究其背后的工作原理以及它們的功能,讓您更好地了解所用布局和修飾符的工作方式,和應(yīng)如何以及在何時構(gòu)建自定義布局,從而實(shí)現(xiàn)滿足確切應(yīng)用需求的設(shè)計(jì)。

布局模型

Compose 布局系統(tǒng)的目標(biāo)是提供易于創(chuàng)建的布局,尤其是自定義布局。這要求布局系統(tǒng)具備強(qiáng)大的功能,使開發(fā)者能創(chuàng)建應(yīng)用所需的任何布局,并且讓布局具備優(yōu)異的性能。接下來,我們來看看 Compose 的布局模型是如何實(shí)現(xiàn)這些目標(biāo)的。

Jetpack Compose 可將狀態(tài)轉(zhuǎn)換為界面,這個過程分為三步:組合、布局、繪制。組合階段執(zhí)行可組合函數(shù),這些函數(shù)可以生成界面,從而創(chuàng)建界面樹。例如,下圖中的 SearchResult 函數(shù)會生成對應(yīng)的界面樹:

Jetpack Compose基礎(chǔ)知識科普

可組合函數(shù)生成對應(yīng)的界面樹

可組合項(xiàng)中可以包含邏輯和控制流,因此可以根據(jù)不同的狀態(tài)生成不同的界面樹。在布局階段,Compose 會遍歷界面樹,測量界面的各個部分,并將每個部分放置在屏幕 2D 空間中。也就是說,每個節(jié)點(diǎn)決定了其各自的寬度、高度以及 x 和 y 坐標(biāo)。在繪制階段,Compose 將再次遍歷這棵界面樹,并渲染所有元素。 本文將深入探討布局階段。布局階段又細(xì)分為兩個階段: 測量和放置。這相當(dāng)于 View 系統(tǒng)中的 onMeasure 和 onLayout。但在 Compose 中,這兩個階段會交叉進(jìn)行,因此我們把它看成一個布局階段。將界面樹中每個節(jié)點(diǎn)布局的過程分為三步:每個節(jié)點(diǎn)必須測量自身的所有子節(jié)點(diǎn),再決定自身的尺寸,然后放置其子節(jié)點(diǎn)。如下例,單遍即可對整個界面樹完成布局。

Jetpack Compose基礎(chǔ)知識科普

布局過程

其過程簡述如下:

  1. 測量根布局 Row;
  2. Row 測量它的第一個子節(jié)點(diǎn) Image;
  3. 由于 Image 是一個不含子節(jié)點(diǎn)的葉子節(jié)點(diǎn),它會測量自身尺寸并加以報告,還會返回有關(guān)如何放置其子節(jié)點(diǎn)的指令。Image 的葉子節(jié)點(diǎn)通常是空節(jié)點(diǎn),但所有布局都會在設(shè)置其尺寸的同時返回這些放置指令;
  4. Row 測量它的第二個子節(jié)點(diǎn) Column;
  5. Column 測量其子節(jié)點(diǎn),首先測量第一個子節(jié)點(diǎn) Text;
  6. Text 測量并報告其尺寸以及放置指令;
  7. Column 測量第二個子節(jié)點(diǎn) Text;
  8. Text 測量并報告其尺寸以及放置指令;
  9. Column 測量完其子節(jié)點(diǎn),可以決定其自身的尺寸和放置邏輯;
  10. Row 根據(jù)其所有子節(jié)點(diǎn)的測量結(jié)果決定其自身尺寸和放置指令。
測量完所有元素的尺寸后,將再次遍歷界面樹,并且會在放置階段執(zhí)行所有放置指令。

Layout 可組合項(xiàng)

我們已經(jīng)了解這個過程涉及的步驟,接下來看一下它的實(shí)現(xiàn)方式。先看看組合階段,我們采用 Row、Column、Text 等更高級別的可組合項(xiàng)來表示界面樹,每個高級別的可組合項(xiàng)實(shí)際上都是由低級別的可組合項(xiàng)構(gòu)建而成。以 Text 為例,可以發(fā)現(xiàn)它由若干更低級別的基礎(chǔ)構(gòu)建塊組成,而這些可組合項(xiàng)都會包含一個或多個 Layout 可組合項(xiàng)。
Jetpack Compose基礎(chǔ)知識科普

每個可組合項(xiàng)都包含一個或多個 Layout

Layout 可組合項(xiàng)是 Compose 界面的基礎(chǔ)構(gòu)建塊,它會生成 LayoutNode。在 Compose 中,界面樹,或者說組合 (composition) 是一棵 LayoutNode 樹。以下是 Layout 可組合項(xiàng)的函數(shù)簽名:
@Composablefun Layout(    content: @Composable () -> Unit,    modifier: Modifier = Modifier,    measurePolicy: MeasurePolicy) {}
Layout 可組合項(xiàng)的函數(shù)簽名

其中,content 是可以容納任何子可組合項(xiàng)的槽位,出于布局需要,content 中也會包含子 Layout。modifier 參數(shù)所指定的修飾符將應(yīng)用于該布局,這在下文中會詳細(xì)介紹。measurePolicy 參數(shù)是 MeasurePolicy 類型,它是一個函數(shù)式接口,指定了布局測量和放置項(xiàng)目的方式。一般情況下,如需實(shí)現(xiàn)自定義布局的行為,您要在代碼中實(shí)現(xiàn)該函數(shù)式接口:
@Composablefun MyCustomLayout(    modifier: Modifier = Modifier,    content: @Composable () -> Unit) {    Layout(         modifier = modifier,         content = content    ) { measurables: List,         constraints: Constraints ->        // TODO 測量和放置項(xiàng)目   }}
實(shí)現(xiàn) MeasurePolicy 函數(shù)式接口

在 MyCustomLayout 可組合項(xiàng)中,我們調(diào)用 Layout 函數(shù)并以 Trailing Lambda 的形式提供 MeasurePolicy 作為參數(shù),從而實(shí)現(xiàn)所需的 measure 函數(shù)。該函數(shù)接受一個 Constraints 對象來告知 Layout 它的尺寸限制。Constraints 是一個簡單類,用于限制 Layout 的最大和最小寬度與高度:

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

Constraints

measure 函數(shù)還會接受 List 作為參數(shù),這表示的是傳入的子元素。Measurable 類型會公開用于測量項(xiàng)目的函數(shù)。如前所述,布局每個元素需要三步:每個元素必須測量其所有子元素,并以此判斷自身尺寸,再放置其子元素。其代碼實(shí)現(xiàn)如下:

@Composablefun MyCustomLayout(    content: @Composable () -> Unit,    modifier: Modifier = Modifier) {    Layout(         modifier = modifier,         content = content    ) { measurables: List,         constraints: Constraints ->        // placeables 是經(jīng)過測量的子元素,它擁有自身的尺寸值        val placeables = measurables.map { measurable ->            // 測量所有子元素,這里不編寫任何自定義測量邏輯,只是簡單地            // 調(diào)用 Measurable 的 measure 函數(shù)并傳入 constraints            measurable.measure(constraints)        }        val width = // 根據(jù) placeables 計(jì)算得出        val height = // 根據(jù) placeables 計(jì)算得出        // 報告所需的尺寸        layout (width, height) {            placeables.foreach { placeable ->                // 通過遍歷將每個項(xiàng)目放置到最終的預(yù)期位置                placeable.place(                    x = …                    y = …                )            }        }   }}
布局每個元素的代碼示例

上述代碼中使用了 Placeable 的 place 函數(shù),它還有一個 placeRelative 函數(shù)可用于從右到左的語言設(shè)置中,當(dāng)使用該函數(shù)時,它會自動對坐標(biāo)進(jìn)行水平鏡像。 請注意,API 在設(shè)計(jì)上可阻止您嘗試放置未經(jīng)測量的元素,place 函數(shù)只適用于 Placeable,也就是 measure 函數(shù)的返回值。在 View 系統(tǒng)中,調(diào)用 onMeasure 以及 onLayout 的時機(jī)由您決定,而且調(diào)用順序沒有強(qiáng)制要求,但這會產(chǎn)生一些微妙的 bug 以及行為上的差異。

自定義布局示例

MyColumn 示例


Jetpack Compose基礎(chǔ)知識科普

Column

Compose 提供一個 Column 組件用于縱向排布元素。為了理解這個組件背后的工作方式及其使用 Layout 可組合項(xiàng)的方式,我們來實(shí)現(xiàn)自己的一個 Column。暫且將其命名為 MyColumn,其實(shí)現(xiàn)代碼如下:
@Composablefun MyColumn(    modifier: Modifier = Modifier,    content: @Composable () -> Unit) {    Layout(         modifier = modifier,         content = content    ) { measurables, constraints ->        // 測量每個項(xiàng)目并將其轉(zhuǎn)換為 Placeable        val placeables = measurables.map { measurable ->            measurable.measure(constraints)        }        // Column 的高度是所有項(xiàng)目所測得高度之和        val height = placeables.sumOf { it.height }        // Column 的寬度則為內(nèi)部所含最寬項(xiàng)目的寬度        val width = placeables.maxOf { it.width }        // 報告所需的尺寸        layout (width, height) {            // 通過跟蹤 y 坐標(biāo)放置每個項(xiàng)目            var y = 0            placeables.forEach { placeable ->                placeable.placeRelative(x = 0, y = y)                // 按照所放置項(xiàng)目的高度增加 y 坐標(biāo)值                y += placeable.height            }        }    }}

自定義 Column

VerticalGrid 示例

Jetpack Compose基礎(chǔ)知識科普

VerticalGrid

我們再來看另一個示例:構(gòu)建常規(guī)網(wǎng)格。其部分代碼實(shí)現(xiàn)如下:
@Composablefun VerticalGrid(    modifier: Modifier = Modifier,    columns: Int = 2,    content: @Composable () -> Unit) {    Layout(        content = content,         modifier = modifier    ) { measurables, constraints ->        val itemWidth = constraints.maxWidth / columns        // 通過 copy 函數(shù)保留傳遞下來的高度約束,但設(shè)置確定的寬度約束        val itemConstraints = constraints.copy (            minWidth = itemWidth,            maxWidth = itemWidth,        )                // 使用這些約束測量每個項(xiàng)目并將其轉(zhuǎn)換為 Placeable        val placeables = measurables.map { it.measure(itemConstraints) }    }}

自定義 VerticalGrid

在該示例中,我們通過 copy 函數(shù)創(chuàng)建了新的約束。這種為子節(jié)點(diǎn)創(chuàng)建新約束的概念就是實(shí)現(xiàn)自定義測量邏輯的方式。創(chuàng)建不同約束來測量子節(jié)點(diǎn)的能力是此模型的關(guān)鍵,父節(jié)點(diǎn)與子節(jié)點(diǎn)之間并沒有協(xié)商機(jī)制,父節(jié)點(diǎn)會以 Constraints 的形式傳遞其允許子節(jié)點(diǎn)的尺寸范圍,只要子節(jié)點(diǎn)從該范圍中選擇了其尺寸,父節(jié)點(diǎn)必須接受并處理子節(jié)點(diǎn)。

這種設(shè)計(jì)的優(yōu)點(diǎn)在于我們可以單遍測量整棵界面樹,并且禁止執(zhí)行多個測量循環(huán)。這是 View 系統(tǒng)中存在的問題,嵌套結(jié)構(gòu)執(zhí)行多遍測量過程可能會讓葉子視圖上的測量次數(shù)翻倍,Compose 的設(shè)計(jì)能夠防止發(fā)生這種情況。實(shí)際上,如果您對某個項(xiàng)目進(jìn)行兩次測量,Compose 會拋出異常:

Jetpack Compose基礎(chǔ)知識科普

重復(fù)測量某個項(xiàng)目時 Compose 會拋出異常

布局動畫示例

由于具備更強(qiáng)的性能保證,Compose 提供了新的可能性,例如為布局添加動畫。Layout composable 不僅可以創(chuàng)建通用布局,還能創(chuàng)建出符合應(yīng)用設(shè)計(jì)需求的專用布局。以 Jetsnack 應(yīng)用中的自定義底部導(dǎo)航為例,在該設(shè)計(jì)中,如果某項(xiàng)目被選中,則顯示標(biāo)簽;如果未被選中,則只顯示圖標(biāo)。而且,設(shè)計(jì)還需要讓項(xiàng)目的尺寸和位置根據(jù)當(dāng)前選擇狀態(tài)執(zhí)行動畫。Jetpack Compose基礎(chǔ)知識科普

Jetsnack 應(yīng)用中的自定義底部導(dǎo)航

我們可以使用自定義布局來實(shí)現(xiàn)該設(shè)計(jì),從而對布局變化的動畫處理進(jìn)行精確控制:
@Composablefun BottomNavItem(    icon: @Composable BoxScope.() -> Unit,    text: @Composable BoxScope.() -> Unit,    @FloatRange(from = 0.0, to = 1.0) animationProgress: Float) {    Layout(        content = {            // 將 icon 和 text 包裹在 Box 中            // 這種做法能讓我們?yōu)槊總€項(xiàng)目設(shè)置 layoutId            Box(                modifier = Modifier.layoutId(“icon”)                content = icon            )            Box(                modifier = Modifier.layoutId(“text”)                content = text            )        }    ) { measurables, constraints ->        // 通過 layoutId 識別對應(yīng)的 Measurable,比依賴項(xiàng)目的順序更可靠        val iconPlaceable = measurables.first {it.layoutId == “icon” }.measure(constraints)        val textPlaceable = measurables.first {it.layoutId == “text” }.measure(constraints)         // 將放置邏輯提取到另一個函數(shù)中以提高代碼可讀性        placeTextAndIcon(            textPlaceable,            iconPlaceable,            constraints.maxWidth,            constraints.maxHeight,            animationProgress        )    }} fun MeasureScope.placeTextAndIcon(    textPlaceable: Placeable,    iconPlaceable: Placeable,    width: Int,    height: Int,    @FloatRange(from = 0.0, to = 1.0) animationProgress: Float): MeasureResult {     // 根據(jù)動畫進(jìn)度值放置文本和圖標(biāo)    val iconY = (height - iconPlaceable.height) / 2    val textY = (height - textPlaceable.height) / 2     val textWidth = textPlaceable.width * animationProgress    val iconX = (width - textWidth - iconPlaceable.width) / 2    val textX = iconX + iconPlaceable.width     return layout(width, height) {        iconPlaceable.placeRelative(iconX.toInt(), iconY)        if (animationProgress != 0f) {            textPlaceable.placeRelative(textX.toInt(), textY)        }    }}
自定義底部導(dǎo)航

使用自定義布局的時機(jī)


希望以上示例能幫助您了解自定義布局的工作方式以及這些布局的應(yīng)用理念。標(biāo)準(zhǔn)布局強(qiáng)大而靈活,但它們也需要適應(yīng)很多用例。有時,若您知道具體的實(shí)現(xiàn)需求,使用自定義布局可能更加合適。

當(dāng)您遇到以下場景時,我們推薦使用自定義布局:

  • 難以通過標(biāo)準(zhǔn)布局實(shí)現(xiàn)的設(shè)計(jì)。雖然可以使用足夠多的 Row 和 Column 構(gòu)建大部分界面,但這種實(shí)現(xiàn)方式有時難以維護(hù)和升級;

  • 需要非常精確地控制測量和放置邏輯;

  • 需要實(shí)現(xiàn)布局動畫。我們正在開發(fā)可對放置進(jìn)行動畫處理的新 API,未來可能不必自行編寫布局就能實(shí)現(xiàn);

  • 需要完全控制性能。下文會詳細(xì)介紹這一點(diǎn)。

修飾符

至此,我們了解了 Layout 可組合項(xiàng)以及構(gòu)建自定義布局的方式。如果您使用 Compose 構(gòu)建過界面,就會知道修飾符在布局、配置尺寸和位置方面發(fā)揮著重要作用。通過前文的示例可以看到,Layout 可組合項(xiàng)接受修飾符鏈作為參數(shù)。修飾符會裝飾它們所附加的元素,可以在布局自身的測量和放置操作之前參與測量和放置。接下來我們來看看它的工作原理。

修飾符分很多不同的類型,可以影響不同的行為,例如繪制修飾符 (DrawModifier)、指針輸入修飾符 (PointerInputModifier) 以及焦點(diǎn)修飾符 (FocusModifier)。本文我們將重點(diǎn)介紹布局修飾符 (LayoutModifier),該修飾符提供了一個 measure 方法,該方法的作用與 Layout 可組合項(xiàng)基本相同,不同之處在于,它只作用于單個 Measurable 而不是 List,這是因?yàn)樾揎椃膽?yīng)用對象是單個項(xiàng)目。在 measure 方法中,修飾符可以修改約束或者實(shí)現(xiàn)自定義放置邏輯,就像布局一樣。這表示您并不總是需要編寫自定義布局,如果只想對單個項(xiàng)目執(zhí)行操作,則可以改用修飾符。

以 padding 修飾符為例,該工廠函數(shù)以修飾符鏈為基礎(chǔ),創(chuàng)建能夠捕獲所需 padding 值的 PaddingModifier 對象。

fun Modifier.padding(all: Dp) =    this.then(PaddingModifier(            start = all,            top = all,            end = all,            bottom = all        )    ) private class PaddingModifier(    val start: Dp = 0.dp,    val top: Dp = 0.dp,    val end: Dp = 0.dp,    val bottom: Dp = 0.dp) : LayoutModifier { override fun MeasureScope.measure(        measurable: Measurable,        constraints: Constraints    ): MeasureResult {        val horizontal = start.roundToPx() + end.roundToPx()        val vertical = top.roundToPx() + bottom.roundToPx()         // 按 padding 尺寸收縮外部約束來修改測量        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) {                // 按所需的 padding 執(zhí)行偏移以放置內(nèi)容                placeable.placeRelative(start.roundToPx(), top.roundToPx())        }    }}
padding 修飾符的實(shí)現(xiàn)除了通過上例中的方式覆寫 measure 方法實(shí)現(xiàn)測量,您也可以使用 Modifier.layout,在無需創(chuàng)建自定義布局的情況下直接通過修飾符鏈向任意可組合項(xiàng)添加自定義測量和放置邏輯,如下所示:
Box(Modifier            .background(Color.Gray)            .layout { measurable, constraints ->                // 通過修飾符在豎直方向添加 50 像素 padding 的示例                val padding = 50                val placeable = measurable.measure(constraints.offset(vertical = -padding))                layout(placeable.width, placeable.height + padding) {                    placeable.placeRelative(0, padding)                }            }        ) {            Box(Modifier.fillMaxSize().background(Color.DarkGray))}

使用 Modifier.layout 實(shí)現(xiàn)布局

雖然 Layout 接受單個 Modifier 參數(shù),該參數(shù)會建立一個按順序應(yīng)用的修飾符鏈。我們通過示例來了解它與布局模型的交互方式。我們將分析下圖修飾符的效果及其工作原理:Jetpack Compose基礎(chǔ)知識科普 ?
修飾符鏈的效果示例

首先,我們?yōu)?Box 設(shè)置尺寸并將其繪制出來,但這個 Box 放置在了父布局的左上角,我們可以使用 wrapContentSize 修飾符將 Box 居中放置。wrapContentSize 允許內(nèi)容測量其所需尺寸,然后使用 align 參數(shù)放置內(nèi)容,align 參數(shù)的默認(rèn)值為 Center,因此可以省略這個參數(shù)。但我們發(fā)現(xiàn),Box 還是在左上角。這是因?yàn)榇蠖鄶?shù)布局都會根據(jù)其內(nèi)容自適應(yīng)調(diào)整尺寸,我們需要讓測量尺寸占據(jù)整個空間,以便讓 Box 在空間內(nèi)居中。因此,我們在 wrapContentSize 前面添加 fillMaxSize 布局修飾符來實(shí)現(xiàn)這個效果。eff2ea32-b246-11ec-aa7f-dac502259ad0.gif

修飾符鏈的應(yīng)用過程
我們來看一下這些修飾符是如何實(shí)現(xiàn)此效果的。您可以借助下圖動畫來輔助理解該過程:

f007d3de-b246-11ec-aa7f-dac502259ad0.gif
修飾符鏈的工作原理

假設(shè)這個 Box 要放入最大尺寸為 200*300 像素的容器內(nèi),容器會將相應(yīng)的約束傳入修飾符鏈的第一個修飾符中。fillMaxSize 實(shí)際上會創(chuàng)建一組新約束,并設(shè)置最大和最小寬度與高度,使之等于傳入的最大寬度與高度以便填充到最大值,在本例中是 200*300 像素。這些約束沿著修飾符鏈傳遞以測量下一個元素,wrapContentSize 修飾符會接受這些參數(shù),它會創(chuàng)建新的約束來放寬對傳入約束的限制,從而讓內(nèi)容測量其所需尺寸,也就是寬 0-200,高 0-300。這看起來只像是對 fillMax 步驟的反操作,但請注意,我們是使用這個修飾符實(shí)現(xiàn)項(xiàng)目居中的效果,而不是重設(shè)項(xiàng)目的尺寸。這些約束沿著修飾符鏈傳遞到 size 修飾符,該修飾符創(chuàng)建具體尺寸的約束來測量項(xiàng)目,指定尺寸應(yīng)該正好是 50*50。最后,這些約束傳遞到 Box 的布局,它執(zhí)行測量并將解析得到的尺寸 (50*50) 返回到修飾符鏈,size 修飾符因此也將其尺寸解析為 50*50,并據(jù)此創(chuàng)建放置指令。然后 wrapContent 解析其大小并創(chuàng)建放置指令以居中放置內(nèi)容。因?yàn)?wrapContent 修飾符知道其尺寸為 200*300,而下一個元素的尺寸為 50*50,所以使用居中對齊創(chuàng)建放置指令,以便將內(nèi)容居中放置。最后,fillMaxSize 解析其尺寸并執(zhí)行放置操作。

修飾符鏈的執(zhí)行方式與布局樹的工作方式非常相像,差異在于每個修飾符只有一個子節(jié)點(diǎn),也就是鏈中的下一個元素。約束會向下傳遞,以便后續(xù)元素用其測量自身尺寸,然后返回解析得到的尺寸,并創(chuàng)建放置指令。該示例也說明了修飾符順序的重要性。通過使用修飾符對功能進(jìn)行組合,您可以很輕松地將不同的測量和布局策略組合在一起。

高級功能

接下來將介紹布局模型的一些高級功能,雖然您不一定總是需要這些功能,但它們能夠幫助您構(gòu)建更高級的功能。 固有特性測量 (Intrinsic Measurement)前文提到過,Compose 使用單遍布局系統(tǒng)。這個說法并不完全正確,布局并不總是能通過單遍操作就得以完成,有時我們也需要了解有關(guān)子節(jié)點(diǎn)尺寸的信息才能最終確定約束。 以彈出式菜單為例。假設(shè)有一個包含五個菜單項(xiàng)的 Column,如下圖所示,它的顯示基本上是正常的,但是可以看到,每個菜單項(xiàng)的尺寸卻不相同。Jetpack Compose基礎(chǔ)知識科普
菜單項(xiàng)的尺寸不相同
我們很容易想到,讓每個菜單項(xiàng)都占用允許的最大尺寸即可
:
Jetpack Compose基礎(chǔ)知識科普
每個菜單項(xiàng)都占有允許的最大尺寸

但這么做也沒能完全解決問題,因?yàn)椴藛未翱跁U(kuò)大到其最大尺寸。有效的解決方法是使用最大固有寬度來確定尺寸:Jetpack Compose基礎(chǔ)知識科普

使用最大固有寬度來確定尺寸

這里確定了 Column 會盡力為每個子節(jié)點(diǎn)提供所需的空間,對 Text 而言,其寬度是單行渲染全部文本所需的寬度。在確定固有尺寸后,將使用這些值設(shè)置 Column 的尺寸,然后,子節(jié)點(diǎn)就可以填充 Column 的寬度了。

如果使用最小值而非最大值,又會發(fā)生什么呢?

Jetpack Compose基礎(chǔ)知識科普
使用最小固有寬度來確定尺寸

它將確定 Column 會使用子節(jié)點(diǎn)的最小尺寸,而 Text 的最小固有寬度是每行一個詞時的寬度。因此,我們最后得到一個按詞換行的菜單。 如需詳細(xì)了解固有特性測量,請參閱 Jetpack Compose 中的布局 Codelab 中的 "固有特性"部分。

ParentData

到目前為止,我們看到的修飾符都是通用修飾符,也就是說,它們可以應(yīng)用于任何可組合項(xiàng)。有時,您的布局提供的一些行為可能需要從子節(jié)點(diǎn)獲得一些信息,這便要用到 ParentDataModifier。

我們回到前面那個在父節(jié)點(diǎn)中居中放置藍(lán)色 Box 的示例。這一次,我們將這個 Box 放在另一個 Box 中。Box 中的內(nèi)容在一個稱為 BoxScope 的接收器作用域內(nèi)排布。BoxScope 定義了只在 Box 內(nèi)可用的修飾符,它提供了一個名為 Align 的修飾符。這個修飾符剛好能夠提供我們要應(yīng)用到藍(lán)色 Box 的功能。因此,如果我們知道藍(lán)色 Box 位于另一個 Box 內(nèi),就可以改用 Align 修飾符來定位它。

Jetpack Compose基礎(chǔ)知識科普

在 BoxScope 中可以改用 Align 修飾符來定位內(nèi)容

Align 是一個 ParentDataModifier 而不是我們之前看到的那種布局修飾符,因?yàn)樗皇窍蚱涓腹?jié)點(diǎn)傳遞一些信息,所以如果不在 Box 中,該修飾符便不可用。它包含的信息將提供給父 Box,以供其設(shè)置子布局。

您也可以為自己的自定義布局編寫 ParentDataModifier,從而允許子節(jié)點(diǎn)向父節(jié)點(diǎn)告知一些信息,以供父節(jié)點(diǎn)在布局時使用。 對齊線 (Alignment Lines)我們可以使用對齊線根據(jù)布局頂部、底部或中心以外的標(biāo)準(zhǔn)來設(shè)置對齊。最常用的對齊線是文本基線。假設(shè)需要實(shí)現(xiàn)這樣一個設(shè)計(jì):Jetpack Compose基礎(chǔ)知識科普

需要實(shí)現(xiàn)設(shè)計(jì)圖中的圖標(biāo)和文本對齊



我們很自然就能想到這樣來實(shí)現(xiàn)它:
Row {    Icon(modifier = Modifier        .size(10. dp)        .align(Alignment.CenterVertically)    )    Text(modifier = Modifier        .padding(start = 8.dp)        .align(Alignment.CenterVertically)    )}

有問題的對齊實(shí)現(xiàn)

仔細(xì)觀察,會發(fā)現(xiàn)圖標(biāo)并沒有像設(shè)計(jì)稿那樣對齊在文本的基線上。
Jetpack Compose基礎(chǔ)知識科普
圖標(biāo)和文本居中對齊,圖標(biāo)底部沒有落在文本基線上

我們可以通過以下代碼進(jìn)行修復(fù):

Row {    Icon(modifier = Modifier        .size(10. dp)        .alignBy { it.measuredHeight }    )    Text(modifier = Modifier        .padding(start = 8.dp)        .alignByBaseline()    )}
正確的對齊實(shí)現(xiàn)

首先,對 Text 使用 alignByBaseline 修飾符。而圖標(biāo)既沒有基線,也沒有其他對齊線,我們可以使用 alignBy 修飾符讓圖標(biāo)對齊到我們需要的任何位置。在本例中,我們知道圖標(biāo)的底部是對齊的目標(biāo)位置,因此將圖標(biāo)的底部進(jìn)行對齊。最終便實(shí)現(xiàn)了期望的效果:

Jetpack Compose基礎(chǔ)知識科普

圖標(biāo)底部與文本基線完美對齊

由于對齊功能會穿過父節(jié)點(diǎn),因此,處理嵌套對齊時,只需設(shè)置父節(jié)點(diǎn)的對齊線,它會從子節(jié)點(diǎn)獲取相應(yīng)的值。如下例所示:

Jetpack Compose基礎(chǔ)知識科普

未設(shè)置對齊的嵌套布局

Jetpack Compose基礎(chǔ)知識科普

通過父節(jié)點(diǎn)設(shè)置對齊線

您甚至可以在自定義布局中創(chuàng)建自己的自定義對齊,從而允許其他可組合項(xiàng)對齊到它。

BoxWithConstraints

BoxWithConstraints 是一個功能強(qiáng)大且很實(shí)用的布局。在組合中,我們可以根據(jù)條件使用邏輯和控制流來選擇要顯示的內(nèi)容,但是,有時候可能希望根據(jù)可用空間的大小來決定布局內(nèi)容。

從前文中我們知道,尺寸信息直到布局階段才可用,也就是說,這些信息一般無法在組合階段用來決定要顯示的內(nèi)容。此時 BoxWithConstraints 便派上用場了,它與 Box 類似,但它將內(nèi)容的組合推遲到布局階段,此時布局信息已經(jīng)可用了。BoxWithConstraints 中的內(nèi)容在接收器作用域內(nèi)排布,布局階段確定的約束將通過該作用域公開為像素值或 DP 值。

@Composablefun BoxWithConstraints(        ...        content: @Composable BoxWithConstraintsScope.() -> Unit) // BoxWithConstraintsScope 公開布局階段確定的約束interface BoxWithConstraintsScope : BoxScope {    val constraints: Constraints    val minWidth: Dp    val maxWidth: Dp    val minHeight: Dp    val maxHeight: Dp}

BoxWithConstraints 和 BoxWithConstraintsScope

它內(nèi)部的內(nèi)容可以使用這些約束來選擇要組合的內(nèi)容。例如,根據(jù)最大寬度選擇不同的呈現(xiàn)方式:

@Composablefun MyApp(...) {    BoxWithConstraints() { // this: BoxWithConstraintsScope        when {            maxWidth < 400.dp -> CompactLayout()            maxWidth < 800.dp -> MediumLayout()            else -> LargeLayout()        }    }}
在 BoxWithConstraintsScope 中根據(jù)最大寬度選擇不同的布局

性能

我們介紹了單遍布局模型如何防止在測量或放置方面花費(fèi)過多時間,也演示了布局階段兩個不同的子階段: 測量和放置?,F(xiàn)在,我們將介紹性能相關(guān)的內(nèi)容。

盡量避免重組

單遍布局模型的設(shè)計(jì)效果是,任何只影響項(xiàng)目的放置而不影響測量的修改都可以單獨(dú)執(zhí)行。以 Jetsnack 為例:

Jetpack Compose基礎(chǔ)知識科普

Jetsnack 應(yīng)用中產(chǎn)品詳情頁的協(xié)調(diào)滾動效果

這個產(chǎn)品詳情頁包含協(xié)調(diào)滾動效果,頁面上的一些元素根據(jù)滾動操作進(jìn)行移動或縮放。請注意標(biāo)題區(qū)域,這個區(qū)域會隨著頁面內(nèi)容而滾動,最后固定在屏幕的頂部。

@Composablefun SnackDetail(...) {    Box {        val scroll = rememberScrollState(0)        Body(scroll)        Title(scroll = scroll.value)        ...    }} @Composablefun Body(scroll: ScrollState) {    Column(modifier = Modifier.verticalScroll(scroll)) {    }}
詳情頁的大致實(shí)現(xiàn)

為了實(shí)現(xiàn)此效果,我們將不同元素作為獨(dú)立的可組合項(xiàng)疊放在一個 Box 中,提取滾動狀態(tài)并將其傳入 Body 組件。Body 會使用滾動狀態(tài)進(jìn)行設(shè)置以使內(nèi)容能夠垂直滾動。在 Title 等其他組件中可以觀察滾動位置,而我們的觀察方式會對性能產(chǎn)生影響。例如,使用最直接的實(shí)現(xiàn),簡單地使用滾動值對內(nèi)容進(jìn)行偏移:

@Composablefun Title(scroll: Int) {    Column(        modifier = Modifier.offset(scroll)    ) {    }}
簡單地使用滾動值偏移 Title 的內(nèi)容

這種方法的問題是,滾動是一個可觀察的狀態(tài)值,讀取該值所處的作用域規(guī)定了狀態(tài)發(fā)生變化時 Compose 需要重新執(zhí)行的操作。在此示例中,我們要讀取組合中的滾動偏移值,然后使用它來創(chuàng)建偏移修飾符。只要滾動偏移值發(fā)生變化,Title 組件都需要重新組合,也就需要創(chuàng)建并執(zhí)行新的偏移修飾符。由于滾動狀態(tài)是從組合中讀取的,任何更改都會導(dǎo)致重組,在重組時,還需要進(jìn)行布局和繪制這兩個后續(xù)階段。 不過,我們不是要更改顯示的內(nèi)容,而是更改內(nèi)容的位置。我們還可以進(jìn)一步提高效率,通過修改一下實(shí)現(xiàn),不再接受原始滾動位置,而是傳遞一個可以提供滾動位置的函數(shù):
@Composablefun Title(scrollProvider: () -> Int) {    Column(        modifier = Modifier.offset {            val scroll = scrollProvider()            val offset = (maxOffset - scroll).coerceAtLeast(minOffset)            IntOffset(x = 0, y = offset)        }    ) {    }}

使用提供滾動位置的函數(shù)代替原始滾動位置

這時,我們可以在不同的時間只調(diào)用此 Lambda 函數(shù)并讀取滾動狀態(tài)。這里使用了 offset 修飾符,它接受能提供偏移值的 Lambda 函數(shù)作為參數(shù)。這意味著在滾動發(fā)生變化時,不需要重新創(chuàng)建修飾符,只在放置階段才會讀取滾動狀態(tài)的值。所以,當(dāng)滾動狀態(tài)變化時我們只需要執(zhí)行放置和繪制操作,不需要重組或測量,因此能夠提高性能。

再回到底部導(dǎo)航的示例,它存在同樣的問題,我們可以用相同方法加以修正:
@Composablefun BottomNavItem(    icon: @Composable BoxScope.() -> Unit,    text: @Composable BoxScope.() -> Unit,    animationProgress: () -> Float) {     val progress = animationProgress()     val textWidth = textPlaceable.width * progress    val iconX = (width - textWidth - iconPlaceable.width) / 2    val textX = iconX + iconPlaceable.width     return layout(width, height) {        iconPlaceable.placeRelative(iconX.toInt(), iconY)        if (animationProgress != 0f) {            textPlaceable.placeRelative(textX.toInt(), textY)        }    }}

△修正后的底部導(dǎo)航

我們使用了能提供當(dāng)前動畫進(jìn)度的函數(shù)作為參數(shù),因此不需要重組,只執(zhí)行布局即可。 您需要掌握一個原則: 只要可組合項(xiàng)或修飾符的參數(shù)可能頻繁發(fā)生更改,都應(yīng)當(dāng)保持謹(jǐn)慎,因?yàn)檫@種情況可能導(dǎo)致過度組合。只有在更改顯示內(nèi)容時,才需要重組,更改顯示位置或顯示方式則不需要這么做。

BoxWithConstraints 可以根據(jù)布局執(zhí)行組合,是因?yàn)樗鼤诓季蛛A段啟動子組合。出于性能考慮,我們希望盡量避免在布局期間執(zhí)行組合。因此,相較于 BoxWithConstraints,我們傾向于使用會根據(jù)尺寸更改的布局。當(dāng)信息類型隨尺寸更改時才使用 BoxWithConstraints。

提高布局性能

有時候,布局不需要測量其所有子節(jié)點(diǎn)便可獲知自身大小。舉個例子,有如下構(gòu)成的卡片:

Jetpack Compose基礎(chǔ)知識科普
△布局卡片示例

圖標(biāo)和標(biāo)題構(gòu)成標(biāo)題欄,剩下的是正文。已知圖標(biāo)大小為固定值,標(biāo)題高度與圖標(biāo)高度相同。測量卡片時,就只需要測量正文,它的約束就是布局高度減去 48 DP,卡片的高度則為正文的高度加上 48 DP。

Jetpack Compose基礎(chǔ)知識科普
測量過程只測量正文尺寸

系統(tǒng)識別出只測量了正文,因此它是決定布局尺寸的唯一重要子節(jié)點(diǎn),圖標(biāo)和文本仍然需要測量,但可以在放置過程中執(zhí)行。Jetpack Compose基礎(chǔ)知識科普

放置過程測量圖標(biāo)和文本

假設(shè)標(biāo)題是 "Layout",當(dāng)標(biāo)題發(fā)生變化時,系統(tǒng)不必重新執(zhí)行布局的測量操作,因此不會重新測量正文,從而省去不必要的工作。Jetpack Compose基礎(chǔ)知識科普

標(biāo)題發(fā)生變化時不必重新測量

總結(jié)

在本文中,我們介紹了自定義布局的實(shí)現(xiàn)過程,還使用修飾符構(gòu)建和合并布局行為,進(jìn)一步降低了滿足確切功能需求的難度。此外,還介紹了布局系統(tǒng)的一些高級功能,例如跨嵌套層次結(jié)構(gòu)的自定義對齊,為自有布局創(chuàng)建自定義 ParentDataModifier,支持自動從右向左設(shè)置,以及將組合操作推遲到布局信息已知時,等等。我們還了解如何執(zhí)行單遍布局模型,如何跳過重新測量以使其只執(zhí)行重新放置操作的方法,熟練使用這些方法,您將能編寫出通過手勢進(jìn)行動畫處理的高性能布局邏輯。

對布局系統(tǒng)的理解能夠幫助您構(gòu)建滿足確切設(shè)計(jì)需求的布局,從而創(chuàng)建用戶喜愛的優(yōu)秀應(yīng)用。

原文標(biāo)題:深度解析 Jetpack Compose 布局

文章出處:【微信公眾號:谷歌開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • Android
    +關(guān)注

    關(guān)注

    12

    文章

    3909

    瀏覽量

    126917
  • 界面
    +關(guān)注

    關(guān)注

    0

    文章

    59

    瀏覽量

    15591
  • 工具包
    +關(guān)注

    關(guān)注

    0

    文章

    45

    瀏覽量

    9512

原文標(biāo)題:深度解析 Jetpack Compose 布局

文章出處:【微信號:Google_Developers,微信公眾號:谷歌開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    C語言基礎(chǔ)知識科普

    C語言是單片機(jī)開發(fā)中的必備基礎(chǔ)知識,本文列舉了部分STM32學(xué)習(xí)中比較常見的一些C語言基礎(chǔ)知識。
    發(fā)表于 07-21 10:58 ?1844次閱讀

    科普一下CAN總線的基礎(chǔ)知識

    CAN總線是一種常用的總線,對于剛開始接觸CAN總線的,面對著各式各樣的資料,可能不知道從何看起,今天科普一下CAN總線的基礎(chǔ)知識。CAN2.0協(xié)議分為A版本和B版本,A版本協(xié)議為11位標(biāo)識符(標(biāo)準(zhǔn)幀),B版本在兼容11位ID標(biāo)識符的同時,向上擴(kuò)展到29位ID標(biāo)識符。
    發(fā)表于 05-16 09:49 ?3083次閱讀
    <b class='flag-5'>科普</b>一下CAN總線的<b class='flag-5'>基礎(chǔ)知識</b>

    compose的使用技巧是什么?

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

    五分鐘讀懂WiFi基礎(chǔ)知識

    家1、嵌入式技術(shù)常識科普【物聯(lián)網(wǎng)】WiFi基礎(chǔ)知識五分鐘讀懂TCP/IP;協(xié)議STM32開發(fā) -- Keil基本使用如何看懂時序圖(以SPI/I2C為例)ESP8266配網(wǎng)思路(不使用...
    發(fā)表于 12-01 06:36

    通信基礎(chǔ)知識教程

    通信基礎(chǔ)知識 1、電信基礎(chǔ)知識2、通信電源技術(shù)3、配線設(shè)備結(jié)構(gòu)、原理與防護(hù)4、防雷基礎(chǔ)知識5、EMC基礎(chǔ)知識6、防腐蝕原理與技術(shù)7、產(chǎn)品安
    發(fā)表于 03-04 16:48 ?33次下載

    科普】卷積神經(jīng)網(wǎng)絡(luò)基礎(chǔ)知識

    本文的主要目的,是簡單介紹時下流行的深度學(xué)習(xí)算法的基礎(chǔ)知識,本人也看過許多其他教程,感覺其中大部分講的還是太過深奧,于是便有了寫一篇科普文的想法。博主也是現(xiàn)學(xué)現(xiàn)賣,文中如有不當(dāng)之處,請各位指出
    發(fā)表于 11-10 14:49 ?1676次閱讀
    【<b class='flag-5'>科普</b>】卷積神經(jīng)網(wǎng)絡(luò)<b class='flag-5'>基礎(chǔ)知識</b>

    電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識

    電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識電源管理基礎(chǔ)知識
    發(fā)表于 09-15 14:36 ?76次下載
    電源管理<b class='flag-5'>基礎(chǔ)知識</b>電源管理<b class='flag-5'>基礎(chǔ)知識</b>電源管理<b class='flag-5'>基礎(chǔ)知識</b>

    詳解Jetpack Compose 1.1版本的新功能

    我們一如既往地搭建產(chǎn)品路線圖,現(xiàn)在已經(jīng)發(fā)布了 Jetpack Compose 的 1.1 版本,這是 Android 的現(xiàn)代原生界面工具包。此版本新增了一些功能,比如經(jīng)過優(yōu)化的焦點(diǎn)處理、觸摸目標(biāo)值
    的頭像 發(fā)表于 03-11 10:14 ?1361次閱讀

    Jetpack Compose 更新一覽 | 2022 Android 開發(fā)者峰會

    作者 /?Android 開發(fā)者關(guān)系工程師 Jolanda Verhoef 去年我們發(fā)布了 Jetpack Compose ,此后一直在進(jìn)行優(yōu)化。我們已添加了新的功能并創(chuàng)造出功能更強(qiáng)大的工具,幫助
    的頭像 發(fā)表于 11-23 17:55 ?1059次閱讀

    Google計(jì)劃用Jetpack Compose來重建Android系統(tǒng)中的設(shè)置應(yīng)用

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

    Compose for Wear OS 1.1 推出穩(wěn)定版: 了解新功能!

    為 Wear OS 構(gòu)建出色的響應(yīng)式應(yīng)用。 ? Compose for Wear OS?1.1 版本 https://developer.android.google.cn/jetpack
    的頭像 發(fā)表于 02-22 01:30 ?844次閱讀

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

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

    電氣基本知識科普

    電氣基本知識科普
    的頭像 發(fā)表于 09-09 10:23 ?5992次閱讀
    電氣基本<b class='flag-5'>知識</b><b class='flag-5'>科普</b>

    科普|電源管理知識

    科普|電源管理知識
    的頭像 發(fā)表于 10-17 16:31 ?547次閱讀
    <b class='flag-5'>科普</b>|電源管理<b class='flag-5'>知識</b>

    Jetpack Compose和設(shè)備類型的三大重要更新

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