Android自定义View(四)Path之贝塞尔曲线

一、概述

1、贝赛尔曲线来源

在数学的数值分析领域中,贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例
贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线

2、贝塞尔曲线公式

一阶贝塞尔曲线

公式:B(t)=(1 - t)Po + tP1,t∈[0,1]

动画演示为:

 

其中Po表示起点,P1表示终点,t表示时间,B(t)表示计算结果

图中移动的黑色点代表随着时间的增长B(t)的变化规律,其实一阶贝塞尔曲线就是在起点和终点之间随着B的变化而形成一个匀速增长的轨迹,所以说B(t)也是一个匀速增长的值

二阶贝塞尔曲线

公式:B(t)=(1 - t)²Po + 2t(1 - t)P1 + t²P2,t∈[0,1]

在这里Po是起点,P2是终点,P1是控制点,我们给公式做一个分解因式的转换

            B(t)= (1-t)[(1-t)Po + tP1] + t[(1-t)P1 + tP2]

通过分解因式,我们发现其实二阶曲线公式是一阶曲线公式的组合,首先各自取得(Po、P1)和(P1、P2)的一阶曲线值,然后再将两个值作为一阶曲线的起点和终点计算得到B(t)得到整个二阶曲线的值

动画演示:

可以看到二阶贝塞尔曲线会形成一个圆滑的曲线轨迹

下面取动画的其中一帧来分析一下,假设t = 0.25时刻,此时的画面如下:

其中点B为公式的计算结果,上面我们分析过B(t)的生成规律,这里我们直观地解析一下

首先Qo是Po和P1作为一阶曲线的生成结果,Q1是P1和P2作为一阶曲线的生成结果,然后Qo和Q1作为一阶曲线生成的B就是当前二阶曲线的最终结果值。随着时间的变化,就形成了上面的红色轨迹线了

三阶贝塞尔曲线

公式:B(t)= Po(1 - t)^3 + 3P1(1-t)² + 3P2 t²(1-t) + P3 t^3,t∈[0,1] 

演示动画:

同样我们取t = 0.25时刻,如图

其实三阶曲线的形成规律和二阶曲线是一样的,这里就不再详细解析了,因为三阶曲线在我Android开发中也用得很少,当然还有四阶、五阶曲线等等。本篇我们主要讲解二阶曲线,下面要讲的例子也是基于二阶曲线实现的

 

二、Android中的贝塞尔曲线

在android的Path类中有四个与贝塞尔曲线相关的方法,如下

//二阶贝赛尔
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
//三阶贝赛尔
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)

三阶曲线的方法这里就不作分析了,来看看quadTo方法,有四个参数,其中x1和y1是指控制点的坐标,x2和y2是终点的坐标,那起点的坐标在哪里呢?其实起点的坐标就是上一个曲线的终点坐标,或者Path开始的moveTo方法的坐标,如果没有指定起点坐标,那么就会默认为原点了。至于rQuadTo方法我们后面再分析

1.quadTo的使用及自定义手指轨迹画板

下面我们自定义一个可以用手指画轨迹的画板,代码如下:

public class TrackView extends View {

    private Path mPath = new Path();

    public TrackView(Context context) {
        super(context);
    }

    public TrackView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mPath.moveTo(event.getX(), event.getY());
                return true;
            }
            case MotionEvent.ACTION_MOVE:
                mPath.lineTo(event.getX(), event.getY());
                invalidate();
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);

        canvas.drawPath(mPath, paint);
    }

    public void reset() {
        mPath.reset();
        invalidate();
    }

}

很简单,就是根据手势轨迹生成的一系列点生成一个Path,然后画出来,来看看效果

Android自定义View(四)Path之贝塞尔曲线_第1张图片

放大了图片,可以看到明显的纹路,这是因为在手指滑动走过的时候,不可能平滑地过渡,坐标之间变化会比较剧烈,所以整条线段就会形成很多比较明显的转折。好了,到这里我们是不是自然就想到了可以用二阶贝塞尔曲线来实现线段的转折呢,从而实现线段之间的平滑过渡

来看看二阶贝塞尔曲线是怎么实现两个线段的平滑过渡的?下面这个图

Android自定义View(四)Path之贝塞尔曲线_第2张图片

上面假设有三个手指触点,然后形成了两条转折的线段,然后把线段AB的中点P0作为曲线的起点,点B作为曲线的控制点,线段BC的中点P1作为曲线的终点,这样就形成了一个生成二阶贝塞尔曲线的条件了。为什么要取AB和BC的中点作为曲线的起点和终点,而不是A和C呢?因为这个曲线的终点就是下一个曲线的起点,比如P1会成为下一个曲线的起点,而点C明显不可能成为下一个曲线的起点,所以取线段的中间点作为起点和终点是最合适的

接下来改造一下TrackView,如下

public class TrackView extends View {

