Jetpack compose學習筆記之自定義layout(布局)


一,簡介

Compose中的自定義Layout主要通過LayoutModifier和Layout方法來實現。

不管是LayoutModifier還是Layout,都只能measure一次它的孩子View。

二,LayoutModifier(自定義View)

fun Modifier.customLayoutModifier(...) = Modifier.layout { 
// measurable: child to be measured and placed
// constraints: minimum and maximum for the width and height of the child
measurable, constraints ->
  ...
  // measure child
  val placeable = measurable.measure(constraints)
  // 設置view的width和height
  layout(width, height) {
      ...
      // 設置child顯示的位置
      placeable.placeRelative(0, 0)
  }
})

 例:實現設置Firstbaseline的padding功能

效果圖

 自定義LayoutModifier

@Composable
fun Modifier.firstBaselineTop(firstBaselineToTop: Dp) = this.then(
    layout { measurable, constraints ->
        val placeable = measurable.measure(constraints)
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        val firstBaseline = placeable[FirstBaseline]
        // 計算需要設置的padding值和原來的firstBaseline的差值
        val placeableY = firstBaselineToTop.roundToPx() - firstBaseline
        // View的高度(placeable.height為原來view的高度,包括padding)
        val height = placeable.height + placeableY

        layout(placeable.width, height) {
            placeable.placeRelative(
                0, placeableY
            )
        }
    }
)

設置padding

@Preview
@Composable
fun TextWithPaddingToBaselinePreview() {
  ComposeTestTheme {
    Text("Hi there!", Modifier.firstBaselineToTop(24.dp))
  }
}

繼承LayoutModifier,創建自定義Modifier

// How to create a modifier
@Stable
fun Modifier.padding(all: Dp) =
    this.then(
        PaddingModifier(start = all, top = all, end = all, bottom = all, rtlAware = true)
    )

// Implementation detail
private class PaddingModifier(
    val start: Dp = 0.dp,
    val top: Dp = 0.dp,
    val end: Dp = 0.dp,
    val bottom: Dp = 0.dp,
    val rtlAware: Boolean,
) : LayoutModifier {

    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {

        val horizontal = start.roundToPx() + end.roundToPx()
        val vertical = top.roundToPx() + bottom.roundToPx()

        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) {
            if (rtlAware) {
                placeable.placeRelative(start.roundToPx(), top.roundToPx())
            } else {
                placeable.place(start.roundToPx(), top.roundToPx())
            }
        }
    }
}

三,Layout(自定義ViewGroup)

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each child
            measurable.measure(constraints)
            // Set the size of the layout as big as it can
            layout(constraints.maxWidth, constraints.maxHeight) {
                // Place children
                placeable.placeRelative(x = 0, y = yPosition)
            }
        }
    }
}

例:實現自定義Column

效果圖

@Composable
fun MyOwnColumn(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // Don't constrain child views further, measure them with given constraints
        // List of measured children
        val placeables = measurables.map { measurable ->
            // Measure each child
            measurable.measure(constraints)
        }

        // child y方向的位置
        var yPosition = 0

        // Set the size of the layout as big as it can
        layout(constraints.maxWidth, constraints.maxHeight) {
            // Place children in the parent layout
            placeables.forEach { placeable ->
                // Position item on the screen
                placeable.placeRelative(x = 0, y = yPosition)

                // 累加當前view的高度
                yPosition += placeable.height
            }
        }
    }
}

使用自定義Layout

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    MyOwnColumn(modifier.padding(8.dp)) {
        Text("MyOwnColumn")
        Text("places items")
        Text("vertically.")
        Text("We've done it by hand!")
    }
}

四,創建類似瀑布流式自定義View

效果圖

 自定義Layout

@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    // 默認顯示的行數
    rows: Int = 3,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->

        // 每行的寬度
        val rowWidths = IntArray(rows) {0}
        // 每行的高度
        val rowHeights = IntArray(rows) {0}

        val placeables = measurables.mapIndexed { index, measurable ->
            // measure每一個孩子
            val placeable = measurable.measure(constraints)

            val row = index % rows
            // 根據children累計每行的寬度
            rowWidths[row] += placeable.width
            // 選擇children中最高的高度
            rowHeights[row] = max(rowHeights[row], placeable.height)

            placeable
        }

        // 選擇所有行中最寬的寬度
        val width = rowWidths.maxOrNull()
                // 限制rowWidths在minWidth和maxWidth之間           
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))
// 如果rowWidths為null,則設置為minWidth ?: constraints.minWidth // 累加所有行的高度 val height = rowHeights.sumOf { it} // 限制rowHeights在minHeight和maxHeight之間 .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight)) // 計算每行縱向顯示的位置 val rowY = IntArray(rows) { 0 } for (i in 1 until rows) { rowY[i] = rowY[i-1] + rowHeights[i-1] } // width,height為父布局的寬度和高度(即StaggeredGrids) layout(width, height) { val rowX = IntArray(rows) { 0 } placeables.forEachIndexed { index, placeable -> val row = index % rows placeable.placeRelative( x = rowX[row], y = rowY[row] ) rowX[row] += placeable.width } } } }

創建GridItemView

@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, 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)
            )
            // 創建4dp的空隙
            Spacer(Modifier.width(4.dp))
            Text(text = text)
        }
    }
}

設置數據源

val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)

調用自定義Layout

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    // 不設置horizontalScroll的話,橫向無法滑動
    StaggeredGrid(modifier = modifier.horizontalScroll(rememberScrollState())) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

設置行數為5

@Composable
fun BodyContent(modifier: Modifier = Modifier) {
    StaggeredGrid(modifier = modifier.horizontalScroll(rememberScrollState()), rows = 5) {
        for (topic in topics) {
            Chip(modifier = Modifier.padding(8.dp), text = topic)
        }
    }
}

 效果圖

 

 

更多信息請參看Layouts in Jetpack Compose (google.cn)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM