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)

 

posted @ 2021-08-03 11:02  minminjy123  阅读(920)  评论(0编辑  收藏  举报