Android---贝赛尔曲线基础详解

推荐贝塞尔曲线详解视频---贝塞尔曲线详解

一 。什么是贝塞尔曲线

贝赛尔曲线又称为贝兹曲线、贝济埃曲线,是应用于二维图像应用程序的数学曲线。

二 。贝塞尔曲线计算公式


三 。贝塞尔曲线图解

                   

一阶贝塞尔曲线是条直线,贝塞尔点从P0到P1。

二阶贝塞尔曲线(图中红色的线)是个曲线,绘制过程如下:P0与P1之间有个绿色的点(我们设为X1),P1与P2之间也有个绿色的点(我们设为X2),图中绿色的线(X1与X2的连线)有个黑色的点(我们设为X3),P0到X1的距离等于P1到X2的距离等于X1到X3的距离,也就是图中t时刻的值,曲线就是t时刻贝塞尔点从P0到P2的移动过程。

 

三阶贝赛尔曲线绘制过程如下:P0与P1之间有个绿色的点(我们设为X1),P1与P2之间也有个绿色的点(我们设为X2),P2到P3之间也有个绿色的点(我们设为X3),X1与X2的连线(绿色的线)中蓝色的点(我们设为X4),X2与X3的连线(绿色的线)中蓝色的点(我们设为X5),X4与X5的连线(蓝色的线)中黑色的点(我们设为X6),P0到X1的距离等于P1到X2的距离等于P2到X3的距离等于X1到X4的距离等于X2到X5的距离等于X4到X6的距离,也就是图中t的值,曲线就是贝塞尔点从P0到P3的移动过程。


四节、五阶曲线乃至以上绘制原理都是一样的,不断地从线中取出长度为t的点,点与点连线再取出长度为t的点,直至最后剩下一条线,而这条线是不断的移动的,曲线就是t时刻贝塞尔点在这条线中移动的轨迹。

下面是贝赛尔曲线详细效果图



四 。1~3阶贝赛尔曲线代码实现

① 一阶贝塞尔曲线

public class TextLine extends View {

    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private Path mPath = new Path();

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

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

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

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

        init();
    }

    private void init() {
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        mPaint = paint;

        //一阶贝赛尔曲线
        Path path = new Path();
        path.moveTo(100, 100);
        path.lineTo(400, 400);
        
        mPath = path;
    }

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

        canvas.drawPath(mPath, mPaint);
    }
}

一阶贝赛尔曲线是条直线,起始点P0(100,100),终点P1(400,400)。


② 二阶贝赛尔曲线

 //一阶贝赛尔曲线
        Path path = new Path();
        path.moveTo(100, 100);
        path.lineTo(400, 400);
        //二阶贝塞尔曲线
        path.quadTo(700, 100, 800, 400);

        mPath = path;
二阶贝赛尔曲线quadTo()方法中前两个参数(700,100),是曲线P1的坐标,后两个参数(800,400)是曲线P2的坐标,而P0的坐标为(400,400)。我们可以看下二阶曲线源码实现。

 /**
     * Add a quadratic bezier from the last point, approaching control point
     * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
     * this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the control point on a quadratic curve
     * @param y1 The y-coordinate of the control point on a quadratic curve
     * @param x2 The x-coordinate of the end point on a quadratic curve
     * @param y2 The y-coordinate of the end point on a quadratic curve
     */
    public void quadTo(float x1, float y1, float x2, float y2) {
        isSimplePath = false;
        nQuadTo(mNativePath, x1, y1, x2, y2);
    }
源码注释中可以看到,贝赛尔曲线是从最后一个点,经过控制点(X1,Y1),终点在(X2,Y2),如果没有进行moveTo()操作,将从(0,0)开始。而我们在一阶曲线进行了moveTo()操作,lineTo()操作已经把起始点(100,100)链接到(400,400),所以起始点P0变为(400,400),如果没有进行lineTo()操作,起始点就为(100,100)。


二阶贝赛尔曲线还有个实现方法

 //二阶贝塞尔曲线,和下面表达不同
//        path.quadTo(700, 100, 800, 400);
        //相对的实现,不用考虑运算
        path.rQuadTo(300, -300, 400, 0);

