Jetpack-Compose 學(xué)習(xí)筆記(三)—— Compose 的自定義“View”

在上一篇中,我們不僅了解了 Compose 中的 Column、Row、Box 等幾種常見的布局方式 還學(xué)習(xí)了 CompositionLocal 類在 Compose 中進(jìn)行傳值的方法;還有可快速搭建 App 結(jié)構(gòu)的 Scaffold 腳手架組件,順便學(xué)習(xí)了 Surface、Modifier 的一些使用,還有 ConstraintLayout 在Compose 中的使用方法。雖然官方提供了這么多 Compose 組件,但在實際需求開發(fā)中,定制化組件仍然必不可少。

在傳統(tǒng)的 View 體系中,系統(tǒng)為開發(fā)者提供了許多可以直接使用的組件 View,比如:TextView、ImageView、RelativeLayout等。我們也可以通過自定義 View 來創(chuàng)建一些系統(tǒng)沒有提供給我們的、具有特殊功能的 View。Compose 當(dāng)然也不甘落后,在 Compose 中我們可以使用 Layout 組件來自定義我們自己的 Composable 組件。實際上,所有類似于 Column、Row 等組件底層都是用 Layout 進(jìn)行擴(kuò)展實現(xiàn)的。

在 View 體系中,自定義 View 最為常見的兩種情況是:1)繼承已有 View 進(jìn)行功能擴(kuò)展,例如繼承 TextView 或直接繼承 View 進(jìn)行改寫;2)繼承 ViewGroup,并重寫父類的 onMeasure 和 onLayout 方法。而在 Compose 中我們只需要簡單地使用 Layout 組件自定義就可以了。

在開始之前,我們需要先了解一下 Layout Composable 組件的一些基礎(chǔ)知識。

1. Compose 自定義 Layout 的基本原則

在 Compose 中,一個 Composable 方法被執(zhí)行時,會被添加到 UI 樹中,然后會被渲染展示在屏幕上。這個 Composable 方法我們可以看成是一個 View 系統(tǒng)中的布局,在 Compose 中稱為 Layout。每個 Layout 都有一個 parent Layout 和 0 個或多個 children,這跟 View 體系很像。當(dāng)然,這個 Layout 自身含有在它的 parent Layout 中的位置信息,包括位置坐標(biāo)(x, y)和它的尺寸大小 widthheight

Layout 中的 children Layout 子元素會被調(diào)用去測量它們自身的大小,同時需要滿足規(guī)定的 Constraints 約束。這些 Constraints 約束限制了 widthheight的最大值和最小值。當(dāng) Layout 把自己的 children Layout 測量完成之后,它自己的尺寸才會確定下來,又是遞歸。。。一旦一個 Layout 元素完成自身的測量,它就可以將自己的 children 根據(jù) Constraints 約束在自己的空間中進(jìn)行擺放了。是不是跟 View 體系一樣?先測量后擺放。

OK,最重要的來了!Compose UI 不允許多次測量。 Layout 元素為了嘗試不同的測量設(shè)置,它不能多次測量其任何子元素。單次測量(Single-pass measurement)當(dāng)然會提升渲染效率,尤其是在 Compose 處理深度較大的 UI 樹時。如果一個 Layout 元素需要測量兩次它的所有子元素,子元素中的子元素就會被測量四次,以此類推,測量的次數(shù)就會隨著布局深度成指數(shù)級增長!其實 View 體系就是這樣的,所以在 View 體系中開發(fā)一定要減少布局的層數(shù)!不然在需要重復(fù)測量的情況下,渲染效率將會及其低下。所以 Compose 中才做了不允許多次測量的限制,然而,在有些場景下,我們又是需要獲取到子元素多次測量并獲取信息的。對于這些情況,還是有方法做到多次測量的,限于篇幅原因,后面有空再說~

Compose 中自定義一個控件(官方稱之為 Layout)也有兩種情況:

  1. 自定義 Layout 沒有其他子元素,就只是它自己本身,類似于 View 體系中的 “自定義View”;
  2. 自定義 Layout 有子元素,需要考慮子元素的擺放位置,類似于 View 體系中的 “自定義ViewGroup”。

我們先來看第一種情況。

