Compose Canvas 自定义绘制:图形、路径与动画

2024-05-12 · 28 min · 自定义绘制

当内置组件无法满足设计需求时,你需要使用 Canvas 进行自定义绘制。Compose 的 Canvas API 提供了强大而简洁的绘制能力,让你可以创建任意复杂的图形和动画效果。

一、Canvas 基础

Canvas Composable

@Composable
fun SimpleCanvas() {
    Canvas(
        modifier = Modifier
            .fillMaxWidth()
            .height(200.dp)
    ) {
        // this: DrawScope
        // size: Size - 画布尺寸
        // center: Offset - 画布中心点
        
        drawCircle(
            color = Color.Blue,
            radius = 100f,
            center = center
        )
    }
}

二、基础图形绘制

绘制矩形

Canvas(modifier = Modifier.size(300.dp)) {
    // 填充矩形
    drawRect(
        color = Color.Blue,
        topLeft = Offset(20f, 20f),
        size = Size(200f, 100f)
    )
    
    // 描边矩形
    drawRect(
        color = Color.Red,
        topLeft = Offset(20f, 140f),
        size = Size(200f, 100f),
        style = Stroke(width = 4f)
    )
    
    // 圆角矩形
    drawRoundRect(
        color = Color.Green,
        cornerRadius = CornerRadius(16f)
    )
}

绘制圆形和弧形

Canvas(modifier = Modifier.size(300.dp)) {
    // 圆形
    drawCircle(
        color = Color.Blue,
        radius = 80f,
        center = Offset(100f, 100f)
    )
    
    // 弧线
    drawArc(
        color = Color.Red,
        startAngle = 0f,
        sweepAngle = 270f,
        useCenter = false,
        style = Stroke(width = 8f, cap = StrokeCap.Round)
    )
}

三、Path 路径绘制

Path 是自定义绘制的核心,可以创建任意复杂的形状。

基本 Path 操作

Canvas(modifier = Modifier.size(300.dp)) {
    val path = Path().apply {
        moveTo(50f, 200f)       // 移动到起点
        lineTo(150f, 50f)      // 直线到下一点
        lineTo(250f, 200f)
        close()                  // 闭合路径
    }
    
    drawPath(path = path, color = Color.Blue)
}

贝塞尔曲线

val path = Path().apply {
    moveTo(50f, 200f)
    
    // 二次贝塞尔曲线
    quadraticBezierTo(
        x1 = 150f, y1 = 0f,   // 控制点
        x2 = 250f, y2 = 200f  // 终点
    )
}

val cubicPath = Path().apply {
    moveTo(50f, 350f)
    
    // 三次贝塞尔曲线
    cubicTo(
        x1 = 100f, y1 = 200f,  // 控制点1
        x2 = 200f, y2 = 500f,  // 控制点2
        x3 = 250f, y3 = 350f   // 终点
    )
}

四、渐变与画刷

Canvas(modifier = Modifier.size(300.dp)) {
    // 线性渐变
    val linearGradient = Brush.linearGradient(
        colors = listOf(Color.Red, Color.Yellow, Color.Green),
        start = Offset.Zero,
        end = Offset(size.width, size.height)
    )
    drawRect(brush = linearGradient)
    
    // 径向渐变
    val radialGradient = Brush.radialGradient(
        colors = listOf(Color.Yellow, Color.Transparent),
        center = center,
        radius = size.minDimension / 2
    )
    drawCircle(brush = radialGradient)
}

五、实战:环形进度条

@Composable
fun CircularProgressIndicator(
    progress: Float,  // 0f - 1f
    strokeWidth: Dp = 8.dp,
    trackColor: Color = Color.LightGray,
    progressColor: Color = Color.Blue
) {
    Canvas(modifier = Modifier.size(100.dp)) {
        val stroke = strokeWidth.toPx()
        val radius = (size.minDimension - stroke) / 2
        
        // 绘制轨道
        drawCircle(
            color = trackColor,
            radius = radius,
            style = Stroke(width = stroke)
        )
        
        // 绘制进度
        drawArc(
            color = progressColor,
            startAngle = -90f,
            sweepAngle = 360f * progress,
            useCenter = false,
            style = Stroke(width = stroke, cap = StrokeCap.Round)
        )
    }
}

六、Canvas 动画

@Composable
fun RotatingArc() {
    val infiniteTransition = rememberInfiniteTransition(label = "rotation")
    val rotation by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing)
        ),
        label = "rotation"
    )
    
    Canvas(modifier = Modifier.size(100.dp)) {
        rotate(rotation) {
            drawArc(
                color = Color.Blue,
                startAngle = 0f,
                sweepAngle = 270f,
                useCenter = false,
                style = Stroke(width = 8f, cap = StrokeCap.Round)
            )
        }
    }
}

七、性能优化

使用 drawWithCache

@Composable
fun CachedDrawing() {
    Box(
        modifier = Modifier
            .size(200.dp)
            .drawWithCache {
                // 这里的计算只在尺寸变化时执行
                val path = Path().apply { ... }
                val brush = Brush.linearGradient(...)
                
                onDrawBehind {
                    // 这里每帧执行,但使用缓存的对象
                    drawPath(path, brush)
                }
            }
    )
}

💡 性能建议

避免在 Canvas lambda 中创建对象(Path、Brush 等),使用 remember 或 drawWithCache 缓存复杂计算。

总结