这两句代码实现效果是一样的,rQuadTo()方法中前两个参数(300,-300)是P1(700,100)与P0(400,400)的相差距离,后两个参数(400,0)是P2(800,400)与P0(400,400)的相差距离。我们可以看下源码

 /**
     * Same as quadTo, but the coordinates are considered relative to the last
     * point on this contour. If there is no previous point, then a moveTo(0,0)
     * is inserted automatically.
     *
     * @param dx1 The amount to add to the x-coordinate of the last point on
     *            this contour, for the control point of a quadratic curve
     * @param dy1 The amount to add to the y-coordinate of the last point on
     *            this contour, for the control point of a quadratic curve
     * @param dx2 The amount to add to the x-coordinate of the last point on
     *            this contour, for the end point of a quadratic curve
     * @param dy2 The amount to add to the y-coordinate of the last point on
     *            this contour, for the end point of a quadratic curve
     */
    public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
        isSimplePath = false;
        nRQuadTo(mNativePath, dx1, dy1, dx2, dy2);
    }

源码注释中可以看到,这个方法是类似于quadTo()方法的,只不过它的参数是P1与P2相对于最后一个点P0(400,400)的差值,如果没有最后一个点,就设置为(0,0)。


我们可以看到图中二阶贝塞尔曲线向上弯曲的最高的的Y轴坐标是在控制点P1(700,100)的Y轴坐标的下面,我们可以通过第三部分的二阶曲线图了解其实现的原因。

③ 三阶贝赛尔曲线

   //一阶贝赛尔曲线
        Path path = new Path();
        path.moveTo(100, 100);
        path.lineTo(400, 400);

        //二阶贝塞尔曲线,和下面表达不同
        path.quadTo(700, 100, 800, 400);
        //相对的实现,不用考虑运算
//        path.rQuadTo(300, -300, 400, 0);
        path.moveTo(400, 800);
        //三阶贝赛尔曲线
        path.cubicTo(500,600,700,1200,800,800);
        mPath = path;

三阶贝塞尔曲线实现方法cubicTo()方法中前两个参数(500,600)和中间两个参数(700,1200)为控制点P1、P2的坐标,最后两个参数(800,800)为终点P3的坐标 ,我们可以看下源码

/**
     * Add a cubic bezier from the last point, approaching control points
     * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
     * made for this contour, the first point is automatically set to (0,0).
     *
     * @param x1 The x-coordinate of the 1st control point on a cubic curve
     * @param y1 The y-coordinate of the 1st control point on a cubic curve
     * @param x2 The x-coordinate of the 2nd control point on a cubic curve
     * @param y2 The y-coordinate of the 2nd control point on a cubic curve
     * @param x3 The x-coordinate of the end point on a cubic curve
     * @param y3 The y-coordinate of the end point on a cubic curve
     */
    public void cubicTo(float x1, float y1, float x2, float y2,
                        float x3, float y3) {
        isSimplePath = false;
        nCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }
通过源码注释,我们可以看到三阶贝塞尔曲线是从上次最后移动的点开始,经过控制点(X1,Y1)和(X2,Y2 ),终点在(X3,Y3),如果前面没有moveTo操作,就设为(0,0),代码中我们通过moveto ()方法将起始点P0设为(400,800)。



如果我们不通过moveTo()方法更改P0坐标,代码如下

 //一阶贝赛尔曲线
        Path path = new Path();
        path.moveTo(100, 100);
        path.lineTo(400, 400);

        //二阶贝塞尔曲线,和下面表达不同
        path.quadTo(700, 100, 800, 400);
        //相对的实现,不用考虑运算
//        path.rQuadTo(300, -300, 400, 0);
//        path.moveTo(400, 800);
        //三阶贝赛尔曲线
        path.cubicTo(900,200,1000,1200,1100,800);
        mPath = path;

改写参数,三阶贝塞尔曲线起始点P0坐标就为二阶贝塞尔曲线终点坐标(800,400)


三阶贝塞尔曲线也有个相似的实现方法

 //三阶贝赛尔曲线
//        path.cubicTo(500,600,700,1200,800,800);
        path.rCubicTo(100, -200, 300, 400, 400, 0);
        mPath = path;

rCubicTo()方法中前两个参数(100,-200)是P1(500,600)与P0(400,800)的相差距离,中间两个参数(300,400)是P2(700,1200)与P0(400,800)的相差距离,后面两个参数(400,0)是P3(800,800)与P0(400,800)的相差距离,我们看下源码

  /**
     * Same as cubicTo, but the coordinates are considered relative to the
     * current point on this contour. If there is no previous point, then a
     * moveTo(0,0) is inserted automatically.
     */
    public void rCubicTo(float x1, float y1, float x2, float y2,
                         float x3, float y3) {
        isSimplePath = false;
        nRCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
    }
通过源码我们可以看到,这个方法类似于cubicTo(),只不过它的参数是P1、P2与P3相对于最后一个点P0(400,800)的差值,如果没有最后一个点,就设置为(0,0)。


五 。四阶及以上贝赛尔曲线代码实现

