Compose(?/N) - 自定义布局

 一、概念

组合函数是整个UI中的某一部分界面,界面首次渲染时会构建一个UI树,每个被调用的组合函数都会作为一个节点添加进去。每个节点都有一个父容器以及可能的多个子元素,每个子元素在父容器中都有一个坐标(x,y)和尺寸(width,height)。 

二、单个子元素

就是自定义一个 Modifier 的扩展函数,实现它的 layout( ) 方法如何测量及放置自身。形参Lambda有2个参数,measurable用于子元素的测量和放置,constraints约束子元素宽高的最大和最小值。 

 2.1 例子一

需求:Text无法直接设置基准线到屏幕的距离,即图中的a。

//自定义一个设置基准线到顶部屏幕距离的Modifier扩展函数,返回值还是Modifier
fun Modifier.firstBaseLine2Top(e: Dp): Modifier = then(
    layout { measurable, constraints ->
        //调用measurable.measure()对子元素进行测量,此例不需要进行任何限制,直接将constraints传入
        val placeable = measurable.measure(constraints)
        //检查该组件是否支持FirstBaseline(false抛出IllegalStateException )
        check(placeable[FirstBaseline] != AlignmentLine.Unspecified)
        //存在的情况下,获取FirstBaseline离组件顶部的距离
        val c = placeable[FirstBaseline]
        //计算y轴上组件放置位置(x轴直接是0)
        val d = e.roundToPx() - c
        //计算组件的高度heifht(width宽度直接是placeable.width)
        val height = placeable.height + d
        //根据测量尺寸摆放内容
        layout(placeable.width,height) {
            //对自身进行摆放
            placeable.placeRelative(0,d)
        }
    }
)
//使用
@Composable
fun Show() {
    Text(
        text = "Hello Word!",
        modifier = Modifier.firstBaseLine2Top(24.dp)
    )
}

三、多个子元素

自定义组合函数的形参需要接收一个Modifier(外部用来修饰该自定义组件的属性或约束),一个组合函数(作为该自定义容器的子元素),一个Lambda表达式measurePolicy(对子元素进行测量和摆放,此处只实现measure,需要固有特性测量见下方介绍)。

3.1 例子一(简单)

需求:自定义一个纵向摆放子元素的布局,且尽可能大的占用父容器。

//自定义一个纵向摆放子元素
@Composable
fun MyColumn(
    modifier: Modifier = Modifier,  //用于外部修饰自己
    content: @Composable () -> Unit //接收子元素
){
    //对子元素进行测量和摆放
    Layout(
        modifier = modifier,
        content = content
    ) {measurables, constraints ->
        // 测量每个子元素的尺寸
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        var yPosition = 0   //下一个子元素在y轴上摆放的y坐标
        //摆放子元素
        //尽可能大的占用父布局类似于match_parent(官方Column是尽可能小的占用类似于wrap_content)
        layout(constraints.maxWidth,constraints.maxHeight) {
            placeables.forEach { placeable ->
                placeable.placeRelative(0, yPosition)
                yPosition += placeable.height
            }
        }
    }
}
//使用
@Composable
fun Show() {
    MyColumn(modifier = Modifier.padding(10.dp)) {
        Text(text = "条目1")
        Text(text = "条目2")
        Text(text = "条目3")
    }
}

 3.2 例子二(复杂)

需求:横向滑动的瀑布流,可以设置行数。

