使用JetPack Compose绘制曲线图

921 阅读3分钟

本文目的是拆解Google Android官方Compose项目JetLagged中曲线图的实现过程,JetLagged项目地址。文章中的代码是我自己实现过程总结,和官方的案例代码会有一些出入,请注意。

最终效果

3.png

知识点

  1. 如何使用Brush 绘制渐变色
  2. 如何使用Path绘制曲线

前期准备

准备需要的颜色

Color.kt文件中写入以下颜色:

val Pink = Color(0xFFEAA8A9)
val Purple = Color(0xFFD2B4D3)
val Green = Color(0xFFADD7B9)

创建一份虚拟数据

val fakeData = arrayOf(
    PointF(30f, 100f),
    PointF(50f, 30f),
    PointF(70f, 60f),
    PointF(90f, 100f),
    PointF(110f, 30f),
    PointF(130f, 60f),
    PointF(150f, 10f),
    PointF(170f, 20f),
    PointF(180f, 50f),
    PointF(190f, 80f),
    PointF(200f, 60f),
    PointF(210f, 90f),
    PointF(230f, 100f),
    PointF(250f, 30f),
    PointF(260f, 60f),
    PointF(280f, 10f),
    PointF(300f, 90f),
    PointF(320f, 75f),
    PointF(340f, 30f),
    PointF(360f, 10f),
)

步骤一:简单地绘制并连接每个点

我们先简单地使用Path将每个点用直线连接起来,并且设置一个固定的颜色和10px的线条宽度:

@Composable
fun WaveGraph() {
    Box(
        Modifier
            .fillMaxWidth()
            .height(100.dp)
            .drawWithCache {
                val path = Path()
                fakeData.forEach { point ->
                    val x = point.x.dp.toPx()
                    val y = point.y.dp.toPx()
                    path.lineTo(x, y)
                }
                onDrawBehind {
                    drawPath(path, SolidColor(Pink), style = Stroke(10f))
                }
            }
    )
}

效果图如下:

1.png

步骤二:绘制曲线

接下来我们将lineTo()替换掉,使用PathcubicTo()将直线修改成曲线:

@Composable
fun WaveGraph() {
    Box(
        Modifier
            .fillMaxWidth()
            .height(100.dp)
            .drawWithCache {
                val path = Path()
                var previousX = 0f // ADD
                var previousY = 0f // ADD
                fakeData.forEach { point ->
                    val x = point.x.dp.toPx()
                    val y = point.y.dp.toPx()

                    // ADD
                    val controlPoint1 = PointF((x + previousX) / 2f, previousY)
                    val controlPoint2 = PointF((x + previousX) / 2f, y)
                    path.cubicTo(
                        controlPoint1.x,
                        controlPoint1.y,
                        controlPoint2.x,
                        controlPoint2.y,
                        x,
                        y
                    )
                    previousX = x
                    previousY = y
                }
                onDrawBehind {
                    drawPath(path, SolidColor(Pink), style = Stroke(10f))
                }
            }
    )
}

💡cubicTo()是根据贝赛尔曲线原理绘制曲线的,关于贝赛尔曲线的知识请自行学习

controlPoint1controlPoint2是绘制贝赛尔曲线时的两个用于辅助的点,并不会绘制在屏幕上。

效果图如下:

2.png

步骤三:增加渐变色

现在离最终效果已经非常接近了,让我们再修改一下颜色即可。先定义一个array,确定不同颜色在图形上的分布,这里的颜色在一开始为Pink,到图形中间时会过渡到Purple,到图形末尾过渡到Green

@Composable
fun WaveGraph() {
    // ADD
    val waveColorStops = remember {
        arrayOf(
            0f to Pink,
            0.5f to Purple,
            1f to Green
        )
    }
    Box(
        Modifier
            .fillMaxWidth()
            .height(100.dp)
            .drawWithCache {
            
            }
               
    )
}

然后我们自定义一个渐变Brush,我们的渐变色是在垂直方向上过渡的,也支持定义其它方向的过渡色:

@Composable
fun WaveGraph() {
    val waveColorStops = remember {
        arrayOf(
            0f to Pink,
            0.5f to Purple,
            1f to Green
        )
    }
    Box(
        Modifier
            .fillMaxWidth()
            .height(100.dp)
            .drawWithCache {
                // ADD
                val gradientBrush = Brush.verticalGradient(
                    colorStops = waveColorStops,
                    startY = 0f,
                    endY = size.height
                )
            }
    )
}

最后在drawPath()时将渐变gradientBrush作为参数传入即可:

onDrawBehind {
    drawPath(path, gradientBrush, style = Stroke(10f)) // ADD
}

来看一下最终效果:

3.png

完整代码如下:

@Composable
fun WaveGraph() {
    val waveColorStops = remember {
        arrayOf(
            0f to Pink,
            0.5f to Purple,
            1f to Green
        )
    }
    Box(
        Modifier
            .fillMaxWidth()
            .height(100.dp)
            .drawWithCache {
                val gradientBrush = Brush.verticalGradient(
                    colorStops = waveColorStops,
                    startY = 0f,
                    endY = size.height
                )

                val path = Path()
                var previousX = 0f
                var previousY = 0f
                fakeData.forEach { point ->
                    val x = point.x.dp.toPx()
                    val y = point.y.dp.toPx()

                    val controlPoint1 = PointF((x + previousX) / 2f, previousY)
                    val controlPoint2 = PointF((x + previousX) / 2f, y)
                    path.cubicTo(
                        controlPoint1.x,
                        controlPoint1.y,
                        controlPoint2.x,
                        controlPoint2.y,
                        x,
                        y
                    )
                    previousX = x
                    previousY = y
                }
                onDrawBehind {
                    drawPath(path, gradientBrush, style = Stroke(10f))
                }
            }
    )
}