因为Android只提供了1~3阶贝赛尔曲线代码实现方法,四阶及其以上用到的情况很少,所以想实现四阶及其以上贝赛尔曲线,我们得通过代码一步一步的实现。

public class BezierView extends View {

    private Path mPath = new Path();
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

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

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

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

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

        init();
    }

    private void init() {
        //初始化画笔
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(5);
        mPaint = paint;

        initBezier();
    }

    /**
     * 初始化贝塞尔曲线4阶及以上
     */
    private void initBezier() {
        //(0,0)、(300,300)、(200,700)、(500,500)、(700,1200)
        float[] xPoints = new float[]{0, 300, 200, 500, 700};
        float[] yPoints = new float[]{0, 300, 700, 1200, 200};

        Path path = new Path();
        //fps为组成贝赛尔曲线的点的个数
        int fps = 30;
        for (int i = 0; i <= fps; i++) {
            //进度
            float progress = i / (float) fps;
            float x = calculateBezier(progress, xPoints);
            float y = calculateBezier(progress, yPoints);
            //使用连接的方式,当x、y变动足够小的情况下,就是平滑曲线
            path.lineTo(x, y);
        }
        mPath = path;
    }

    /**
     * 计算某时刻的贝塞尔所处的值(x或y)
     *
     * @param t      时间、进度(0~1)
     * @param values 贝塞尔点的集合 (x或y)
     * @return 当期t时刻贝塞尔所处的点
     */
    private float calculateBezier(float t, float... values) {
        //采用双重for循环
        final int len = values.length;
        for (int i = len - 1; i > 0; i--) {
            //外层
            for (int j = 0; j < i; j++) {
                values[j] = values[j] + (values[j + 1] - values[j]) * t;
            }
        }

        //运算时结果保存在第一位,所以我们返回第一位
        return values[0];
    }

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

        canvas.drawPath(mPath, mPaint);
    }
}

通过代码我们可以发现,我们在initBezier()方法中设置了P0~P4五个点,是一个四阶的贝赛尔曲线,因为贝塞尔曲线是由一个个贝塞尔点连线组成的,所以我们先设置贝塞尔点的个数fps为30,calculateBezier()方法采用双层for循环,主要是计算t时刻,贝塞尔点x或y的坐标,然后在for循环内通过这个方法得到贝塞尔点的坐标,再将每个贝塞尔点连线。


我们最上面有一系列的点,相邻两个点两两运算,将运算得到的值存到前面一个点上(两个点相比较而言在前面的一个点),这时候将少掉一位,然后依次运算,最后得到一个点,这个点就是贝塞尔点。calculateBezier()方法i循环为外层循环,j为内层循环,通过--i不断地循环,然后当i=0时,外层循环结束,但j已经计算了最后一次,得到value【0】,也就是最后的返回值。


下面我们设置不同的fps值,如下图:

                              

                    fps=30                                                                                       fps=3000


下面我们验证一下我们的代码是否正确。

我们再用Android自带方法实现一个三阶贝赛尔曲线

private Path mSrcPath = new Path();
 /**
     * 初始化贝塞尔曲线4阶及以上
     */
    private void initBezier() {
        //(0,0)、(300,300)、(200,700)、(500,500)、(700,1200)
        float[] xPoints = new float[]{0, 200, 500, 700};
        float[] yPoints = new float[]{0, 700, 1200, 200};

        Path path = new Path();
        //fps为组成贝赛尔曲线的点的个数
        int fps = 3000;
        for (int i = 0; i <= fps; i++) {
            //进度
            float progress = i / (float) fps;
            float x = calculateBezier(progress, xPoints);
            float y = calculateBezier(progress, yPoints);
            //使用连接的方式,当x、y变动足够小的情况下,就是平滑曲线
            path.lineTo(x, y);
        }
        mPath = path;
        //三阶贝赛尔曲线
        mSrcPath.moveTo(100,0);
        mSrcPath.cubicTo(300, 700, 600, 1200, 800, 200);
    }
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mPaint.setColor(Color.BLACK);
        canvas.drawPath(mPath, mPaint);

        mPaint.setColor(Color.BLUE);
        canvas.drawPath(mSrcPath, mPaint);
    }
我们先去掉一个点的坐标(300,300),将自己实现的代码变为3阶贝赛尔曲线,然后重画一个三阶贝赛尔曲线,并将它的x轴坐标的值增加100px,并用两种不同颜色的画笔绘制。
                                
我们可以看到两条曲线一模一样,我们的代码也就是正确的。如果我们想要实现五阶、六阶及其以上,我们只要在initBezier()方法里面加入相应点的坐标即可。


猜你喜欢

转载自blog.csdn.net/qq_35820350/article/details/79479796