在绝大多数的金融、健康或数据统计类 App 中,折线图(Line Chart)都是必不可少的组件。面对这种需求,很多开发者会选择引入庞大且沉重的第三方图表库(如 MPAndroidChart)。但在 Jetpack Compose 时代,得益于强大的CanvasAPI,手写一个满足绝大多数定制需求的高颜值、带丝滑展开动画的图表,其实只需要不到两百行代码!
本文将通过 Compose 的CanvasAPI 实现AnimatedLineChart组件,揭开“平滑曲线”、“底部渐变投影”以及“动态展开动画”背后的数学和绘制魔法。
1. 如何把数据画到屏幕上?
假设我们有一组数据[120f, 340f, 210f, 560f],我们的第一个挑战是:如何将这些抽象的数字,映射为屏幕上的像素坐标 (X, Y)?
- X 轴映射:非常简单,我们只需要计算出两个点之间的间距 (
spacingX = width / (data.size - 1)),然后通过索引乘以间距即可。 - Y 轴映射:需要注意两点:
- Android 屏幕坐标系的原点 (0,0) 在左上角,Y 轴向下是正方向。而折线图中,数值越大,点越应该靠上(Y 坐标越小)。
- 我们不能把图表画得太满,要留出上下内边距 (
paddingTop,paddingBottom) 供坐标文字使用。
坐标转换的算法如下:
// 找到数据的极值,并稍微拉伸一下,让最高点不要顶破天花板 val maxValue = data.maxOrNull() ?: 0f val minValue = data.minOrNull() ?: 0f val range = maxValue - minValue data.forEachIndexed { index, value -> // 计算 X 坐标 val x = paddingLeft + index * spacingX // 计算归一化比例 (0.0 ~ 1.0) val normalizedY = (value - minValue) / range // 反转 Y 坐标 (1f - normalizedY),映射到实际像素高度 val y = paddingTop + (1f - normalizedY) * chartHeight points.add(Offset(x, y)) }2. 曲线平滑魔法:告别僵硬的折角
最基础的折线图是用直线连接的:path.lineTo(x, y)。但如果你想要那种优雅的波浪形平滑曲线,就需要用到三次贝塞尔曲线 (cubicTo)。
贝塞尔曲线除了起点和终点,还需要两个控制点 (Control Points)来决定曲线的弯曲方向。最简单的平滑算法是:
- 控制点 1的 X 坐标取前一个点和当前点的中点,Y 坐标和前一个点一样。
- 控制点 2的 X 坐标也取中点,Y 坐标和当前点一样。
val previousPoint = points[index - 1] // X 轴中点 val midX = previousPoint.x + (x - previousPoint.x) / 2f // 绘制平滑曲线 linePath.cubicTo( x1 = midX, y1 = previousPoint.y, // 控制点 1 x2 = midX, y2 = y, // 控制点 2 x3 = x, y3 = y // 目标终点 )仅仅把lineTo换成这几行代码,你的图表瞬间就拥有了德芙般的丝滑感!
3. 视觉进阶:底部渐变色投影
纯粹的一根线显得单薄,我们通常会在折线下方填充半透明的渐变色。
做法是同时维护两条 Path: