Compose 中的绘制功能简介

Introduction to drawing in Compose

全面了解如何在 Compose 中绘制自定义内容。

1. Introduction to drawing in Compose

当内置组件不能完全满足应用需求时,自定义绘图很有用。

drawBehind

drawBehind 允许我们在 Composables 内容后面绘制内容。例如:

kotlin 复制代码
Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind({
            // this = DrawScope
            drawCircle(Color.Magenta)
        })
)
kotlin 复制代码
@Composable
@NonRestartableComposable
fun Spacer(modifier: Modifier) {
    Layout(measurePolicy = SpacerMeasurePolicy, modifier = modifier)
}

2. What is DrawScope?

DrawScope 接口部分源码:

kotlin 复制代码
@DrawScopeMarker
@JvmDefaultWithCompatibility
interface DrawScope : Density {

    val drawContext: DrawContext

    val center: Offset
        get() = drawContext.size.center

    val size: Size
        get() = drawContext.size

    val layoutDirection: LayoutDirection

    fun drawLine(
        brush: Brush,
        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
    )

    fun drawRect(
        brush: Brush,
        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
    )

    fun drawImage(
        image: ImageBitmap,
        topLeft: Offset = Offset.Zero,
        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
        style: DrawStyle = Fill,
        colorFilter: ColorFilter? = null,
        blendMode: BlendMode = DefaultBlendMode
    )

    @Deprecated(
        "Prefer usage of drawImage that consumes an optional FilterQuality parameter",
        level = DeprecationLevel.HIDDEN,
        replaceWith = ReplaceWith(
            "drawImage(image, srcOffset, srcSize, dstOffset, dstSize, alpha, style, " +
                "colorFilter, blendMode, FilterQuality.Low)",
            "androidx.compose.ui.graphics.drawscope",
            "androidx.compose.ui.graphics.FilterQuality"
        )
    ) // Binary API compatibility.
    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
    )


    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
    ) {
        drawImage(
            image = image,
            srcOffset = srcOffset,
            srcSize = srcSize,
            dstOffset = dstOffset,
            dstSize = dstSize,
            alpha = alpha,
            style = style,
            colorFilter = colorFilter,
            blendMode = blendMode
        )
    }

    fun drawRoundRect(
        brush: Brush,
        topLeft: Offset = Offset.Zero,
        size: Size = this.size.offsetSize(topLeft),
        cornerRadius: CornerRadius = CornerRadius.Zero,
        @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f,
        style: DrawStyle = Fill,
        colorFilter: ColorFilter? = null,
        blendMode: BlendMode = DefaultBlendMode
    )

    fun drawCircle(
        brush: Brush,
        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
    )

    fun drawOval(
        brush: Brush,
        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
    )

    fun drawArc(
        brush: Brush,
        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
    )
    
    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
    )

    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
    )
    ...
}

3. Canvas Composable

如果我们想要一个简单的绘图,可以使用 Canvas Composable。

kotlin 复制代码
Canvas(modifier = Modifier, onDraw = {

})

看源码可以发现,Canvas Composable 只是 drawBehind 的便捷包装。

kotlin 复制代码
@Composable
fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =
    Spacer(modifier.drawBehind(onDraw))

4. Drawing Modifiers

Drawing in Compose

  • Modifier.drawBehind

  • Modifier.drawWithContent

  • Modifier.drawWithCache

DrawScope 上的所有绘图功能都为绘图命令的大小和位置设置了默认值,当我们需要更改它们时,可以轻松覆盖它们。

5. Coordinate System

使用像素为单位执行绘制和布局,而不是用 dp。

绘制调用也始终与父可组合项相关。

修改中心点的坐标:

修改圆的半径:

6. DrawScope Transformations

scale

kotlin 复制代码
inline fun DrawScope.scale(
    scaleX: Float,
    scaleY: Float,
    pivot: Offset = center,
    block: DrawScope.() -> Unit
) = withTransform({ scale(scaleX, scaleY, pivot) }, block)

translate

kotlin 复制代码
inline fun DrawScope.translate(
    left: Float = 0.0f,
    top: Float = 0.0f,
    block: DrawScope.() -> Unit
) {
    drawContext.transform.translate(left, top)
    try {
        block()
    } finally {
        drawContext.transform.translate(-left, -top)
    }
}

rotate

kotlin 复制代码
inline fun DrawScope.rotate(
    degrees: Float,
    pivot: Offset = center,
    block: DrawScope.() -> Unit
) = withTransform({ rotate(degrees, pivot) }, block)

inset

kotlin 复制代码
inline fun DrawScope.inset(
    left: Float,
    top: Float,
    right: Float,
    bottom: Float,
    block: DrawScope.() -> Unit
) {
    drawContext.transform.inset(left, top, right, bottom)
    try {
        block()
    } finally {
        drawContext.transform.inset(-left, -top, -right, -bottom)
    }
}

Multiple transformations

使用 withTransform

