Jetpack Compose Canvas 详解

1,450 阅读7分钟

Canvas 是 Jetpack Compose 中的一个 Composable 函数,可以使用 Canvas 来绘制各种形状,文本和路径。

@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
    Spacer(modifier.drawBehind(onDraw))
  • modifier:修饰外观和行为,Modifier 可用于设置大小,填充,对齐,背景颜色等属性。
  • onDraw:执行具体的绘制操作。

drawLine

drawLine 用于绘制一条线,如下所示:

fun drawLine(
    color: Color,
    start: Offset,
    end: Offset,
    strokeWidth: Float = Stroke.HairlineWidth,
    cap: StrokeCap = Stroke.DefaultCap,
    pathEffect: PathEffect? = null,
    /*FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
  • color:线的颜色。
  • start:线的起点坐标,使用 Offset 类型表示,即 (x, y) 坐标。
  • end:线的终点坐标,同样使用 Offset 类型表示。
  • strokeWidth:线的宽度。
  • cap:线的端点样式,可以选择 StrokeCap.Butt,StrokeCap.Round 或 StrokeCap.Square 中的一种。
  • pathEffect:路径效果,可以使用特定的路径效果对象,如绘制虚线可以用 PathEffect.dashPathEffect。
  • alpha:线的透明度,取值范围为 0.0 到 1.0,0.0 表示完全透明,1.0 表示完全不透明。
  • colorFilter:颜色过滤器,可以使用颜色过滤器对象来调整线的颜色。
  • blendMode:用于指定绘制线条时所使用的混合模式,如 BlendMode.SrcOver、BlendMode.DstIn 等。

举个例子,绘制一条对角的虚线。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawLine(
            color = Color.Red,
            start = Offset(0f, 0f),
            end = Offset(size.width, size.height),
            strokeWidth = 6f,
            cap = StrokeCap.Round,
            // 20f表示虚线的宽度,10f表示间隔宽度,5f表示初始的偏移距离
            pathEffect = PathEffect.dashPathEffect(floatArrayOf(20f, 10f), 5f)
        )
    }
}

image.png

drawRect 和 drawRoundRect

drawRect 用于绘制矩形,如下所示:

fun drawRect(
    color: Color,
    topLeft: Offset = Offset.Zero,
    size: Size = this.size.offsetSize(topLeft),
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
  • color:矩形的颜色
  • topLeft:表示矩形左上角的坐标。默认值是 Offset.Zero,即画布的原点 (0, 0)。
  • size:矩形的宽度和高度。
  • alpha:透明度,范围在 0.0 到 1.0 之间。0.0 表示完全透明,1.0 表示完全不透明。
  • style:绘制样式,可以是 Fill(填充矩形)或 Stroke(仅绘制矩形边框)。
  • colorFilter:用于修改绘制内容的颜色。
  • blendMode:混合模式,控制源颜色如何与目标颜色混合。

举个例子,绘制一个红色的矩形边框。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawRect(
            color = Color.Red,
            topLeft = Offset(50f, 50f),
            size = Size(200f, 200f),
            alpha = 0.5f,
            style = Stroke(width = 10f)
        )
    }
}

image.png

如果想要绘制一个圆角的矩形,可以使用 drawRoundRect,绘制跟 drawRect 几乎无异,就是多了个 cornerRadius 参数表示圆角的半径,可以分别指定 x 轴和 y 轴的半径。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawRoundRect(
            color = Color.Red,
            topLeft = Offset(50f, 50f),
            size = Size(200f, 200f),
            alpha = 0.5f,
            style = Stroke(width = 10f),
            cornerRadius = CornerRadius(16f, 16f)
        )
    }
}

image.png

drawCircle

drawCircle 用于绘制圆形,如下所示:

fun drawCircle(
    color: Color,
    radius: Float = size.minDimension / 2.0f,
    center: Offset = this.center,
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)

其中 center 是指圆心的位置,radius 是指半径大小。

举个例子,在屏幕中心绘制一个红色的圆。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawCircle(
            color = Color.Red,
            radius = 100f,
            center = Offset(size.width / 2, size.height / 2)
        )
    }
}

image.png

drawOval

drawOval 用于绘制椭圆,如下所示:

fun drawOval(
    color: Color,
    topLeft: Offset = Offset.Zero,
    size: Size = this.size.offsetSize(topLeft),
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)

举个例子,在 (100, 100) 位置绘制了一个红色的椭圆,宽度为 100,高度为 150。

@Composable
fun MyCanvas() {
    //在 (100, 100) 位置绘制了一个红色的椭圆,宽度为 100,高度为 150。
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawOval(
            color = Color.Red,
            topLeft = Offset(100f, 100f),
            size = Size(100f, 150f)
        )
    }
}

image.png

drawArc

drawArc 用于绘制弧形或扇形,如下所示:

fun drawArc(
    color: Color,
    startAngle: Float,
    sweepAngle: Float,
    useCenter: Boolean,
    topLeft: Offset = Offset.Zero,
    size: Size = this.size.offsetSize(topLeft),
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
  • startAngle:表示弧线的起始角度,以度为单位,0 度表示水平向右方向,顺时针增加角度。例如:0f 表示从水平向右方向开始,90f 表示从垂直向上方向开始。
  • sweepAngle:表示弧线的扫过的角度,以度为单位。正值表示顺时针方向,负值表示逆时针方向。例如:90f 表示从起始角度开始顺时针旋转 90 度。
  • useCenter: 表示是否连接弧线的起点和终点以形成一个封闭的区域。如果为 true,则会连接起点和终点,形成一个扇形,如果为 false,则只绘制弧线而不连接起点和终点。

举个例子,绘制一个180度的扇形。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawArc(
            color = Color.Red,
            startAngle = 0f,
            sweepAngle = 180f,
            useCenter = true,
            topLeft = Offset(100f, 100f),
            size = Size(150f, 150f),
            style = Stroke(width = 10f),
            blendMode = BlendMode.SrcOver
        )
    }
}

image.png

drawImage

drawImage 用于绘制图片,如下所示:

fun drawImage(
    image: ImageBitmap,
    srcOffset: IntOffset = IntOffset.Zero,
    srcSize: IntSize = IntSize(image.width, image.height),
    dstOffset: IntOffset = IntOffset.Zero,
    dstSize: IntSize = srcSize,
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode,
    filterQuality: FilterQuality = DefaultFilterQuality
)
  • image:要绘制的位图图像
  • srcOffset:源图像中裁剪区域的左上角偏移
  • srcSize:源图像中裁剪区域的大小
  • dstOffset:目标画布上绘制位置的左上角偏移
  • dstSize:目标画布上绘制区域的大小

举个例子,在画布的 (150, 150) 位置绘制,并将其缩放到 200x200 像素上,完整的显示图片。

@Composable
fun MyCanvas() {
    val imageBitmap = ImageBitmap.imageResource(id = R.drawable.test_img)
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawImage(
            image = imageBitmap,
            dstOffset = IntOffset(150, 150),
            dstSize = IntSize(200, 200),
        )
    }
}

image.png

如果此时想从源图像的 (20, 20) 位置裁剪出一个 100x100 像素的区域,可以这样干:

@Composable
fun MyCanvas() {
    val imageBitmap = ImageBitmap.imageResource(id = R.drawable.test_img)
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawImage(
            image = imageBitmap,
            srcOffset = IntOffset(20, 20),
            srcSize = IntSize(100, 100),
            dstOffset = IntOffset(150, 150),
            dstSize = IntSize(200, 200),
        )
    }
}

image.png

drawPath

drawPath 用于绘制路径,如下所示:

fun drawPath(
    path: Path,
    color: Color,
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    style: DrawStyle = Fill,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)

path 就是要绘制的路径对象,举个例子,画一个闪电线。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val path = Path().apply {
            moveTo(200f, 0f) //移动到起点
            lineTo(100f, 100f) //从起点画一条线到(100, 100)
            lineTo(300f, 100f) //再画一条线到(300, 100)
            lineTo(100f, 300f) //再画一条线到(100, 300)
        }
        drawPath(
            path = path,
            color = Color.Red,
            style = Stroke(width = 6f) 
        )
    }
}

image.png

如果想闭合路径,回到起点的话,可以最后加上 close()。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val path = Path().apply {
            moveTo(200f, 0f) //移动到起点
            lineTo(100f, 100f) //从起点画一条线到(100, 100)
            lineTo(300f, 100f) //再画一条线到(300, 100)
            lineTo(100f, 300f) //再画一条线到(100, 300)
            close()
        }
        drawPath(
            path = path,
            color = Color.Red,
            style = Stroke(width = 6f) // 填充路径
        )
    }
}

image.png

drawPoints

drawPoints 用于绘制点,如下所示:

fun drawPoints(
    points: List<Offset>,
    pointMode: PointMode,
    color: Color,
    strokeWidth: Float = Stroke.HairlineWidth,
    cap: StrokeCap = StrokeCap.Butt,
    pathEffect: PathEffect? = null,
    /*@FloatRange(from = 0.0, to = 1.0)*/
    alpha: Float = 1.0f,
    colorFilter: ColorFilter? = null,
    blendMode: BlendMode = DefaultBlendMode
)
  • points:这是一个包含 Offset 对象的列表,表示要绘制的点的位置。
  • pointMode:绘制点的模式。PointMode.Points 表示单独绘制每个点,PointMode.Lines 表示绘制相邻点之间的连线。
  • color:指定绘制点或线的颜色。
  • strokeWidth:绘制线条时使用的宽度。
  • cap: 指定线条的端点样式。StrokeCap.Butt 表示端点是平直的,StrokeCap.Round 表示端点是圆形的StrokeCap.Square 表示端点是方形的。

