要么说 Compose 高雅呢,假如你想画个东西,用安卓 View 的话你要继承 View 并且完成其间的 onDraw 办法,然后才干拿到 Canvas 开始制作,但 Compose 你只需求这样:

Canvas(modifier = Modifier.size(100.dp)) {
    // draw in DrawScope
}

十分的自然,没有比这个更自然的工作了。

先看下作用图吧。

使用 Compose 绘制渐变贝塞尔曲线趋势图

贝塞尔曲线

贝塞尔曲线相关的科普网上有十分多的文章,这儿就不具体介绍了,本文首要内容是怎么经过 Compose 制作上面款式的贝塞尔曲线趋势图。

首要简单回顾一下贝塞尔曲线,贝塞尔曲线一般分为一阶贝塞尔曲线(直线),二阶、三阶等更高阶的贝塞尔曲线,经过设置不同的控制点咱们可以得到几乎所有类型的曲线。

使用 Compose 绘制渐变贝塞尔曲线趋势图

咱们可以经过这个特性制作出一些很美丽的曲线出来。

相同 Compose 的 Canvas 也供给了相关的 API。

// Path.kt
// 二阶贝塞尔曲线
fun quadraticBezierTo(x1: Float, y1: Float, x2: Float, y2: Float)
// 三阶贝塞尔曲线
fun cubicTo(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float)

起始点 P0 是 Path 的当时方位,对于初始 Path 来说可以经过 moveTo 办法设置初始方位。

需求分析

已然咱们是期望制作趋势图,那么趋势图应该是包括了一系列的趋势,趋势咱们可以经过 Float 浮点类型来表明。

那么咱们的趋势图就会依据这个浮点数据列表进行制作,因而需求经过一些核算将浮点数转换为坐标值,转换规则也比较简单,核算出最大值和最小值,然后依照每个浮点数的比例分配 x 轴和 y 轴即可。

考虑到上面的款式,如果做的美丽一点的话,最好是运用三阶贝塞尔曲线。

使用 Compose 绘制渐变贝塞尔曲线趋势图

依照这样的方法设置的控制点制作出来的曲线比较美丽。

假如把红点(startPoint)和黄点(endPoint)理解为两个连续的趋势,那么绿点和蓝点便是两个控制点。

绿点的坐标公式为:

val firstControlPoint = Offset(
        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
        y = startPoint.y,
)

蓝点的坐标公式为:

val secondControlPoint = Offset(
        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
        y = endPoint.y,
)

咱们只需依照上面的方法核算出控制点方位,然后设置到三阶贝塞尔曲线,就可以了。

别的,考虑到丰厚多变的需求,咱们尽可能在不增加太多工作量前提下多支撑一些款式。

趋势图首要包括三种款式:

  • 只要趋势线段(作用图一)
  • 只要趋势图上色部分(作用图二)
  • 包括线段和上色区(作用图三)

此外还需求支撑渐变色。

接口设计

首要考虑下款式,上面说了包括三种款式,那咱们可以经过 Kotlin 的 sealed class 来表明款式。

sealed class BezierCurveStyle {
    // 只要趋势图上色部分
    class Fill(val brush: Brush) : BezierCurveStyle()
    // 只要趋势线段
    class CurveStroke(
        val brush: Brush,
        val stroke: Stroke,
    ) : BezierCurveStyle()
    // 包括线段和上色区
    class StrokeAndFill(
        val fillBrush: Brush,
        val strokeBrush: Brush,
        val stroke: Stroke,
    ) : BezierCurveStyle()
}

因为要支撑渐变色,所以色彩经过 Brush 替代,这样的话即使运用者期望只运用纯色也可以运用 SolidColor 来设置参数。

然后咱们定义一个名为 BezierCurve 的 Composable 函数。

@Composable
fun BezierCurve(
    modifier: Modifier,
    points: List<Float>,
    minPoint: Float? = null,
    maxPoint: Float? = null,
    style: BezierCurveStyle,
)

points 便是咱们刚刚说的趋势点,最大值和最小值这儿设置为非必传参数,不传的话咱们默许将这个列表中的最大值最小值当作坐标系的最大最小值。

具体完成

因为制作需求核算坐标值,所以需求先获取到画布的巨细。

var size by remember {
    mutableStateOf(IntSize.Zero)
}
Canvas(
    modifier = modifier.onSizeChanged {
        size = it
    },
    onDraw = {
        if (size != IntSize.Zero && points.size > 1) {
            drawBezierCurve(
                size = size,
                points = points,
                fixedMinPoint = minPoint,
                fixedMaxPoint = maxPoint,
                style = style,
            )
        }
    },
)

接着需求核算每个点的坐标值:

val total = maxPoint - minPoint
val xSpacing = width / (points.size - 1F)
for (index in points.indices) {
    val x = index * xSpacing
    val y = height - height * ((points[index] - minPoint) / total)
}

咱们在制作曲线图时,需求两个分组来制作,也便是制作相邻两个点的曲线。

因而咱们需求遍历这些点,然后逐个与上一个点构建曲线,最后将构建好的曲线交给 Canvas 制作。

var lastPoint: Offset? = null
val path = Path()
var firstPoint = Offset(0F, 0F)
for (index in points.indices) {
    val x = index * xSpacing
    val y = height - height * ((points[index] - minPoint) / total)
    if (lastPoint != null) {
        buildCurveLine(path, lastPoint, Offset(x, y))
    }
    lastPoint = Offset(x, y)
    if (index == 0) {
        path.moveTo(x, y)
        firstPoint = Offset(x, y)
    }
}

buildCurveLine 办法便是用来构建两个点之间的曲线,这儿就依照咱们上面说的方法核算出贝塞尔曲线的两个控制点即可。

private fun buildCurveLine(path: Path, startPoint: Offset, endPoint: Offset) {
    val firstControlPoint = Offset(
        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
        y = startPoint.y,
    )
    val secondControlPoint = Offset(
        x = startPoint.x + (endPoint.x - startPoint.x) / 2F,
        y = endPoint.y,
    )
    path.cubicTo(
        x1 = firstControlPoint.x,
        y1 = firstControlPoint.y,
        x2 = secondControlPoint.x,
        y2 = secondControlPoint.y,
        x3 = endPoint.x,
        y3 = endPoint.y,
    )
}

因为依据款式的不同,咱们可能会需求制作面,也便是给曲线到坐标系最下方的部分上色,所以还需求供给一个函数用来闭合曲线。

fun closeWithBottomLine() {
    path.lineTo(width.toFloat(), height.toFloat())
    path.lineTo(0F, height.toFloat())
    path.lineTo(firstPoint.x, firstPoint.y)
}

到了这儿曲线差不多就构建完成了,然后依据款式制作出来。

when (style) {
    is BezierCurveStyle.Fill -> {
        closeWithBottomLine()
        drawPath(
            path = path,
            style = Fill,
            brush = style.brush,
        )
    }
    is BezierCurveStyle.CurveStroke -> {
        drawPath(
            path = path,
            brush = style.brush,
            style = style.stroke,
        )
    }
    is BezierCurveStyle.StrokeAndFill -> {
        drawPath(
            path = path,
            brush = style.strokeBrush,
            style = style.stroke,
        )
        closeWithBottomLine()
        drawPath(
            path = path,
            brush = style.fillBrush,
            style = Fill,
        )
    }
}

完整代码在这儿:gist.github.com/0xZhangKe/0…

Demo 在这儿:gist.github.com/0xZhangKe/f…