2. Compose 自定義一個 “View”

Compose 中的自定義 Layout 跟 View 體系是很不同的。我們需要自定義的 Layout 居然就是自定義一個 Modifier 屬性!就是去自己實現(xiàn) Modifier 中 Layout 方法,去實現(xiàn)如何測量以及放置它自己本身即可。一個常見的自定義 Layout Modifier 的結(jié)構(gòu)代碼如下:

// code 1
fun Modifier.customLayoutModifier(...) {    // 可以自定義一些屬性
    Modifier.layout { measurable, constraints ->
        ...    // 在這里需要自己實現(xiàn) 測量 和 放置的方法
    }
}

可以看出來,關(guān)鍵就是 Modifier.layout 方法,它有兩個 lambda 表達(dá)式:

  1. measurable:用于子元素的測量和位置放置的;
  2. constraints:用于約束子元素 width 和 height 的最大值和最小值。

舉個簡單的栗子進(jìn)行說明。一個普通的 Text 組件只能調(diào)整文案的邊緣離 Text 組件上下左右四邊緣的距離,例如圖1所示。這個 Text 只能設(shè)置四周的 padding 值,上下我設(shè)置的 15dp,左右設(shè)置的 30dp。

圖 1

如果我想控制文案的底部 baseline 離 Text 上邊距的距離呢?啥是底部 baseline?這就需要了解一下 Android 在繪制文案時的算法了。

圖 2

從圖 2 可以看出,Android 繪制文案時,baseline 決定了文案主體的底部位置。Compose 中的 Text 只能通過 Modifier.padding 設(shè)置 leading 離 Text 組件頂部的距離。而這里我們自定義的 Layout 需要滿足可設(shè)置 Baseline 離 Text 頂部的距離。即下圖圖 3 中上方的效果,怎么做呢?

圖 3

首先當(dāng)然就是測量啦,記住 Layout 只能測量它的子元素一次。在 code1 中調(diào)用 measure 方法,就可以測量了:

// code 2
fun Modifier.firstBaselineToTop(  // firstBaselineToTop 就是你自定義的 modifier 的方法名
    firstBaselineToTop: Dp    // 自定義 modifier 方法中的參數(shù),這里就是一個
) = this.then(
    layout { measurable, constraints -> // 調(diào)用 layout 方法去測量和放置子元素組件
        val placeable = measurable.measure(constraints) // 首先是測量
        ...
    }
)

當(dāng)調(diào)用 measurable 的 measure 方法后,就會返回一個 Placeable 對象。在這里,我們可以將 layout 中的 constraints 約束條件傳遞給 measure 方法,或者傳入我們自定義的約束條件的 lambda。因為在這個場景下我們不需要再去對測量進(jìn)行任何的限制,所以直接傳入 layout 中給的 constraints 即可。總之,這一步就是為了得到這個 Placeable 對象,拿到這個之后就可以在后面調(diào)用 Placeable 對象的 placeRelative 方法對子元素進(jìn)行位置的擺放了!

OK,現(xiàn)在已經(jīng)對 Composable 組件進(jìn)行了測量,然后我們就可以調(diào)用 layout(width, height) 方法去根據(jù)測量的尺寸來放置內(nèi)容。width 不用求,直接用測量得來的 width 就行,關(guān)鍵就是如何求出傳入 layout 方法的 height 值,看代碼再來說吧:

// code 3
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        // 檢查這個 Composable 組件是否存在 FirstBaseline
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        // 存在的情況下,獲取 FirstBaseline 離 Composable 組件頂部的距離
        val firstBaseline = placeable[FirstBaseline]
        // 計算 Y 軸方向上 Composable 組件的放置位置
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        // 計算得出此 Composable 組件真正的 height 值
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            ...
        }
    }
)

說實話最初看到這段代碼也是懵逼了好久。。。
首先 check 方法類似于一個 assert 斷言,如果里面的結(jié)果是 false 則會拋出一個 IllegalStateException 異常。這里是檢查下被我們自定義的 Modifier 修飾的 Composable 組件是否存在 FirstBaseline 屬性,Text 組件里是存在 baseline 的,如果不存在當(dāng)然就不能用我們自定義的這個 firstBaselineToTop Modifier了。

