Android 笔记之贝塞尔曲线的应用
贝塞尔曲线是用节点和控制点绘制的高精度曲线,Android 中常用的有二阶、三阶贝塞尔曲线。本文介绍使用贝塞尔曲线绘制折线图,并实现动画效果。
贝塞尔曲线介绍
下图是二阶贝塞尔曲线绘制方法介绍,只要各个点满足条件:AD/AB = BE/BC = DF/DE,那么当沿着当前线段移动 D、E 点时,F 点的运动轨迹就是一个贝塞尔曲线:
动图示意如下:
可以在下面的两个网站在线体验贝塞尔曲线:
https://aaaaaaaty.github.io/bezierMaker.js/playground/playground.html
计算控制点坐标
在绘制折线图时,我们获取的数据可以当做贝塞尔曲线的端点,Android 为我们提供了绘制二阶和三阶贝塞尔曲线的方法:
Path.quadTo(float x1, float y1, float x2, float y2)//二阶贝塞尔曲线:分别是控制点的 x、y 坐标和结束的的 x、y 坐标
Path.cubicTo(float x1, float y1, float x2, float y2, float x3, float y3)//三阶贝塞尔曲线:分别是控制点 1、2 的 x、y 坐标和结束的的 x、y 坐标
以Path.cubicTo()
方法为例,在绘制三阶贝塞尔曲线时,起点和终点已知,剩下工作就是计算两个控制点的坐标。
方法 1
按照贝塞尔曲线的定义,计算各个点对应控制点的坐标,具体的计算原理我们可以参考这篇文章
假设起点、终点分别为startPoint
,endPoint
,起点前一个点为beforePointF
,终点后一个点为afterPoint
,那么终止点 1、2(controlPoint1
、controlPoint2
)的坐标满足(其中 a,b 为任意正数,比如 1/6):
val controlPoint1X = startPoint.x + (endPoint.x - beforePointF.x) * a
val controlPoint1Y = startPoint.y + (endPoint.y - beforePointF.y) * a
val controlPoint2X = endPoint.x - (afterPoint.x - startPoint.x) * b
val controlPoint2Y = endPoint.y - (afterPoint.y - startPoint.y) * b
这里要处理特殊情况:第一个点 P0的前一个仍然为 P0,最后一个点 Pn的后一个点仍为 Pn
但这种情况绘制出来的贝塞尔曲线如下:
可以看到除了 P0和 Pn外,其他点的曲线坐标和对应的点坐标不一致。
方法 2
为了解决方法 1 存在的问题,我们人为的在两个点之间加入两个控制点,这样在startPoint
,endPoint
之间的贝塞尔曲线首尾点的坐标必定落在起点和终点上(思路来自这里)。
所以,两个控制点的坐标为:
val controlPoint1X = (startPoint.x + endPoint.x) / 2
val controlPoint1Y = startPoint.y
val controlPoint2X = (startPoint.x + endPoint.x) / 2
val controlPoint2Y = endPoint.y
这样绘制出来的曲线比较符合我们的要求。
所以,最终贝塞尔曲线 path 计算方法如下:
var bezierPath = Path()
bezierPath.moveTo(pointList.first().x, -pointList.first().y)
pointList.forEachIndexed { index, startPoint ->
when (index) {
pointList.lastIndex -> {
//在绘制 P(n-1) ~ P(n) 点的贝塞尔曲线时,已经绘制到了 P(n) 点,所以此处不用再绘制
}
else -> {
val endPoint = pointList[index + 1]
bezierPath.cubicTo(
(startPoint.x + endPoint.x) / 2,
-startPoint.y, //为了解决 view 坐标原点在左上角而做的特殊处理,下同
(startPoint.x + endPoint.x) / 2,
-endPoint.y,
endPoint.x,
-endPoint.y
)
}
}
}
给 Path 添加渐变背景
我们可以使用Paint.setShader(Shader shader)
方法,在绘制 Path 的时候绘制渐变背景。
渐变背景使用 Shader 实现。
为了确保绘制效果,我们需要在 Path 计算完成后,将其闭合,以确保绘制的背景在我们需要的范围内:
val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG)
shadowPaint.style = Paint.Style.FILL
val shader =
LinearGradient(0F, 0F, 0F, 500F, Color.GREEN, Color.TRANSPARENT, Shader.TileMode.CLAMP)
shadowPaint.shader = shader
val shadowPath = Path(path)
shadowPath.lineTo(endPoint.x, 800F)
shadowPath.lineTo(startPoint.x, 800F)
shadowPath.lineTo(startPoint.x, startPoint.y)
shadowPath.close()
canvas.drawPath(shadowPath, shadowPaint)
给 Path 添加动画
为了让 Path 看起来是从起点慢慢绘制到终点去的,我们可以先计算 path 的总长度,然后结合ValueAnimator
实时获得对应长度的 path 并绘制:
var mValueAnimator = ValueAnimator.ofFloat(0f, 1f)
mValueAnimator.duration = 10000
mValueAnimator.repeatCount = ValueAnimator.INFINITE
mValueAnimator.interpolator = AccelerateDecelerateInterpolator()
mValueAnimator.addUpdateListener { animation -> //获取从 0-1 的变化值
progress = animation.animatedValue as Float
//不断刷新绘图,实现路径绘制
invalidate()
}
mValueAnimator.start()
然后在onDraw()
方法中绘制对应的 path:
var mPathMeasure: PathMeasure = PathMeasure(bezierPath, false)
val totalPathLength = mPathMeasure.length //获取 path 总长度
// 按照进度绘制贝塞尔曲线
val stopD = progress * totalPathLength
mPathMeasure.getSegment(0F, stopD, dstPath, true) //按照长度比例截取对应的 path 并赋值给 dstPath
//bezier anim
canvas.drawPath(dstPath, bezierPaint) //绘制对应的 path
注意事项
- 使用 canvas 绘制坐标时,需要注意 android 的坐标原点位于屏幕左上角。所以在绘制曲线图时可以先将坐标原点向下平移一段距离,再绘制对应坐标(可以绘制实际的 y 坐标负值)
- 在拼接贝塞尔曲线的 path 时候注意,
path.moveTo()
方法会将 path 切断
参考资料
https://wenku.baidu.com/view/c790f8d46bec0975f565e211.html
https://blog.csdn.net/laizuling/article/details/51162011