kotlin 复制代码
inline fun DrawScope.withTransform(
    transformBlock: DrawTransform.() -> Unit,
    drawBlock: DrawScope.() -> Unit
) = with(drawContext) {
    // Transformation can include inset calls which change the drawing area
    // so cache the previous size before the transformation is done
    // and reset it afterwards
    val previousSize = size
    canvas.save()
    try {
        transformBlock(transform)
        drawBlock()
    } finally {
        canvas.restore()
        size = previousSize
    }
}

7. Example - Line Graph

8. drawLine background

首先绘制背景

kotlin 复制代码
@Composable
fun LineGraph() {
    Box(
        modifier = Modifier
            .background(Purple40)
            .fillMaxSize()
    ) {
        Canvas(
            modifier = Modifier
                .padding(8.dp)
                .aspectRatio(3 / 2f)
                .fillMaxSize()
                .align(Alignment.Center)
        ) {
            val barWidthPx = 1.dp.toPx()
            drawRect(Color.White, style = Stroke(barWidthPx))

            val verticalLines = 4
            val verticalSize = size.width / (verticalLines + 1)
            repeat(verticalLines) { i ->
                val startX = verticalSize * (i + 1)
                drawLine(
                    Color.White,
                    start = Offset(startX, 0f),
                    end = Offset(startX, size.height),
                    strokeWidth = barWidthPx
                )
            }

            val horizontalLines = 3
            val sectionSize = size.height / (horizontalLines + 1)
            repeat(horizontalLines) { i ->
                val startY = sectionSize * (i + 1)
                drawLine(
                    Color.White,
                    start = Offset(0f, startY),
                    end = Offset(size.width, startY),
                    strokeWidth = barWidthPx
                )
            }
        }
    }
}

9. drawPath

绘制代表数据的实际线条。

我们不想在每次重新组合时都重新创建这个路径对象。为了避免这种情况,我们可以切换到使用 drawWithCache 修饰符,它将负责缓存对象,直到绘图区域的大小发生变化。

创建路径数据:

kotlin 复制代码
data class Balance(
    val x: Int,
    val y: Int
)


val graphData = mutableListOf<Balance>(
    Balance(0, 0),
    Balance(1, 45),
    Balance(2, 34),
    Balance(3, 9),
    Balance(4, 21),
    Balance(5, 36),
    Balance(6, 4),
    Balance(7, 4),
    Balance(8, 54),
    Balance(9, 87),
    Balance(10, 81),
    Balance(11, 51),
    Balance(12, 95),
    Balance(13, 52),
    Balance(14, 42),
    Balance(15, 93),
    Balance(16, 8),
    Balance(17, 11),
    Balance(18, 63),
    Balance(19, 52),
    Balance(20, 7),
    Balance(21, 12),
    Balance(22, 90),
    Balance(23, 74),
    Balance(24, 8),
    Balance(25, 99),
    Balance(26, 75),
    Balance(27, 92),
    Balance(28, 61),
    Balance(29, 11),
    Balance(30, 21),
    Balance(31, 60),
    Balance(32, 74),
    Balance(33, 89),
    Balance(34, 74),
    Balance(35, 82),
    Balance(36, 23),
    Balance(37, 8),
    Balance(38, 47),
    Balance(39, 18),
    Balance(40, 4),
    Balance(41, 55),
    Balance(42, 54),
    Balance(43, 61),
    Balance(44, 34),
    Balance(45, 23),
    Balance(46, 47),
    Balance(47, 64),
    Balance(48, 66),
    Balance(49, 35),
    Balance(50, 8),
    Balance(51, 15),
    Balance(52, 89),
    Balance(53, 13),
    Balance(54, 24),
    Balance(55, 53),
    Balance(56, 57),
    Balance(57, 23),
    Balance(58, 86),
    Balance(59, 68),
    Balance(60, 80),
    Balance(61, 60),
    Balance(62, 90),
    Balance(63, 54),
    Balance(64, 41),
    Balance(65, 40),
    Balance(66, 62),
    Balance(67, 24),
    Balance(68, 32),
    Balance(69, 30),
    Balance(70, 91),
    Balance(71, 83),
    Balance(72, 84),
    Balance(73, 91),
    Balance(74, 23),
    Balance(75, 48),
    Balance(76, 32),
    Balance(77, 13),
    Balance(78, 74),
    Balance(79, 16),
    Balance(80, 37),
    Balance(81, 3),
    Balance(82, 4),
    Balance(83, 8),
    Balance(84, 16),
    Balance(85, 82),
    Balance(86, 5),
    Balance(87, 34),
    Balance(88, 61),
    Balance(89, 84),
    Balance(90, 13),
    Balance(91, 38),
    Balance(92, 32),
    Balance(93, 54),
    Balance(94, 35),
    Balance(95, 78),
    Balance(96, 58),
    Balance(97, 50),
    Balance(98, 31),
    Balance(99, 9),
    Balance(100, 71)
)