存在的情況下,再去獲取這個 Baseline 與 此組件頂部的距離,也就是圖4 中 c 的長度。圖中藍(lán)色框代表的是普通的 Text 組件所占的空間位置;黑色框代表的是屏幕邊緣;紅色虛線代表的是 Text 中的 Baseline。a 表示的就是我們自定義的 Modifier.firstBaselineToTop 方法的 firstBaselintToTop 參數(shù)。我們的目標(biāo)就是可以根據(jù)傳入的 firstBaselintToTop 參數(shù)計算出 Text 組件在 Y 軸上的擺放位置,以及真正的 width 和 height 值大小。

圖 4

之前在 layout 方法中調(diào)用了 measurable 的 measure 方法測量的是普通 Text 組件的寬高,即圖4 中藍(lán)色框的寬高,而我們自定義的 Layout 的寬高則是圖中用橙色和綠色標(biāo)注的寬高尺寸。width 直接由 Placeable 對象就可獲得(placeable.width),而高度由示意圖可以得出計算方法:height = placeable.height + d,即普通 Text 的高度再加上 d,d = a - c,即 d = firstBaselintToTop - baseline。所以,d 就是 placeableY 參數(shù)。終于看懂 code 3 了,原來就是為了算出自定義 Layout 的 width 和 height,然后通過 layout 方法進(jìn)行設(shè)置啊!

接下來就是位置的放置了。調(diào)用 Placeable 對象的 placeRelative 方法即可:

// code 4
fun Modifier.firstBaselineToTop(
    firstBaselineToTop: Dp
) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        val firstBaseline = placeable[FirstBaseline]
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        val height = placeable.height + placeableY
        layout(placeable.width, height) {
            placeable.placeRelative(0, placeableY)
        }
    }
)

注意,自定義 Layout 必須調(diào)用 placeRelative 方法,否則該自定義 Layout 將不可見。 placeRelative 方法會根據(jù)當(dāng)前的 layoutDirection 布局方向?qū)ψ远x Layout 自動進(jìn)行位置調(diào)整。在這里我們自定義的 Layout 擺放比較簡單,就是 Y 軸上有個偏移量,X 軸上沒有偏移,看圖2 也可直觀得知。

那么如何使用呢?想必你們也猜到了,就跟之前使用其他 Modifier 方法修飾 Text 或其他 Composable 組件一樣使用就好:

// code 5
@Composable
fun CustomLayoutDemo() {
    Row {
        Text(
            text = "我是栗子1",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 20.sp
        )

        Spacer(modifier = Modifier.width(20.dp))

        Text(
            text = "我是栗子2",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 15.sp
        )

        Spacer(modifier = Modifier.width(20.dp))

        Text(
            text = "我是栗子3",
            modifier = Modifier.firstBaselineToTop(40.dp),
            fontSize = 30.sp
        )
    }
}
圖 5

在 code 5 中分別展示了 3 個 Text,都使用了我們自定義的 Modifier 修飾符 firstBaselineToTop,且設(shè)置的參數(shù)都是 40dp,不同的是字號。從圖 5 的顯示效果來看,達(dá)到了我們想要的自定義 Layout 的效果,即雖然字號大小不同,但是每個 Text 中文案的 Baseline 離自定義 Layout 的頂部距離是一樣的。

3. 自定義一個 “ViewGroup”

說完了 Compose 自定義“View” 的方法,當(dāng)然也就少不了自定義“ViewGroup” 了。其實,Compose 中的 Row、Column 組件都是使用 Layout 方法實現(xiàn)的,它也是 Compose 用來自定義一個 “ViewGroup” 的核心方法。我們可以通過 Layout 組件手動地對它其中的子元素進(jìn)行測量和擺放,一個自定義 “ViewGroup”的 Layout 代碼結(jié)構(gòu)通常如下代碼所示:

// code 6
@Composable
fun CustomLayout(
    modifier: Modifier = Modifier,
    // 此處可添加自定義的參數(shù)
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurable, constraints ->
        // 對 children 進(jìn)行測量和放置
        ···
    }
}

對于一個自定義 Layout 來說,,最少需要三個參數(shù):

  • modifier:由外部傳入的修飾符,用來修飾我們自定義的這個 Layout 組件的一些屬性或 Constraints;
  • content:我們自定義的這個 Layout 組件中所包含的子元素 children;
  • measurePolicy:熟悉 Kotlin 語法的同學(xué)們會知道,code 6 中 Layout 后跟著的 lambda 表達(dá)式其實也是 Layout 的一個參數(shù),從字面意思上也可知道,這個是為了對 children 進(jìn)行測量和擺放操作的。默認(rèn)場景下只實現(xiàn) measure 方法即可,當(dāng)我們想讓我們自定義的 Layout 組件適配 Intrinsics (官方稱之為 固有特性測量)時,就需要重寫 minIntrinsicWidth、minIntrinsicHeight、maxIntrinsicWidth、maxIntrinsicHeight 方法。篇幅原因以后再說哈~

這里我們用 Layout 組件自定義一個基本的簡單的 Column 組件,用于豎直方向上擺放子元素,我們?nèi)∶麨?MyOwnColumn。如之前所述的,我們第一件事就是測量 children,并且只能測量一次。與之前的自定義“View”不同的是,這里需要測量的不是它本身的尺寸,而是測量它其中包含的所有 children 的尺寸:

// code 7
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // 此處可添加自定義的參數(shù)
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // 對 children 進(jìn)行測量和放置
        val placeables = measurables.map { measurable ->
            // 測量每個 child 的尺寸
            measurable.measure(constraints)
        }
        ...
    }
}

可以看出,在 map 里每個 child 都調(diào)用 measure 方法進(jìn)行了測量,并且與之前一樣,我們無需再針對測量進(jìn)行限制,所以直接傳入 Layout 中的 constraints 即可。到這里,我們已經(jīng)測量了所有的 children 子元素。

在設(shè)置這些 children 的位置之前,我們還需要根據(jù)測量的 children 尺寸來計算得出我們自定義的 MyOwnColumn 組件自身的寬高了。下面代碼是盡最大可能地設(shè)置我們自定義的 MyOwnColumn 的 Layout 尺寸:

// code 8
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // 此處可添加自定義的參數(shù)
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // 對 children 進(jìn)行測量和放置
        val placeables = measurables.map { measurable ->
            // 測量每個 child 的尺寸
            measurable.measure(constraints)
        }
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 擺放 children
            ...
        }
    }
}

最后就可以對 children 進(jìn)行擺放了。與上述的自定義“View”相同,我們也是調(diào)用placeable.placeRelative(x,y)來放置位置。因為是自定義一個 Column,需要豎直方向上一個個進(jìn)行擺放,所以每個 child 水平方向上 x 肯定從最左邊開始,設(shè)置為 0 。而豎直方向上需要一個變量記錄下一個 child 在豎直方向上的位置值。詳細(xì)代碼如下:

// code 9
@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    // 此處可添加自定義的參數(shù)
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ){ measurables, constraints ->
        // 對 children 進(jìn)行測量和放置
        val placeables = measurables.map { measurable ->
            // 測量每個 child 的尺寸
            measurable.measure(constraints)
        }
        var yPosition = 0  // 記錄下一個元素豎直方向上的位置
        layout(constraints.maxWidth, constraints.maxHeight) {
            // 擺放 children
            placeables.forEach { placeable ->  
                placeable.placeRelative(x = 0, yPosition)
                yPosition += placeable.height
            }
        }
    }
}

注意一下我們自定義的這個 Column 的寬高設(shè)置的是盡最大可能撐滿父布局:layout(constraints.maxWidth, constraints.maxHeight),所以跟官方的 Column 是有很大的不同的。這里只是為了說明 Compose 中自定義一個“ViewGroup”的方法流程。

MyOwnColumn 在使用上與 Column 一致,只是占用父布局空間的策略不一樣。官方的 Column 布局默認(rèn)情況下寬高是盡可能小的占用父布局,類似于 wrap_content;而 MyOwnColumn 是盡可能大的占用父布局,類似于 match_parent。下圖圖6 也可以清楚地看到效果。