    private Path mPath = new Path();

    private float mPreX,mPreY;

    public TrackView(Context context) {
        super(context);
    }

    public TrackView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                mPath.moveTo(event.getX(),event.getY());
                mPreX = event.getX();
                mPreY = event.getY();
                return true;
            }
            case MotionEvent.ACTION_MOVE:{
                float endX = (mPreX+event.getX())/2;
                float endY = (mPreY+event.getY())/2;
                mPath.quadTo(mPreX,mPreY,endX,endY);
                mPreX = event.getX();
                mPreY =event.getY();
                invalidate();
            }
            break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Paint paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(Color.RED);
        paint.setStrokeWidth(2);

        canvas.drawPath(mPath,paint);
    }

    public void reset() {
        mPath.reset();
        invalidate();
    }

}

用mPreX和mPreY来记录上一个曲线的终点,然后成为下一个曲线的起点,我们来看看画图效果

Android自定义View(四)Path之贝塞尔曲线_第3张图片

看起来明显比上一个B要顺滑吧!

2、rQuadTo的使用及自定义水波纹效果

该函数声明如下

public void rQuadTo(float dx1, float dy1, float dx2, float dy2)

其实rQuadTo和quadTo的使用效果是一样的,都是二阶贝塞尔曲线的函数,只不过它们参数的意义有差别。rQuadTo的参数的值是以上一个曲线的终点作为参照的相对值

其中:dx1和dy1作为当前曲线的控制点相对于上一个终点增加或减少的相对值,可正可负。dx2和dy2则作为当前曲线终点的相对值

举个例子,首先用quadTo来实现一个曲线段,如下代码

        mPath.moveTo(200,200);
        mPath.quadTo(300,300,400,200);
        mPath.quadTo(500,100,600,200);

如果用rQuadTo实现同样的效果,代码如下

        mPath.moveTo(200,200);
        mPath.rQuadTo(100,100,200,0);
        mPath.rQuadTo(100,-100,200,0);
        
        //上面的代码等效于
        mPath.moveTo(200,200);
        mPath.quadTo(200+100,200+100,200+200,200+0);
        mPath.quadTo(200+200+100,200+0-100,200+200+200,200+0+0);

        //然后等同于
        mPath.moveTo(200,200);
        mPath.quadTo(300,300,400,200);
        mPath.quadTo(500,100,600,200);

其实用rQuadTo比quadTo更加方便于参数的取值,我们只需要根据曲线的变化趋势就可以知道参数该取什么值了,而不用每次都要知道上一个曲线的终点坐标是什么

下面我们要来实现一个水波纹效果的图形,如下

Android自定义View(四)Path之贝塞尔曲线_第4张图片

其实这个实现原理很简单,先来看看完整代码

public class WaveView extends View{

    private Paint mPaint;
    private Path mPath;
    //一个波浪长,相当于两个二阶贝塞尔曲线的长度
    private int mItemWaveLength = 400;
    //波浪在Y轴方向的位置
    int originY = 400;
    //波浪幅度
    private int range=100;
    private int dx;

    public WaveView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setColor(Color.GREEN);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        int halfWaveLen = mItemWaveLength/2; //半个波长,即一个贝塞尔曲线长度
        mPath.moveTo(-mItemWaveLength+dx,originY);  //波浪的开始位置
        //每一次for循环添加一个波浪的长度到path中,根据view的宽度来计算一共可以添加多少个波浪长度
        for (int i = -mItemWaveLength;i<=getWidth()+mItemWaveLength;i+=mItemWaveLength){
            mPath.rQuadTo(halfWaveLen/2,-range,halfWaveLen,0);
            mPath.rQuadTo(halfWaveLen/2,range,halfWaveLen,0);
        }
        mPath.lineTo(getWidth(),getHeight());
        mPath.lineTo(0,getHeight());
        mPath.close(); //封闭path路径

        canvas.drawPath(mPath,mPaint);
    }

    public void startAnim(){
        //根据一个动画不断得到0~mItemWaveLength的值dx,通过dx的增加不断去改变波浪开始的位置,dx的变化范围刚好是一个波浪的长度,
        //所以可以形成一个完整的波浪动画,假如dx最大小于mItemWaveLength的话, 就会不会画出一个完整的波浪形状
        ValueAnimator animator = ValueAnimator.ofInt(0,mItemWaveLength);
        animator.setDuration(2000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                dx = (int)animation.getAnimatedValue();
                postInvalidate();
            }
        });
        animator.start();
    }

}

基本上代码的注释也能解析清楚波浪View的形成原理了,重点理解for循环那里,根据定义的波浪长度和View的宽度计算要多少个波浪才能充满整个View的宽度,然后画上去就OK了

关于贝塞尔曲线的相关知识就到这里吧!

 

你可能感兴趣的:(Android自定义控件,Android)