Compose 自定义容器布局 笔记

40 阅读4分钟

在界面树中布置每个节点的过程分为三个步骤,每个节点必须:

  1. 测量所有子项
  2. 确定自己的尺寸
  3. 放置其子项

image.png

在Compose中“自定义复杂View”是一个思维转换:不再通过继承View类并重写onDrawonMeasure等方法,而是通过组合基础组件、使用Modifier系统、或直接调用CanvasDrawScope API来构建

1️⃣ 路径一:组合现有组件 (最常见)

这是Compose最主要的思想。通过将内置的基础组件(Box, Column, Row, Text, Icon等)像搭积木一样组合起来,并施以一系列Modifier(修饰符),你就能创造出全新的、功能复杂的组件。

示例:自定义一个带图标和文字的统计卡片

@Composable
fun StatCard(title: String, value: String, icon: ImageVector) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Row(
            modifier = Modifier.padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                imageVector = icon,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.primary,
                modifier = Modifier.size(40.dp)
            )
            Spacer(modifier = Modifier.width(16.dp))
            Column {
                Text(
                    text = title,
                    style = MaterialTheme.typography.bodyMedium,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
                Text(
                    text = value,
                    style = MaterialTheme.typography.headlineSmall,
                    fontWeight = FontWeight.Bold
                )
            }
        }
    }
}
// 使用
StatCard(title = "总点击量", value = "1,234", icon = Icons.Filled.TouchApp)

核心CardRowIconColumnText都是系统提供的可组合项,通过Modifier.padding.size等调整外观和布局,封装成一个新的StatCard

2️⃣ 路径二:使用 Canvas 进行自定义绘制

当你需要绘制自定义图形(如折线图、圆形进度条、不规则形状)时,应使用 Canvas

示例:绘制一个简单的自定义进度指示器

@Composable
fun CustomProgressIndicator(progress: Float) { // progress 范围 0f-1f
    Canvas(
        modifier = Modifier
            .size(100.dp)
            .padding(8.dp)
    ) {
        // 1. 绘制背景圆环
        drawCircle(
            color = Color.LightGray,
            style = Stroke(width = 12.dp.toPx(), cap = StrokeCap.Round)
        )
        // 2. 根据进度绘制前景圆弧
        drawArc(
            color = MaterialTheme.colorScheme.primary,
            startAngle = -90f, // 从12点钟方向开始
            sweepAngle = 360 * progress, // 扫过的角度
            useCenter = false,
            style = Stroke(width = 12.dp.toPx(), cap = StrokeCap.Round)
        )
        // 3. 在中心绘制进度文本
        drawContext.canvas.nativeCanvas.apply {
            drawText(
                "${(progress * 100).toInt()}%",
                center.x,
                center.y + 15.dp.toPx() / 2,
                android.graphics.Paint().apply {
                    textSize = 14.sp.toPx()
                    textAlign = android.graphics.Paint.Align.CENTER
                    color = android.graphics.Color.BLACK
                }
            )
        }
    }
}

核心:在 DrawScope 作用域内(drawArcdrawCircle 等函数的接收者),使用其提供的绘图API。toPx() 用于将 dp 转换为像素。

3️⃣ 路径三:实现自定义布局

当子组件需要特殊的测量或摆放逻辑时(如瀑布流、环形布局),需要使用 Layout 可组合项。

示例:实现一个基础的竖向堆叠但居中对其的布局

@Composable
fun VerticalCenteredLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables, constraints ->
        // 1. 测量每个子项
        // 注意 constraints 表示父元素允许子元素的尺寸范围,如果子元素从该范围内选择尺寸
        // 那么父元素必须接受并处理子元素。尺寸范围包含最大最小高度和最大最小宽度,当最大最小高度/宽度相等的时候表示高度/宽度为确定值
        // minWidth,MaxWidth = 1440
        // minHeight,MaxHeight = 200px
        //modifier.height = 50.dp = 200px
        //modifier.fillMaxWidth = 1440px
        
        val placeables = measurables.map { it.measure(constraints) }
        val placeables = measurables.map {
            it.measure(Constrains(0,constraints.width,0,constraints.height))
        }
        // 2. 计算总高度和最大宽度
        val totalHeight = placeables.sumOf { it.height }
        val maxWidth = placeables.maxOfOrNull { it.width } ?: 0
        // 3. 决定当前布局的尺寸
        layout(maxWidth, totalHeight) {
            var yPosition = 0
            // 4. 摆放每个子项:水平居中,垂直依次排列
            placeables.forEach { placeable ->
                val xPosition = (maxWidth - placeable.width) / 2
                placeable.placeRelative(x = xPosition, y = yPosition)
                yPosition += placeable.height
            }
        }
    }
}
// 使用
VerticalCenteredLayout(Modifier.fillMaxSize()) {
    Text("第一行")
    Text("第二行稍长的文字")
    Button(onClick = {}) { Text("按钮") }
}

核心:在 Layoutmeasure 块中,手动测量 (measurables[i].measure(constraints)) 和摆放 (placeable.placeRelative(x, y)) 每一个子组件。

💡 将“修饰”行为 抽象:自定义 Modifier

如果你想创建一个可复用的行为样式(如统一的点击涟漪、边框),可以自定义 Modifier

示例:创建一个添加统一阴影和圆角的Modifier

fun Modifier.customCardStyle() = this
    .shadow(elevation = 8.dp, shape = RoundedCornerShape(12.dp))
    .background(
        color = MaterialTheme.colorScheme.surface,
        shape = RoundedCornerShape(12.dp)
    )
    .clip(RoundedCornerShape(12.dp))
// 使用
Box(modifier = Modifier.customCardStyle().size(100.dp)) {
    // 内容...
}

🎯 总结:从传统View到Compose思维的转变

传统 View 系统Jetpack Compose
继承 View,重写 onDraw, onMeasure创建 @Composable 函数,组合或绘制
使用 CanvasPaint 对象DrawScope 作用域内调用绘图指令
onLayout 中计算子View位置Layout 可组合项的测量块中摆放子项
通过 setOnClickListener 添加交互使用 Modifier.clickable
自定义属性通过XML或构造函数设置通过函数参数传递,支持默认值和状态提升

简单来说,在Compose中,一切自定义UI都是可组合函数。你的核心工具是:

  1. 组合:用现有组件搭积木。
  2. 绘制:用 Canvas 画自定义图形。
  3. 布局:用 Layout 定义摆放规则。
  4. 修饰:用 Modifier 添加行为和样式。

如果你的“复杂View”有更具体的需求(比如需要处理复杂手势、实现动画效果,或对性能有极高要求),我们可以进一步深入探讨。