一、概念
组合函数是整个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
}
}
)
}