fun generatePath(data: List<Balance>, size: Size): Path {
    val path = Path()
    data.forEachIndexed { i, balance ->
        // 处理路径起始点不从 (0,0) 开始
        if (i == 0) {
            path.moveTo(0f, size.height)
        }
        val x = (balance.x / 100f) * size.width
        val y = size.height - ((balance.y / 100f) * size.height)
        // 将途经点添加到路径中
        path.lineTo(x, y)
    }
    return path
}
kotlin 复制代码
Spacer(
    modifier = Modifier
        .padding(8.dp)
        .aspectRatio(3 / 2f)
        .fillMaxSize()
        .align(Alignment.Center)
        .drawWithCache({
            val path = generatePath(graphData, size)

            onDrawBehind {
                drawPath(path, Color.Green, style = Stroke(2.dp.toPx()))
            }
        })
)

10. Filling graph area

kotlin 复制代码
Spacer(
    modifier = Modifier
        .padding(8.dp)
        .aspectRatio(3 / 2f)
        .fillMaxSize()
        .align(Alignment.Center)
        .drawWithCache({
            val path = generatePath(graphData, size)
            val filledPath = Path()
            filledPath.addPath(path)
            // 封闭右侧
            filledPath.lineTo(size.width, size.height)
            // 封闭底部
            filledPath.lineTo(0f, size.height)
            // 关闭路径
            filledPath.close()
            
            onDrawBehind {
                // 创建渐变刷子
                val brush = Brush.verticalGradient(
                    listOf(
                        Color.Green.copy(alpha = 0.4f),
                        Color.Transparent
                    )
                )
                // 绘制线条路径
                drawPath(path, Color.Green, style = Stroke(2.dp.toPx()))
                // 绘制渐变区域
                drawPath(filledPath, brush = brush, style = Fill)
            }
        })
)

11. Smoothing path lines

使路径线条变得平滑。

修改 generatePath() 方法,生成带有曲线的路径。

kotlin 复制代码
fun generateSmoothPath(data: List<Balance>, size: Size): Path {
    val path = Path()
    var previousBalanceX = 0f
    var previousBalanceY = size.height
    data.forEachIndexed { i, balance ->
        if (i == 0) {
            path.moveTo(0f, size.height)
        }
        val x = (balance.x / 100f) * size.width
        val y = size.height - ((balance.y / 100f) * size.height)
        val controlPoint1 = PointF((x + previousBalanceX) / 2f, previousBalanceY)
        val controlPoint2 = PointF((x + previousBalanceX) / 2f, y)
        path.cubicTo(controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, x, y)
        previousBalanceX = x
        previousBalanceY = y
    }
    return path
}

12. Animate graph drawing

启动图表时为绘制线条添加动画。

创建动画值并启动:

kotlin 复制代码
val animationProgress = remember { Animatable(0f) }

LaunchedEffect(key1 = graphData) {
    animationProgress.animateTo(1f, tween(3000))
}

使用 clipRect 剪辑绘制:

kotlin 复制代码
clipRect(right = size.width * animationProgress.value) {
    val brush = Brush.verticalGradient(
        listOf(
            Color.Green.copy(alpha = 0.4f),
            Color.Transparent
        )
    )

    drawPath(path, Color.Green, style = Stroke(2.dp.toPx()))
    drawPath(filledPath, brush = brush, style = Fill)
}

clipRect 源码实现:

kotlin 复制代码
inline fun DrawScope.clipRect(
    left: Float = 0.0f,
    top: Float = 0.0f,
    right: Float = size.width,
    bottom: Float = size.height,
    clipOp: ClipOp = ClipOp.Intersect,
    block: DrawScope.() -> Unit
) = withTransform({ clipRect(left, top, right, bottom, clipOp) }, block)
相关推荐
我科绝伦(Huanhuan Zhou)5 小时前
【脚本升级】银河麒麟V10一键安装MySQL9.3.0
android·adb
消失的旧时光-19435 小时前
Android回退按钮处理方法总结
android·开发语言·kotlin
叫我龙翔6 小时前
【MySQL】从零开始了解数据库开发 --- 数据表的约束
android·c++·mysql·数据库开发
2501_916013746 小时前
iOS 上架 App 全流程实战,应用打包、ipa 上传、App Store 审核与工具组合最佳实践
android·ios·小程序·https·uni-app·iphone·webview
2501_915106326 小时前
iOS 26 能耗监测全景,Adaptive Power、新电池视图
android·macos·ios·小程序·uni-app·cocoa·iphone
用户2018792831677 小时前
浅谈Android PID与UID原理
android
TimeFine7 小时前
Android AWS KVS WebRTC 通话声道切换到媒体音乐声道
android
用户2018792831678 小时前
Android文件下载完整性保证:快递员小明的故事
android
用户2018792831679 小时前
自定义 View 的 “快递失踪案”:为啥 invalidate () 喊不动 onDraw ()?
android