贝塞尔曲线

  1. 一、简介
  2. 二、原理
  • 一阶贝塞尔曲线
  • 二阶贝塞尔曲线
  • 三阶贝塞尔曲线
    1. 三、实例
  • QQ红点
  • 画圆
  • 一、简介

    历史就不多讲了,因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域迅速得到了广泛的应用。也就是使用Ps中钢笔的工具。
    这个网站可以玩一玩,对前期的了解很好。
    https://cubic-bezier.com/#.17,.67,.83,.67

    二、原理

    类型 作用
    数据点 确定曲线的起始和结束位置
    控制点 确定曲线的弯曲程度

    一阶贝塞尔曲线

    一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段。直接是从P0到P1。

    github
    对应函数lineTo

    二阶贝塞尔曲线

    二阶则有了控制点,如下图:

    • P0、P2分别连接控制点P1,生成两条线
    • 在P0-P1和P1-P2分别取两点Q0和Q1,使得P0-Q0:P0-P1 = P1-Q1:P1-P2
    • 又连接Q0和Q1,再在上面取点B,使得Q0-B:Q0-Q1 = P0-Q0:P0-P1
      github

    这样就完成了所有点、线的位置,最后我们在时间内做运动,就可以画出贝塞尔曲线了。
    github

    public class BezierView extends View {
    private Paint mPaint, mPointPaint, mLinePaint;
    // 定点
    private PointF one, two, three;
    public BezierView(Context context) {
    super(context);
    init();
    }

    void init() {
    mPaint = new Paint();
    mPaint.setColor(getResources().getColor(R.color.colorAccent));
    mPaint.setStrokeWidth(8);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setTextSize(60);

    mPointPaint = new Paint();
    mPointPaint.setColor(getResources().getColor(R.color.colorPrimaryDark));
    mPointPaint.setStrokeWidth(20);
    mPointPaint.setTextSize(60);

    mLinePaint = new Paint();
    mLinePaint.setColor(getResources().getColor(R.color.colorPrimary));
    mLinePaint.setStrokeWidth(3);
    mLinePaint.setTextSize(60);

    one = new PointF(100, 100);
    two = new PointF(500, 100);
    three = new PointF(300, 300);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // 画点
    canvas.drawPoint(one.x, one.y, mPointPaint);
    canvas.drawPoint(two.x, two.y, mPointPaint);
    canvas.drawPoint(three.x, three.y, mPointPaint);

    // 连接辅助线
    canvas.drawLine(one.x, one.y, three.x, three.y, mLinePaint);
    canvas.drawLine(three.x, three.y, two.x, two.y, mLinePaint);

    // 贝塞尔
    Path path = new Path();
    path.moveTo(one.x, one.y);
    path.quadTo(three.x, three.y, two.x, two.y);

    canvas.drawPath(path, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    three.x = event.getX();
    three.y = event.getY();
    invalidate();
    return true;
    }
    }

    对应函数quadTo

    三阶贝塞尔曲线

    与二阶曲线同理方法,建立三阶曲线如下图。
    github

    github

    public class BezierView2 extends View {
    private Paint mPaint, mPointPaint, mLinePaint;
    // 定点
    private PointF one, two, three, four;

    public BezierView2(Context context) {
    super(context);
    init();
    }

    void init() {
    mPaint = new Paint();
    mPaint.setColor(getResources().getColor(R.color.colorAccent));
    mPaint.setStrokeWidth(8);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setTextSize(60);

    mPointPaint = new Paint();
    mPointPaint.setColor(getResources().getColor(R.color.colorPrimaryDark));
    mPointPaint.setStrokeWidth(20);
    mPointPaint.setTextSize(60);

    mLinePaint = new Paint();
    mLinePaint.setColor(getResources().getColor(R.color.colorPrimary));
    mLinePaint.setStrokeWidth(3);
    mLinePaint.setTextSize(60);

    one = new PointF(100, 100);
    two = new PointF(500, 100);
    three = new PointF(500, 500);
    four = new PointF(100, 500);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 画点
    canvas.drawPoint(one.x, one.y, mPointPaint);
    canvas.drawPoint(two.x, two.y, mPointPaint);
    canvas.drawPoint(three.x, three.y, mPointPaint);
    canvas.drawPoint(four.x, four.y, mPointPaint);

    // 画线
    canvas.drawLine(one.x, one.y, four.x, four.y, mLinePaint);
    canvas.drawLine(four.x, four.y, three.x, three.y, mLinePaint);
    canvas.drawLine(three.x, three.y, two.x, two.y, mLinePaint);

    // 贝塞尔
    Path path = new Path();
    path.moveTo(one.x, one.y);
    path.cubicTo(four.x, four.y, three.x, three.y, two.x, two.y);
    canvas.drawPath(path, mPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    four.x = event.getX();
    four.y = event.getY();
    invalidate();
    return true;
    }
    }

    对应函数cubicTo

    三、实例

    QQ红点

    原理如图:
    github

    • 图中可以看到两条贝塞尔曲线就是用两个数据点(红色)和一个控制点(蓝色)画出来的。
    • 可以看到角度x都是相等的(可以自己推导)。
    • 最后只要覆盖颜色,就可以了。

    为了获取4个红色的数据点,我们需要做Sin、Cos的三角函数处理:

    // 获取两个圆的4点坐标
    void getFourPoint() {
    // 角度的sin和cos
    float sin = Math.abs(mMainCircle.y - mClientCircle.y) / getClicleSpace();
    float cos = Math.abs(mMainCircle.x - mClientCircle.x) / getClicleSpace();

    // 获取静态圆水平和垂直长度
    float x1 = sin * mMainRadius;
    float y1 = cos * mMainRadius;
    // 获取动态圆水平和垂直长度
    float x2 = sin * mClientRadius;
    float y2 = cos * mClientRadius;

    // 判断斜度
    if ((mMainCircle.x - mClientCircle.x)/(mMainCircle.y - mClientCircle.y) > 0) {
    // 同号
    // 获取静态圆两点
    mMainLeftPoint.x = mMainCircle.x - x1;
    mMainLeftPoint.y = mMainCircle.y + y1;
    mMainRightPoint.x = mMainCircle.x + x1;
    mMainRightPoint.y = mMainCircle.y - y1;

    // 获取动态圆两点
    mClientLeftPoint.x = mClientCircle.x - x2;
    mClientLeftPoint.y = mClientCircle.y + y2;
    mClientRightPoint.x = mClientCircle.x + x2;
    mClientRightPoint.y = mClientCircle.y - y2;
    } else {
    //异号
    // 获取静态圆两点
    mMainLeftPoint.x = mMainCircle.x + x1;
    mMainLeftPoint.y = mMainCircle.y + y1;
    mMainRightPoint.x = mMainCircle.x - x1;
    mMainRightPoint.y = mMainCircle.y - y1;

    // 获取动态圆两点
    mClientLeftPoint.x = mClientCircle.x + x2;
    mClientLeftPoint.y = mClientCircle.y + y2;
    mClientRightPoint.x = mClientCircle.x - x2;
    mClientRightPoint.y = mClientCircle.y - y2;
    }
    }

    // 获取两圆心距离
    float getClicleSpace() {
    mCircleSpace = (float)Math.hypot((double) Math.abs(mMainCircle.x - mClientCircle.x), (double)Math.abs(mMainCircle.y - mClientCircle.y));
    return mCircleSpace;
    }

    • 首先我们需要知道X角度的Sin、Cos。
    • 然后再分别获取4个位置点的偏移量
    • 最后根据斜率来计算对应的4点坐标

    然后在onDraw中绘制图片

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // 绘制固定圆
    canvas.drawCircle(mMainCircle.x, mMainCircle.y, mMainRadius, mPaint);
    // 绘制动态圆
    canvas.drawCircle(mClientCircle.x, mClientCircle.y, mClientRadius, mPaint);

    getPointF();
    getFourPoint();

    // 绘制贝塞尔
    Path path = new Path();
    path.moveTo(mMainLeftPoint.x, mMainLeftPoint.y);
    path.quadTo(pointF.x, pointF.y, mClientLeftPoint.x, mClientLeftPoint.y);


    path.lineTo(mClientRightPoint.x, mClientRightPoint.y);
    path.quadTo(pointF.x, pointF.y, mMainRightPoint.x, mMainRightPoint.y);

    path.close();
    canvas.drawPath(path, mPaint);
    }

    根据代码可以知道,绘制固定的两个圆,和两条贝塞尔曲线即可,最后调用close来将曲线闭合。

    最后在滑动使只需要更新点位即可:

    @Override
    public boolean onTouchEvent(MotionEvent event) {

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:

    mClientCircle.x = event.getX();
    mClientCircle.y = event.getY();
    invalidate();

    break;
    case MotionEvent.ACTION_UP:
    if (getClicleSpace() > MAX_SPACE) {
    // 断开并重新绘制心位置
    mClientCircle.x = event.getX();
    mClientCircle.y = event.getY();
    mMainCircle.x = event.getX();
    mMainCircle.y = event.getY();
    invalidate();
    } else if (getClicleSpace() <= MAX_SPACE) {
    // 返回原处
    mClientCircle.x = mMainCircle.x;
    mClientCircle.y = mMainCircle.y;
    invalidate();
    } else {
    // 继续显示
    mClientCircle.x = event.getX();
    mClientCircle.y = event.getY();
    invalidate();
    }
    break;
    case MotionEvent.ACTION_MOVE:
    if (getClicleSpace() > MAX_SPACE) {
    // 断开并重新绘制心位置
    mClientCircle.x = event.getX();
    mClientCircle.y = event.getY();
    mMainCircle.x = event.getX();
    mMainCircle.y = event.getY();
    invalidate();
    } else {
    mClientCircle.x = event.getX();
    mClientCircle.y = event.getY();
    invalidate();
    }
    break;
    }
    return true;
    }

    画圆

    可以看到这里使用的是三阶贝塞尔曲线。蓝点为数据点,红点为控制点。每个圆/4都需要两个控制点来生成,而这里的两个固定数值R和m都是经过推导出来的,我们只需要引用就好。

    private final float DATA = 55.228f;
    private final float S_DATA = 100 - DATA;

    private Paint mPaint;

    private PointF mTopLPoint, mTopRPoint, mLeftTPoint, mLeftBPoint, mRightTPoint, mRightBPoint, mBottomLPoint, mBottomRPoint;
    private PointF mTopPoint, mLeftPoint, mRightPoint, mBottomPoint;

    public CircleBezierView(Context context) {
    super(context);
    init();
    }

    void init() {

    // 笔
    mPaint = new Paint();
    mPaint.setColor(Color.RED);
    mPaint.setStyle(Paint.Style.STROKE);

    // 控制点
    mTopLPoint = new PointF(S_DATA, 0);
    mTopRPoint = new PointF(100+DATA, 0);
    mLeftTPoint = new PointF(0, S_DATA);
    mLeftBPoint = new PointF(0, 100+DATA);
    mRightTPoint = new PointF(200, S_DATA);
    mRightBPoint = new PointF(200, 100+DATA);
    mBottomLPoint = new PointF(S_DATA, 200);
    mBottomRPoint = new PointF(100+DATA, 200);

    // 控制点
    mTopPoint = new PointF(100f, 0);
    mLeftPoint = new PointF(0, 100f);
    mRightPoint = new PointF(200, 100f);
    mBottomPoint = new PointF(100f, 200);
    }

    @Override
    protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.drawPoint(mTopPoint.x, mTopPoint.y, mPaint);
    canvas.drawPoint(mLeftPoint.x, mLeftPoint.y, mPaint);
    canvas.drawPoint(mRightPoint.x, mRightPoint.y, mPaint);
    canvas.drawPoint(mBottomPoint.x, mBottomPoint.y, mPaint);

    Path path = new Path();
    path.moveTo(mTopPoint.x, mTopPoint.y);
    // top -> right
    path.cubicTo(mTopRPoint.x, mTopRPoint.y, mRightTPoint.x, mRightTPoint.y, mRightPoint.x, mRightPoint.y);
    // right -> bottom
    path.cubicTo(mRightBPoint.x, mRightBPoint.y, mBottomRPoint.x, mBottomRPoint.y, mBottomPoint.x, mBottomPoint.y);
    // bottom -> left
    path.cubicTo(mBottomLPoint.x, mBottomLPoint.y, mLeftBPoint.x, mLeftBPoint.y, mLeftPoint.x, mLeftPoint.y);
    // left -> top
    path.cubicTo(mLeftTPoint.x, mLeftTPoint.y, mTopLPoint.x, mTopLPoint.y, mTopPoint.x, mTopPoint.y);

    canvas.drawPath(path, mPaint);
    }