如何在 Jetpack Compose 中绘制控件

270 阅读4分钟

在 Jetpack Compose 中绘制自定义图形,最核心的方式是使用 Canvas 可组合项 (Composable)。Canvas 提供了一个二维绘图环境,你可以在其中使用各种绘制命令来创建你想要的任何视觉效果。

以下是如何在 Compose 中进行自定义绘制的步骤和关键概念:

1. 使用 Canvas Composable:

Canvas 是一个 Composable 函数,它接收一个 modifier 来定义其尺寸(非常重要,否则画布没有大小),以及一个 onDraw lambda 函数。所有的绘制操作都在这个 onDraw lambda 的 DrawScope 上下文中执行。

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color // 导入 Color
import androidx.compose.ui.graphics.drawscope.DrawScope // 导入 DrawScope
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun MyCustomDrawing() {
    Canvas(
        modifier = Modifier.fillMaxSize() // 给 Canvas 指定大小,这里填满父布局
    ) {
        // 所有的绘制代码都写在这里
        // 'this' 指向 DrawScope
        drawRect(color = Color.Blue) // 示例:绘制一个填满画布的蓝色矩形
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewMyCustomDrawing() {
    MyCustomDrawing()
}

2. 理解 DrawScope:

onDraw lambda 的接收者类型是 DrawScope。它提供了:

  • 绘图函数:drawLine, drawRect, drawCircle, drawOval, drawArc, drawPath, drawImage, drawText 等。

  • 画布信息: 最重要的是 size 属性,它是一个 Size 对象,包含画布的 widthheight。你可以基于这个 size 来进行相对定位和尺寸计算。

  • 变换操作:translate, rotate, scale, inset 等,可以改变绘制坐标系。

3. 常用的绘制函数 (DrawScope 内):

  • 绘制线条:

  • drawLine(
        color = Color.Red,
        start = Offset(x = 0f, y = 0f), // 起点坐标 (x, y)
        end = Offset(x = size.width, y = size.height), // 终点坐标
        strokeWidth = 5f // 线条宽度
    )
    
  • 绘制矩形:

  • drawRect(
        color = Color.Green,
        topLeft = Offset(x = size.width / 4, y = size.height / 4), // 左上角坐标
        size = Size(width = size.width / 2, height = size.height / 2) // 矩形尺寸
        // style = Fill // 默认是填充 (Fill)
    )
    // 绘制描边矩形
    drawRect(
        color = Color.Black,
        topLeft = Offset(x = 10f, y = 10f),
        size = Size(width = 100f, height = 50f),
        style = androidx.compose.ui.graphics.drawscope.Stroke(width = 3f) // 使用 Stroke 进行描边
    )
    
  • 绘制圆形:

  • drawCircle(
        color = Color.Magenta,
        radius = size.minDimension / 4, // 半径,取宽高最小值的一半
        center = center // center 是 DrawScope 提供的画布中心 Offset
        // style = Fill // 默认填充
    )
    
  • 绘制椭圆:

  • drawOval(
        color = Color.Yellow,
        topLeft = Offset(x = 50f, y = 200f),
        size = Size(width = 200f, height = 100f) // 定义椭圆的外切矩形
    )
    
  • 绘制圆弧:

  • drawArc(
        color = Color.Cyan,
        startAngle = 0f,       // 起始角度 (0度是3点钟方向)
        sweepAngle = 180f,     // 扫过的角度 (顺时针)
        useCenter = true,      // 是否连接到圆心,形成扇形 (true) 或弓形 (false)
        topLeft = Offset(x = size.width * 0.1f, y = size.height * 0.6f),
        size = Size(width = size.width * 0.8f, height = size.width * 0.8f) // 圆弧所在椭圆的外切矩形
    )
    

4. 使用 Path 绘制复杂图形:

对于不规则或复杂的形状,使用 PathPath 对象允许你定义一系列的线段、曲线等来构建形状。

import androidx.compose.ui.graphics.Path

// ... 在 Canvas 的 onDraw 中 ...
val path = Path().apply {
    moveTo(x = size.width / 2, y = 0f) // 移动到起点
    lineTo(x = size.width, y = size.height / 2) // 画线到...
    lineTo(x = size.width / 2, y = size.height)
    lineTo(x = 0f, y = size.height / 2)
    close() // 闭合路径 (连接起点和终点)
}
drawPath(
    path = path,
    color = Color.DarkGray
)

Path 支持 moveTo, lineTo, quadraticBezierTo (二次贝塞尔曲线), cubicTo (三次贝塞尔曲线), addRect, addOval 等多种命令。

5. 使用 Brush (画笔) 实现渐变等效果:

除了纯色 (Color),你还可以使用 Brush 来填充或描边,创建渐变效果。

import androidx.compose.ui.graphics.Brush

// ... 在 Canvas 的 onDraw 中 ...
val gradientBrush = Brush.linearGradient(
    colors = listOf(Color.Red, Color.Blue),
    start = Offset.Zero, // 渐变起始点
    end = Offset(x = size.width, y = size.height) // 渐变结束点
)
drawCircle(
    brush = gradientBrush, // 使用画笔代替纯色
    radius = size.minDimension / 3,
    center = center
)

val radialBrush = Brush.radialGradient(
    colors = listOf(Color.Yellow, Color.Transparent),
    center = center,
    radius = size.minDimension / 5
)
drawRect(brush = radialBrush)

还有 Brush.radialGradient (径向渐变) 和 Brush.sweepGradient (扫描渐变)。

6. 结合状态和交互:

  • 响应状态变化: 如果你的绘图依赖于某些状态(例如,滑块位置、按钮点击次数),将这些状态用 remember { mutableStateOf(...) } 或通过 ViewModel 管理。当状态改变时,Compose 会触发重组 (recomposition),CanvasonDraw 会重新执行,使用新的状态值进行绘制。
  • 添加交互: 你可以在 Canvas 上应用 Modifier.pointerInput 来检测触摸事件(点击、拖动等),然后更新状态,从而动态地改变绘制内容。

示例:绘制一个简单的同心圆

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun ConcentricCircles() {
    Canvas(modifier = Modifier.size(200.dp)) { // 固定大小的 Canvas
        val canvasWidth = size.width
        val canvasHeight = size.height
        val center = Offset(x = canvasWidth / 2, y = canvasHeight / 2)
        val strokeWidth = 10f

        // 外圆 (红色描边)
        drawCircle(
            color = Color.Red,
            center = center,
            radius = size.minDimension / 2 - strokeWidth / 2, // 考虑描边宽度
            style = Stroke(width = strokeWidth)
        )

        // 中圆 (蓝色描边)
        drawCircle(
            color = Color.Blue,
            center = center,
            radius = size.minDimension / 3 - strokeWidth / 2,
            style = Stroke(width = strokeWidth)
        )

        // 内圆 (绿色填充)
        drawCircle(
            color = Color.Green,
            center = center,
            radius = size.minDimension / 6
            // style = Fill // 默认填充
        )
    }
}

@Preview(showBackground = true)
@Composable
fun PreviewConcentricCircles() {
    ConcentricCircles()
}

总结:

使用 Canvas 可组合项是 Jetpack Compose 中进行自定义图形绘制的主要方式。通过理解 DrawScope 提供的绘图函数、画布信息以及结合 PathBrush,你可以创建出丰富多样的自定义视觉效果。记住,始终要为 Canvas 提供一个 Modifier 来确定其尺寸。