贝塞尔曲线入门

2017-12-01  本文已影响0人  黄烨1121

本章目录


贝塞尔曲线由法国工程师皮埃尔.贝塞尔所广泛发表,他运用贝济埃曲线来为汽车的主体进行设计,现在成为计算机图形学中相当重要的参数曲线。
贝塞尔曲线的原理就是利用多个点的位置来确定出一条曲线。这多个点就是起点,终点,控制点。控制点可以没有,也可以有多个。由这条曲线来确定View的移动轨迹,从而绘制出一些酷炫的动画。
贝塞尔曲线可以有很多阶,一般来说,我们用二阶和三阶比较多。相应的,Android官方也提供了1到3阶贝塞尔曲线的api。
四阶以上的比较少见,如果确实有需要的,可以去网上搜相应的算法。
首先,我们先从最简单的一阶贝塞尔曲线开始看

Part One:一阶贝塞尔曲线

一阶贝塞尔曲线也叫线性曲线,该函数中的t会经过由P0至P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线。

一阶贝塞尔曲线.gif
注:图片来源于维基百科
它的公式是
image.png

具体代码为

package com.terana.mycustomview.customview;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

public class HeartView extends View {
    private Paint linePaint;
    private Paint moveLinePaint;
    private Path linePath;
    private Path moveLinePath;
    private Point startPoint;
    private Point endPoint;

    public HeartView(Context context) {
        this(context, null);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initVariables();
    }

    private void initVariables() {
        //静止曲线画笔
        linePaint = new Paint();
        linePaint.setAntiAlias(true);//抗锯齿
        linePaint.setDither(true);//防抖动
        linePaint.setStyle(Paint.Style.STROKE);//绘制样式
        linePaint.setStrokeWidth(5);//线条粗细
        linePaint.setColor(Color.GRAY);//灰色线条

        // 运动曲线画笔
        moveLinePaint = new Paint();
        moveLinePaint.setAntiAlias(true);//抗锯齿
        moveLinePaint.setDither(true);//防抖动
        moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
        moveLinePaint.setStrokeWidth(5);//线条粗细
        moveLinePaint.setColor(Color.RED);//红色线条

        //静止线条路径
        linePath = new Path();

        //运动线条路径
        moveLinePath = new Path();

        //起始点坐标
        startPoint = new Point();
        startPoint.set(100, 100);

        //终止点坐标
        endPoint = new Point();
        endPoint.set(400, 400);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        linePath.moveTo(startPoint.x, startPoint.y);
        linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
        canvas.drawPath(linePath, linePaint);
        drawMovePath(canvas);
    }

    private float moveX = 100f, moveY = 100f;
    private long delay = 20;
    private long period = 1000;

    private void drawMovePath(Canvas canvas) {
        moveLinePath.moveTo(startPoint.x, startPoint.y);
        moveLinePath.lineTo(moveX, moveY);
        canvas.drawPath(moveLinePath, moveLinePaint);
        getHandler().postDelayed(runnable, delay);
        if (i == (period / delay + 1)){
            getHandler().removeCallbacks(runnable);
        }
    }

    private int i = 0;
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            moveX = calculateDimension(true, i);
            moveY = calculateDimension(false, i);
            invalidate();
            i++;
        }
    };

    private float calculateDimension(boolean flag, int index) {
        float result;
        float t = delay * index * 1.0f / period;
        if (flag) {
            result = (1 - t) * startPoint.x + t * endPoint.x;
        } else {
            result = (1 - t) * startPoint.y + t * endPoint.y;
        }
        return result;
    }
}

Part Two:二阶贝塞尔曲线

二阶贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:


image.png
二阶贝萨尔曲线.gif

自定义View布局部分

