前言
前篇我们已经见过贝塞尔曲线的功能,原理,公式,和推导过程
这节课我们来讲实际应用:如何通过贝塞尔曲线,绘制经过若干固定点的平滑曲线
首先,我们要明确需求,我们的目标是画一条平滑曲线,不是求一条贝塞尔曲线,经过所有点
可以是通过多条贝塞尔曲线来拼接成一条平滑曲线
然后,经过若干点的贝塞尔曲线,可能是有无数条的,并不是唯一解
所以我们的目标,是找一个简单的方案,能画出平滑曲线就行
并不是由曲线上的点,求一个万能的曲线公式,千万别弄错了
方案
- 每两个点之间画一条贝塞尔曲线,然后所有曲线连接成一条平滑曲线(之所以这么设计,是因为这个想法最简单)
- 多条曲线想要连成一条平滑曲线,那么在曲线交点,也就是顶点两侧,曲线的斜率或切线方向,一定得是一致的
- 我们就用x方向,作为所有顶点处,曲线切线的方向(之所以这么设计,是因为这样最简单)
- 第一个控制点,一定在起点切线上,最后一个控制点,一定在终点切线上(贝塞尔曲线特性决定的)
- 选用三阶贝塞尔曲线,在起点和终点切线上,随便选个点作为控制点(之所以这么设计,是因为这样最简单)
- 让曲线两端,控制点到顶点的距离相等(这样曲线就比较匀称,不会出现很突兀的情况)
- 画出所有顶点之间的贝塞尔曲线,得到最终的平滑曲线
流程示意图
确定切线和控制点
画三阶贝塞尔曲线
最终结果
方案说明
- 由于所有切线方向,都是沿x方向,看起来会像是沿x方向的波形图,所以实际应用中,最好是根据数据实际走向来确定切线方向
- 控制点离顶点越近,曲线越接趋于直线
- 控制点离顶点越远,曲线越接趋于波形
- 两端控制点离顶点距离,相差越小,曲线越对称(我们是完全相等,则完全对称)
Android平滑曲线绘制代码
package com.android.architecture;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import com.easing.commons.android.value.color.Colors;
import com.easing.commons.android.value.measure.FPos;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("all")
public class BezierCurve extends View {
final List<FPos> posList = new ArrayList();
final List<FPos> controlPosList = new ArrayList();
final Paint posPaint = new Paint();
final Paint linePaint = new Paint();
final Paint tangentPaint = new Paint();
final Paint bezierPaint = new Paint();
public BezierCurve(Context context) {
this(context, null);
}
public BezierCurve(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
init(context, attributeSet);
}
protected void init(Context context, AttributeSet attributeSet) {
//创建初始点
posList.add(new FPos(100, 100));
posList.add(new FPos(300, 200));
posList.add(new FPos(560, 300));
posList.add(new FPos(720, 350));
posList.add(new FPos(900, 600));
posList.add(new FPos(1200, 650));
posList.add(new FPos(1300, 700));
//初始化点画笔
posPaint.setColor(Colors.RED);
posPaint.setStrokeWidth(10);
posPaint.setStyle(Paint.Style.STROKE);
posPaint.setStrokeCap(Paint.Cap.ROUND);
//初始化折线画笔
linePaint.setColor(Colors.LIGHT_BLUE);
linePaint.setStrokeWidth(2);
linePaint.setStyle(Paint.Style.STROKE);
//初始化切线画笔
tangentPaint.setColor(Colors.LIGHT_GREY);
tangentPaint.setStrokeWidth(4);
tangentPaint.setStyle(Paint.Style.STROKE);
tangentPaint.setPathEffect(new DashPathEffect(new float[]{4, 4}, 0));
//初始化曲线画笔
bezierPaint.setColor(Colors.ORANGE);
bezierPaint.setStrokeWidth(2);
bezierPaint.setStyle(Paint.Style.STROKE);
//计算控制点
final float ratio = 0.3F;
for (int i = 0; i < posList.size() - 1; i++) {
FPos p1 = posList.get(i);
FPos p2 = posList.get(i + 1);
FPos cp1 = new FPos();
cp1.x = p1.x + ratio * (p2.x - p1.x);
cp1.y = p1.y;
FPos cp2 = new FPos();
cp2.x = p1.x + (1 - ratio) * (p2.x - p1.x);
cp2.y = p2.y;
controlPosList.add(cp1);
controlPosList.add(cp2);
}
}
@Override
protected void onDraw(Canvas canvas) {
//画点
for (int i = 0; i < posList.size(); i++) {
FPos p = posList.get(i);
canvas.drawPoint(p.x, p.y, posPaint);
}
//画折线
{
Path path = new Path();
FPos p0 = posList.get(0);
path.moveTo(p0.x, p0.y);
for (int i = 0; i < posList.size(); i++) {
FPos p = posList.get(i);
path.lineTo(p.x, p.y);
}
canvas.drawPath(path, linePaint);
}
//画切线
{
FPos p1 = posList.get(0);
FPos p2 = controlPosList.get(0);
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, tangentPaint);
}
for (int i = 1; i < controlPosList.size() - 1; i += 2) {
FPos p1 = controlPosList.get(i);
FPos p2 = controlPosList.get(i + 1);
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, tangentPaint);
}
{
FPos p1 = controlPosList.get(controlPosList.size() - 1);
FPos p2 = posList.get(posList.size() - 1);
canvas.drawLine(p1.x, p1.y, p2.x, p2.y, tangentPaint);
}
//画曲线
for (int i = 0; i < posList.size() - 1; i++) {
FPos p1 = posList.get(i);
FPos p2 = posList.get(i + 1);
FPos cp1 = controlPosList.get(i * 2);
FPos cp2 = controlPosList.get(i * 2 + 1);
Path path = new Path();
path.moveTo(p1.x, p1.y);
path.cubicTo(cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y);
canvas.drawPath(path, bezierPaint);
}
}
}
源码下载