举个例子,绘制一些平行于 X 轴的点和线。

@Composable
fun MyCanvas() {
    Canvas(modifier = Modifier.fillMaxSize()) {
        val points = listOf(
            Offset(100f, 100f),
            Offset(200f, 100f),
            Offset(300f, 100f),
            Offset(400f, 100f),
            Offset(500f, 100f)
        )
        //绘制单个点集合
        drawPoints(
            points = points,
            pointMode = PointMode.Points,
            color = Color.Red,
            strokeWidth = 20f,
            cap = StrokeCap.Round,
        )

        val connectedPoints = listOf(
            Offset(100f, 200f),
            Offset(200f, 200f),
            Offset(300f, 200f),
            Offset(400f, 200f),
            Offset(500f, 200f)
        )
        //绘制连接线
        drawPoints(
            points = connectedPoints,
            pointMode = PointMode.Lines,
            color = Color.Red,
            strokeWidth = 6f,
            cap = StrokeCap.Round,
        )
    }
}

image.png

drawText

drawText 用于绘制文字,如下所示:

@ExperimentalTextApi
fun DrawScope.drawText(
    textLayoutResult: TextLayoutResult,
    color: Color = Color.Unspecified,
    topLeft: Offset = Offset.Zero,
    alpha: Float = Float.NaN,
    shadow: Shadow? = null,
    textDecoration: TextDecoration? = null,
    drawStyle: DrawStyle? = null,
    blendMode: BlendMode = DrawScope.DefaultBlendMode
) 
  • textLayoutResult:文本布局的结果,它包含了绘制文本的信息。
  • color:文本的颜色,如果不指定,将使用 TextLayoutResult 中设定的颜色。
  • topLeft:文本绘制的起始位置(左上角)。
  • shadow:文本的阴影效果。
  • textDecoration:文本的装饰,如 TextDecoration.Underline(下划线)或 TextDecoration.LineThrough(删除线)

举个例子,绘制一段带阴影的文字。

@OptIn(ExperimentalTextApi::class)
@Composable
fun MyCanvas() {
    val textMeasurer = rememberTextMeasurer()
    val textLayoutResult = textMeasurer.measure(
        text = "Hello, Compose!",
        style = TextStyle(fontSize = 20.sp),
    )
    Canvas(modifier = Modifier.fillMaxSize()) {
        drawText(
            textLayoutResult = textLayoutResult,
            color = Color.Red,
            topLeft = Offset(50f, 50f),
            shadow = Shadow(
                color = Color.Gray, //阴影颜色
                offset = Offset(6f, 6f),//偏移量
                blurRadius = 4f //阴影模糊半径
            ),
            textDecoration = TextDecoration.Underline,
            blendMode = DrawScope.DefaultBlendMode
        )
    }
}

image.png