// code 10
@Composable
fun MyOwnColumnDemo() {
    MyOwnColumn(Modifier.padding(20.dp)) {
        Text("我是栗子1")
        Text("我是栗子2")
        Text("我是栗子3")
    }
}
圖 6

對比一下 Compose 中的自定義 Layout 的兩種方式,一種是針對某個組件進(jìn)行的功能擴(kuò)展,類似于 View 體系中對某個已有的 View 或直接繼承 View 進(jìn)行的自定義,它其實是自定義一個 Modifier 方法;另一種是針對某個容器組件的自定義,類似于 View 體系中對某個已有的 ViewGroup 或直接繼承 ViewGroup 進(jìn)行自定義,它其實就是一個 Layout 組件,是布局的主要核心組件。接下來讓我們看看更加復(fù)雜的自定義 Layout。

4. 自定義復(fù)雜的 Layout

OK,了解了 Compose 自定義 Layout 的基本方法步驟,讓我們看看一個稍微復(fù)雜的栗子。假如需要實現(xiàn)一個橫向滑動的瀑布流布局,例如下圖中間部分所示:

圖 7

可以設(shè)置展示成多少行,這里是展示成 3 行,我們只需要傳入所有的子元素即可。現(xiàn)有的官方 Compose 組件中沒有這種功能的組件,這就需要定制化了。先按照之前的模板代碼構(gòu)建一下框架:

// code 11
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,  // 自定義的參數(shù),控制展示的行數(shù),默認(rèn)為 3行
    content: @Composable () -> Unit
){
    Layout(    // 主要還是這個 Layout 方法
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 測量和位置擺放邏輯
    }
}

接下來還是那個流程:1)測量所有子元素尺寸;2)計算自定義 Layout 的尺寸;3)擺放子元素。這里只展示 Layout 方法中的代碼:

// code 12
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 用于記錄每一行的寬度信息
        val rowWidths = IntArray(rows){0}
        // 用于記錄每一行的高度信息
        val rowHeights = IntArray(rows){0}
        val placeables = measurables.mapIndexed { index, measurable ->
            // 標(biāo)準(zhǔn)流程:測量每個 child 尺寸,獲得 placeable
            val placeable = measurable.measure(constraints)
            // 根據(jù)序號給每個 child 分組,記錄每一組的寬高信息
            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = max(rowHeights[row], placeable.height)
            
            placeable // 測量完了要記得返回 placeable 對象
        }
        ...
    }

接下來,就是計算自定義 Layout 自身的尺寸了。通過上面的操作,我們已經(jīng)得知每行 children 的最大高度,那么所有行高度相加就可以得到自定義 Layout 的高度了;而所有行中寬度最大值就是自定義 Layout 的寬度了。此外,我們還得到了每一行在 Y 軸上的位置了。相關(guān)的代碼如下:

// code 13
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        ...
        // 自定義 Layout 的寬度取所有行中寬度最大值
        val width = rowWidths.maxOrNull()
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
            ?: constraints.minWidth
        // 自定義 Layout 的高度當(dāng)然為所有行高度之和
        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        // 計算出每一行的元素在 Y軸 上的擺放位置
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]
        }

        // 設(shè)置 自定義 Layout 的寬高
        layout(width, height) {
            // 擺放每個 child
            ...
        }
    }

咦?是不是又和你想象中的代碼不太一樣?在求寬度 width 時,它還使用了 coerceIn 方法對 width 進(jìn)行了限制,限制 width 在 constraints 約束的最小值和最大值之間,如果超出了則會被設(shè)置成最小值或最大值。height 也是如此。然后還是調(diào)用的 layout 方法來設(shè)置我們自定義 Layout 的寬高。

最后,就是調(diào)用 placeable.placeRelative(x, y)方法將我們的 children 擺放到屏幕上即可。當(dāng)然,還是需要借助變量存儲 X 軸 上的位置信息的。具體代碼如下:

// code 14
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        ...
        // 計算出每一行的元素在 Y軸 上的擺放位置
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]
        }

        // 設(shè)置 自定義 Layout 的寬高
        layout(width, height) {
            // 擺放每個 child
            val rowX = IntArray(rows) { 0 }  // child 在 X 軸的位置
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    rowX[row],
                    rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }

代碼邏輯比較簡單,不再多做什么解釋。綜上,完整的這個自定義 Layout 的代碼如下:

// code 15
// 橫向瀑布流自定義 layout
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    rows: Int = 3,  // 自定義的參數(shù),控制展示的行數(shù),默認(rèn)為 3行
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 用于記錄每一橫行的寬度信息
        val rowWidths = IntArray(rows) { 0 }
        // 用于記錄每一橫行的高度信息
        val rowHeights = IntArray(rows) { 0 }
        // 測量每個 child 尺寸,獲得每個 child 的 Placeable 對象
        val placeables = measurables.mapIndexed { index, measurable ->
            // 標(biāo)準(zhǔn)流程:測量每個 child 尺寸,獲得 placeable
            val placeable = measurable.measure(constraints)
            // 根據(jù)序號給每個 child 分組,記錄每一個橫行的寬高信息
            val row = index % rows
            rowWidths[row] += placeable.width
            rowHeights[row] = max(rowHeights[row], placeable.height)
            placeable    // 這句別忘了,返回每個 child 的placeable
        }

        // 自定義 Layout 的寬度取所有行中寬度最大值
        val width = rowWidths.maxOrNull()
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
            ?: constraints.minWidth
        // 自定義 Layout 的高度當(dāng)然為所有行高度之和
        val height = rowHeights.sumOf { it }
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        // 計算出每一行的元素在 Y軸 上的擺放位置
        val rowY = IntArray(rows) { 0 }
        for (i in 1 until rows) {
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]
        }

        // 設(shè)置 自定義 Layout 的寬高
        layout(width, height) {
            // 擺放每個 child
            val rowX = IntArray(rows) { 0 }  // child 在 X 軸的位置
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(
                    rowX[row],
                    rowY[row]
                )
                rowX[row] += placeable.width
            }
        }
    }
}

OK,再寫一個小的組件作為 children 子元素,用來顯示,具體代碼如下:

// code 16
@Composable
fun Chip(
    modifier: Modifier = Modifier, text: String
) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Magenta, width = Dp.Hairline),
        shape = RoundedCornerShape(8.dp)
    ) {
        Row(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Box(
                modifier = Modifier
                    .size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}

還有一點需要注意的是,我們自定義的 Layout StaggeredGrid 的寬度是會超出屏幕的,所以在實際使用中,還得添加一個 Modifier.horizonalScroll 用于水平方向上滑動,這樣才用著舒服~ 實際使用的代碼樣例如下:

// code 17
val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)
Row(modifier = Modifier.horizontalScroll(rememberScrollState())){
    StaggeredGrid() {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp),text = topic)
        }
    }
}
圖 8

當(dāng)然,還支持自己設(shè)置需要展示成幾行的樣式,這里默認(rèn)值為 3行。

總結(jié)一下,在 Compose 中自定義 Layout 的基本流程其實跟 View 體系中自定義 View 的一樣,其中最大的不同就是在測量的步驟,Compose 為提高效率不允許多次進(jìn)行測量。而且 Compose 的自定義 Layout 的兩種情況也可以對應(yīng)到 View 體系中的兩個情況,但可以看出,Compose 都是在 Layout 組件中進(jìn)行的改寫與編程,可以讓開發(fā)者更加聚焦在具體的代碼邏輯上,這也是 Compose 自定義 Layout 的優(yōu)勢所在。那么,Compose 的自定義“View”,你學(xué)會了么?

ps. 贈人玫瑰,手留余香。歡迎轉(zhuǎn)發(fā)分享加關(guān)注,你的認(rèn)可是我繼續(xù)創(chuàng)作的精神源泉。

參考文獻(xiàn)

  1. https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#6
  2. 大海螺Utopia。《Android文字基線(Baseline)算法》. http://www.lxweimin.com/u/79e66729b5ec
  3. Jetpack Compose 博物館 - 自定義Layout. https://compose.net.cn/layout/custom_layout/
  4. https://developer.android.google.cn/codelabs/jetpack-compose-layouts?continue=https%3A%2F%2Fdeveloper.android.google.cn%2Fcourses%2Fpathways%2Fcompose%23codelab-https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-layouts#7
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容