@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 ->
            val placeable = measurable.measure(constraints)
            //根据索引对子元素分组,记录每一行的宽高
            val row = index % rows    //确保只有3行,该值只会得到 0,1,2
            rowWidths[row] += placeable.width    //一行的宽度=这行所有元素宽度之和
            rowHeights[row] = max(rowHeights[row], placeable.height)    //一行的高度=这行最高的元素
            placeable   //测量完要返回placeable对象
        }
        //【第二步:计算自身的尺寸】
        val width = rowWidths.maxOrNull()   //宽度取所有行中宽度最大值
            ?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))  //宽度限制在最大值和最小值之间
            ?: constraints.minWidth //为null就设为最小值
        val height = rowHeights.sumOf { it }    //高度为所有行高之和
            .coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
        val rowY = IntArray(rows){0}    //每行子元素在y轴上摆放的坐标
        for (i in 1 until rows) {    //第一行肯定是0,从第二行开始赋值
            rowY[i] = rowY[i - 1] + rowHeights[i - 1]    //当前行y坐标=前一行y坐标+前一行高度
        }
        //【第三步:摆放子元素】
        layout(width, height) {    //在自身的尺寸里摆放
            val rowX = IntArray(rows){0}    //每行子元素在x轴上的坐标
            placeables.forEachIndexed { index, placeable ->
                val row = index % rows
                placeable.placeRelative(rowX[row], rowY[row])
                rowX[row] += placeable.width
            }
        }
    }
}
//使用
val topics = listOf(
    "Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
    "Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
    "Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Composable
fun Show() {
    StaggeredGrid {
        for (topic in topics) {
            Text(
                text = topic,
                modifier = Modifier.padding(8.dp).background(MaterialTheme.colors.error)
            )
        }
    }
}

四、固有特性测量 Intrinsics

        Compose为提高效率子元素只能测量一次(再次会抛异常)。为了实现预先获得子元素宽高信息再确定自身宽高信息,Compose提供了固有特性测量机制,允许在子元素正式测量前能获宽高等信息。

        要实现固有特性测量,除了像上面对 Layout( )  中 MeasurePolicy 接口的 Measure( ) 进行重写,还需要重写相应的 Intrinsic 方法提供宽高最大最小值,四个用到哪种写哪种:minIntrinsicHeight、maxIntrinsicHeight、minIntrinsicWidth、maxIntrinsicWidth。

4.1 举例一

未采用固有特性测量:

@Composable
fun WithoutIntrinsics() {
    Row {
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally), text = "Text 1"
        )
        //分割线本意是和左右两个文本一样高
        //Row没有对它的子元素测量做任何限制,填充父容器会尽可能的撑大父容器
        Divider(
            color = Color.Black, modifier = Modifier
                .fillMaxHeight()
                .width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .wrapContentWidth(Alignment.CenterHorizontally), text = "Text 2"
        )
    }
}

采用固有特性测量: 

@Composable
fun WithIntrinsics(){
    //高度设为最小(刚好包裹子元素)
    Row(modifier = Modifier.height(IntrinsicSize.Min)) {
        //其他代码未变
    }
}
@Composable
fun MyColumn2(
    modifier: Modifier = Modifier,  //用于外部修饰自己
    content: @Composable () -> Unit //接收子元素
){
    Layout(
        modifier = Modifier,
        content = content,
        measurePolicy =  object : MeasurePolicy {
            //对子元素进行测量和摆放
            override fun MeasureScope.measure(measurables: List<Measurable>, constraints: Constraints): MeasureResult {
                var height = 0  //自身高度为子元素的高度累加
                var width = 0   //自身宽度为子元素最宽的那个
                val placeables = measurables.map { measurable ->    //测量所有子元素的尺寸
                    val placeable = measurable.measure(constraints)
                    height += placeable.height
                    width = max(width, placeable.width)
                    placeable
                }
                //摆放子元素
                return layout(width, height) {
                    var yPosition = 0   //下一个子元素在y轴上摆放的坐标
                    placeables.map { placeable ->
                        placeable.placeRelative(0, yPosition)
                        yPosition += placeable.height
                    }
                }
            }

            override fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>, height: Int): Int {
                var maxWidth = 0
                measurables.forEach { intrinsicMeasurable->
                    maxWidth = max(maxWidth, intrinsicMeasurable.minIntrinsicWidth(height))
                }
                return maxWidth
            }
        }
    )
}

猜你喜欢

转载自blog.csdn.net/HugMua/article/details/129942245