package com.terana.mycustomview.customview;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HeartView extends View {
    private Paint linePaint;
    private Paint moveLinePaint;
    private Path linePath;
    private Path moveLinePath;
    private Point startPoint;
    private Point endPoint;
    private Point controlPointOne;

    public HeartView(Context context) {
        this(context, null);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initVariables();
    }

    private void initVariables() {
        //静止曲线画笔
        linePaint = new Paint();
        linePaint.setAntiAlias(true);//抗锯齿
        linePaint.setDither(true);//防抖动
        linePaint.setStyle(Paint.Style.STROKE);//绘制样式
        linePaint.setStrokeWidth(5);//线条粗细
        linePaint.setColor(Color.GRAY);//灰色线条

        // 运动曲线画笔
        moveLinePaint = new Paint();
        moveLinePaint.setAntiAlias(true);//抗锯齿
        moveLinePaint.setDither(true);//防抖动
        moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
        moveLinePaint.setStrokeWidth(5);//线条粗细
        moveLinePaint.setColor(Color.RED);//红色线条

        //静止线条路径
        linePath = new Path();

        //运动线条路径
        moveLinePath = new Path();

        //起始点坐标
        startPoint = new Point();
        startPoint.set(100, 100);

        //终止点坐标
        endPoint = new Point();
        endPoint.set(400, 400);

        //控制点1坐标
        controlPointOne = new Point();
        controlPointOne.set(600, 100);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        linePath.moveTo(startPoint.x, startPoint.y);
        //linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
        linePath.quadTo(controlPointOne.x, controlPointOne.y, endPoint.x, endPoint.y);//二阶贝塞尔曲线
        canvas.drawPath(linePath, linePaint);
        drawMovePath(canvas);
    }

    private List<Map<String, Float>> pointList = new ArrayList<>();
    private static final String XPOSITION = "xPosition";
    private static final String YPOSITION = "yPosition";

    private void drawMovePath(final Canvas canvas) {
        moveLinePath.moveTo(startPoint.x, startPoint.y);
        for (int index = 0; index < pointList.size(); index++){
            if (index > 0){
                canvas.drawLine(pointList.get(index - 1).get(XPOSITION), pointList.get(index - 1).get(YPOSITION),
                        pointList.get(index).get(XPOSITION), pointList.get(index).get(YPOSITION), moveLinePaint);
            }
        }
    }

    public Point getStartPoint() {
        return startPoint;
    }

    public Point getEndPoint() {
        return endPoint;
    }

    public Point getControlPointOne() {
        return controlPointOne;
    }

    public void setPointList(float currentX, float currentY){
        Map<String, Float> temp = new HashMap<>();
        temp.put(XPOSITION, currentX);
        temp.put(YPOSITION, currentY);
        pointList.add(temp);
        invalidate();
    }
}

Activity部分:

package com.terana.mycustomview.activities;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import com.terana.mycustomview.R;
import com.terana.mycustomview.customview.HeartView;

public class HeartActivity extends BaseActivity {
    private static Handler handler;
    private HeartView heartView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        removeStatusBar();
        setContentView(R.layout.activity_heart);
        initViews();
        updatePosition();
    }

    float xPosition = 100f;
    float yPosition = 100f;

    private void updatePosition() {

        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    float t = i * 1.0f / 1000;
                    Message message = new Message();
                    message.what = 100;
                    Bundle bundle = new Bundle();
                    bundle.putFloat("x", xPosition);
                    bundle.putFloat("y", yPosition);
                    message.setData(bundle);
                    handler.sendMessage(message);
                    try {
                        sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    xPosition = (1 - t) * (1 - t) * heartView.getStartPoint().x + 2 * t * (1 - t) * heartView.getControlPointOne().x
                    + t * t * heartView.getEndPoint().x;
                    yPosition = (1 - t) * (1 - t) * heartView.getStartPoint().y + 2 * t * (1 - t) * heartView.getControlPointOne().y
                            + t * t * heartView.getEndPoint().y;
                }
            }
        }.start();
    }

    private void initViews() {
        heartView = findViewById(R.id.heartView_heart);
        handler = new MyHandler();
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 100:
                    Bundle bundle = msg.getData();
                    heartView.setPointList(bundle.getFloat("x"), bundle.getFloat("y"));
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    }
}

Part Three:三阶贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P2之前,走向P1方向的“长度有多长”。
曲线的参数形式为:


image.png
三阶贝萨尔曲线.gif

自定义View部分:

package com.terana.mycustomview.customview;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HeartView extends View {
    private Paint linePaint;
    private Paint moveLinePaint;
    private Path linePath;
    private Path moveLinePath;
    private Point startPoint;
    private Point endPoint;
    private Point controlPointOne;
    private Point controlPointTwo;

    public HeartView(Context context) {
        this(context, null);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initVariables();
    }

    private void initVariables() {
        //静止曲线画笔
        linePaint = new Paint();
        linePaint.setAntiAlias(true);//抗锯齿
        linePaint.setDither(true);//防抖动
        linePaint.setStyle(Paint.Style.STROKE);//绘制样式
        linePaint.setStrokeWidth(5);//线条粗细
        linePaint.setColor(Color.GRAY);//灰色线条

        // 运动曲线画笔
        moveLinePaint = new Paint();
        moveLinePaint.setAntiAlias(true);//抗锯齿
        moveLinePaint.setDither(true);//防抖动
        moveLinePaint.setStyle(Paint.Style.STROKE);//绘制样式
        moveLinePaint.setStrokeWidth(5);//线条粗细
        moveLinePaint.setColor(Color.RED);//红色线条

        //静止线条路径
        linePath = new Path();

        //运动线条路径
        moveLinePath = new Path();

        //起始点坐标
        startPoint = new Point();
        startPoint.set(100, 100);

        //终止点坐标
        endPoint = new Point();
        endPoint.set(400, 400);

        //控制点1坐标
        controlPointOne = new Point();
        controlPointOne.set(600, 100);

        //控制点2坐标;
        controlPointTwo = new Point();
        controlPointTwo.set(100, 300);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        linePath.moveTo(startPoint.x, startPoint.y);
        //linePath.lineTo(endPoint.x, endPoint.y);//一阶贝塞尔曲线线条
        linePath.cubicTo(controlPointOne.x, controlPointOne.y, controlPointTwo.x, controlPointTwo.y, endPoint.x, endPoint.y);//二阶贝塞尔曲线
        canvas.drawPath(linePath, linePaint);
        drawMovePath(canvas);
    }

    private List<Map<String, Float>> pointList = new ArrayList<>();
    private static final String XPOSITION = "xPosition";
    private static final String YPOSITION = "yPosition";

    private void drawMovePath(final Canvas canvas) {
        moveLinePath.moveTo(startPoint.x, startPoint.y);
        for (int index = 0; index < pointList.size(); index++) {
            if (index > 0) {
                canvas.drawLine(pointList.get(index - 1).get(XPOSITION), pointList.get(index - 1).get(YPOSITION),
                        pointList.get(index).get(XPOSITION), pointList.get(index).get(YPOSITION), moveLinePaint);
            }
        }
    }

    public Point getStartPoint() {
        return startPoint;
    }

    public Point getEndPoint() {
        return endPoint;
    }

    public Point getControlPointOne() {
        return controlPointOne;
    }

    public Point getControlPointTwo() {
        return controlPointTwo;
    }

    public void setPointList(float currentX, float currentY) {
        Map<String, Float> temp = new HashMap<>();
        temp.put(XPOSITION, currentX);
        temp.put(YPOSITION, currentY);
        pointList.add(temp);
        invalidate();
    }
}

Avtivity部分:

package com.terana.mycustomview.activities;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

import com.terana.mycustomview.R;
import com.terana.mycustomview.customview.HeartView;

public class HeartActivity extends BaseActivity {
    private static Handler handler;
    private HeartView heartView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        removeStatusBar();
        setContentView(R.layout.activity_heart);
        initViews();
        updatePosition();
    }

    float xPosition = 100f;
    float yPosition = 100f;

    private void updatePosition() {

        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    float t = i * 1.0f / 1000;
                    Message message = new Message();
                    message.what = 100;
                    Bundle bundle = new Bundle();
                    bundle.putFloat("x", xPosition);
                    bundle.putFloat("y", yPosition);
                    message.setData(bundle);
                    handler.sendMessage(message);
                    try {
                        sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    xPosition = (float) (Math.pow(1 - t, 3) * heartView.getStartPoint().x
                            + 3 * heartView.getControlPointOne().x * t * Math.pow(1 - t, 2)
                            + 3 * heartView.getControlPointTwo().x * Math.pow(t, 2) * (1 - t)
                            + Math.pow(t, 3) * heartView.getEndPoint().x);
                    yPosition = (float) (Math.pow(1 - t, 3) * heartView.getStartPoint().y
                            + 3 * heartView.getControlPointOne().y * t * Math.pow(1 - t, 2)
                            + 3 * heartView.getControlPointTwo().y * Math.pow(t, 2) * (1 - t)
                            + Math.pow(t, 3) * heartView.getEndPoint().y);
                }
            }
        }.start();
    }

    private void initViews() {
        heartView = findViewById(R.id.heartView_heart);
        handler = new MyHandler();
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 100:
                    Bundle bundle = msg.getData();
                    heartView.setPointList(bundle.getFloat("x"), bundle.getFloat("y"));
                default:
                    break;
            }
            super.handleMessage(msg);
        }
    }
}

最终效果为:


三阶贝塞尔曲线效果图.gif

其实三阶的会了,高阶的也差不多会了,就是控制点多了点,数学算法更复杂了一点,网上有模型,把公式复制过来即可,就不再多说了。

上一篇下一篇

猜你喜欢

热点阅读