天天看点

贝塞尔曲线基础使用

前言

贝塞尔曲线于1962由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。现在贝塞尔曲线在计算机图形学领域也是一个相当重要的参数曲线,很多画图工具软件都包含贝塞尔曲线的工具对象。Android开发过程中也可以通过它实现很多有趣的特效动画,这里通过简单的代码编写来深入学习贝塞尔曲线的生成。

曲线生成

一阶贝塞尔曲线

给定点两个点,一阶贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:

贝塞尔曲线基础使用

接下来使用编程来实现画出这条线,需要在自定义的View控件中包含开始和结束两个点,使用属性动画来做t的生成对象。

public class BezierView extends View {
    private Paint mPaint;
    // 开始位置
    private Point mStart;
    // 结束位置
    private Point mEnd;
    // t是固定值的时候当前点的位置
    private Point mCurrent;
    private ValueAnimator mValueAnimator;
    // 当前t的值
    private float mProgress;

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

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

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

    private void init() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeWidth();

        mStart = new Point();
        mStart.x = ;
        mStart.y = ;

        mEnd = new Point();
        mEnd.x = ;
        mEnd.y = ;

        mCurrent = new Point();

        // t从0到1变化耗时3秒
        mValueAnimator = ValueAnimator.ofFloat(, f);
        mValueAnimator.setDuration();
        mValueAnimator.addUpdateListener(animation -> {
            mProgress = animation.getAnimatedFraction();
            // 每次t发生变化就刷新界面
            invalidate();
        });
        mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        post(() -> {
           mValueAnimator.start();
        });
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 根据当前t的值,获取当前生成的点
        mCurrent.x = (int) (mStart.x + mProgress * (mEnd.x - mStart.x));
        mCurrent.y = (int) (mStart.y + mProgress * (mEnd.y - mStart.y));

        mPaint.setColor(Color.RED);
        canvas.drawLine(mStart.x, mStart.y, mCurrent.x, mCurrent.y, mPaint);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(mCurrent.x, mCurrent.y, , mPaint);
        mPaint.setColor(Color.GREEN);
        canvas.drawLine(mCurrent.x + , mCurrent.y + , mEnd.x, mEnd.y, mPaint);
    }
}
           

上面红色的线就是一阶贝塞尔曲线生成的直线,这个效果还是比较简单的,涉及到的点相对比较少。

贝塞尔曲线基础使用

二阶贝塞尔曲线

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

贝塞尔曲线基础使用

如果直接使用这个公式去计算结果然后把所有的点连接起来,也是可以生成贝塞尔曲线的,不过这种明显不够直观,这里还是使用前面生成直线的方式生成一条贝塞尔曲线。

public class Bezier2View extends View {
    private Paint mPaint;

    // 开始点,控制点和结束点
    private Point mStart;
    private Point mControl;
    private Point mEnd;

    // 开始点和控制点之间随t变化生成的点
    private Point mCurrent1;
    // 控制点和结束点之间随t变化的点
    private Point mCurrent2;

    private Path mPath;

    // 记录上一个t的贝塞尔曲线位置
    private Point mLastPoint;

    private Point mTmpPoint;

    // t的动画生成对象
    private ValueAnimator mValueAnimator;
    // 记录t的值
    private float mProgress;

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

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

    public Bezier2View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

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

        mStart = new Point();
        mStart.x = ;
        mStart.y = ;

        mLastPoint = new Point();
        mLastPoint.x = mStart.x;
        mLastPoint.y = mStart.y;

        mControl = new Point();
        mControl.x = ;
        mControl.y = ;

        mEnd = new Point();
        mEnd.x = ;
        mEnd.y = ;

        mCurrent1 = new Point();
        mCurrent2 = new Point();
        mTmpPoint = new Point();

        mPath = new Path();

        mValueAnimator = ValueAnimator.ofFloat(, f);
        mValueAnimator.setDuration();
        mValueAnimator.addUpdateListener(animation -> {
            mProgress = animation.getAnimatedFraction();
            // 如果刚开始或者刚结束删除上次画的贝塞尔曲线
            if (mProgress <= f || mProgress >= f) {
                mPath.reset();
                mLastPoint.x = mStart.x;
                mLastPoint.y = mStart.y;
            }
            invalidate();
        });
        mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        post(() -> {
           mValueAnimator.start();
        });
    }

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

        // 获取开始点和控制点之间的随t变化的点位置
        mCurrent1.x = (int) (mStart.x + mProgress * (mControl.x - mStart.x));
        mCurrent1.y = (int) (mStart.y + mProgress * (mControl.y - mStart.y));
        mPaint.setColor(Color.GRAY);

        // 绘制从开始点到mCurrent1点的直线
        canvas.drawLine(mStart.x, mStart.y, mCurrent1.x, mCurrent1.y, mPaint);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(mCurrent1.x, mCurrent1.y, , mPaint);
        mPaint.setColor(Color.GRAY);
        canvas.drawLine(mCurrent1.x + , mCurrent1.y - , mControl.x, mControl.y, mPaint);

        // 后去控制点和结束点之间的随t变化的点位置
        mCurrent2.x = (int) (mControl.x + mProgress * (mEnd.x - mControl.x));
        mCurrent2.y = (int) (mControl.y + mProgress * (mEnd.y - mControl.y));

        // 绘制从控制点到结束点的直线
        mPaint.setColor(Color.GRAY);
        canvas.drawLine(mControl.x, mControl.y, mCurrent2.x, mCurrent2.y, mPaint);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(mCurrent2.x, mCurrent2.y, , mPaint);
        mPaint.setColor(Color.GRAY);
        canvas.drawLine(mCurrent2.x + , mCurrent2.y + , mEnd.x, mEnd.y, mPaint);

        mPaint.setColor(Color.CYAN);
        canvas.drawLine(mCurrent1.x + , mCurrent1.y + , mCurrent2.x - , mCurrent2.y - , mPaint);

        // 计算开始点和控制点一阶位置和控制点与结束点的一阶位置
        // 计算这两个点之间的一阶点位置
        mTmpPoint.x = (int) (mCurrent1.x + mProgress * (mCurrent2.x - mCurrent1.x));
        mTmpPoint.y = (int) (mCurrent1.y + mProgress * (mCurrent2.y - mCurrent1.y));
        // 将这个点和上一次记录的点用直线连接起来
        mPath.moveTo(mLastPoint.x, mLastPoint.y);
        mPath.lineTo(mTmpPoint.x, mTmpPoint.y);
        mLastPoint.x = mTmpPoint.x;
        mLastPoint.y = mTmpPoint.y;
        mPaint.setColor(Color.MAGENTA);
        // 绘制这条贝塞尔曲线
        canvas.drawPath(mPath, mPaint);
    }
}
           

使用二阶贝塞尔曲线的时候需要的计算量明显比一阶要多了很多,需要的使用多个一阶的组合生成二阶曲线,到三阶曲线绘制操作已经十分的复杂。

贝塞尔曲线基础使用

幸运的是Android的Path提供了贝塞尔曲线的二阶和三阶函数,使用它们就可以很轻松的绘制贝塞尔曲线,只需要提供开始点、控制点和结束点。

public class CustomBezier2View extends View {
    private Point mStart;
    private Point mControl;
    private Point mEnd;
    private Paint mPaint;
    private Path mPath;

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

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

    public CustomBezier2View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth();

        mStart = new Point();
        mStart.x = ;
        mStart.y = ;

        mControl = new Point();
        mControl.x = ;
        mControl.y = ;

        mEnd = new Point();
        mEnd.x = ;
        mEnd.y = ;

        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(mStart.x, mStart.y, , mPaint);
        canvas.drawCircle(mControl.x, mControl.y, , mPaint);
        canvas.drawCircle(mEnd.x, mEnd.y, , mPaint);
        mPaint.setColor(Color.RED);
        mPath.moveTo(mStart.x, mStart.y);
        mPath.quadTo(mControl.x, mControl.y, mEnd.x, mEnd.y);
        canvas.drawPath(mPath, mPaint);
    }
}
           
贝塞尔曲线基础使用

三阶贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三阶贝塞尔曲线,开始和结束的点会被经过,中间的两点只是做为控制点使用,曲线不会经过它们。

贝塞尔曲线基础使用

三阶的实现这里就直接使用Android提供的方法来实现,定义开始点,结束点和两个控制点就可以了。

public class CustomBezier3View extends View {
    private Point mStart;
    private Point mControl1;
    private Point mControl2;
    private Point mEnd;
    private Paint mPaint;
    private Path mPath;

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

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

    public CustomBezier3View(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setColor(Color.RED);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth();

        mStart = new Point();
        mStart.x = ;
        mStart.y = ;

        mControl1 = new Point();
        mControl1.x = ;
        mControl1.y = ;

        mControl2 = new Point();
        mControl2.x = ;
        mControl2.y = ;

        mEnd = new Point();
        mEnd.x = ;
        mEnd.y = ;

        mPath = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(Color.BLUE);
        canvas.drawCircle(mStart.x, mStart.y, , mPaint);
        canvas.drawCircle(mControl1.x, mControl1.y, , mPaint);
        canvas.drawCircle(mControl2.x, mControl2.y, , mPaint);
        canvas.drawCircle(mEnd.x, mEnd.y, , mPaint);

        mPaint.setColor(Color.RED);
        mPath.moveTo(mStart.x, mStart.y);
        mPath.cubicTo(mControl1.x, mControl1.y, mControl2.x, mControl2.y, mEnd.x, mEnd.y);
        canvas.drawPath(mPath, mPaint);
    }
}
           
贝塞尔曲线基础使用

水波纹效果

水波纹下过最重要的是先绘制出一条正弦曲线,再使用多条正弦曲线填充满屏幕,再在屏幕的左侧添加和屏幕宽度一样的正弦曲线,最后再不断的修改曲线的相位值就可以实现无限循环的正弦波效果。

public class WaveView extends View {
    private int mVisibleWaveCount = ;
    private int mHeight = CommonUtils.dp2px();
    private int mOffset = ;
    private int mWaveLength;
    private int mWaveHeight;
    private Paint mPaint;
    private Path mPath;
    private ValueAnimator mValueAnimator;

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

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

    public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mPaint.setDither(true);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth();
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(Color.CYAN);
        mPath = new Path();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWaveLength = w / mVisibleWaveCount;
        mWaveHeight = CommonUtils.dp2px();
        mValueAnimator = ValueAnimator.ofInt(-w, );
        mValueAnimator.setDuration();
        mValueAnimator.setRepeatMode(ValueAnimator.RESTART);
        mValueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mValueAnimator.setInterpolator(new LinearInterpolator());
        mValueAnimator.addUpdateListener( animation -> {
            // offset从负的屏幕宽度到0,这时左边的正弦波播放完毕,需要从头开始
            mOffset = (int) animation.getAnimatedValue();
            invalidate();
        });
        mValueAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int drawWaveCount =  * mVisibleWaveCount;
        mPath.reset();
        // 绘制多个波
        for (int i = ; i < drawWaveCount; i++) {
            drawWave(mOffset + i * mWaveLength, mHeight);
        }
        canvas.drawPath(mPath, mPaint);
    }

    // 绘制一个包含波谷和波峰的正弦波,底线会合并起来
    // x,y是开始为之的坐标
    private void drawWave(int x, int y) {
        int halfLength = mWaveLength / ;
        int controlHeight = mWaveHeight + CommonUtils.dp2px();
        mPath.moveTo(x, y);
        mPath.rQuadTo(halfLength / , -controlHeight, halfLength, );
        mPath.rQuadTo(halfLength / , controlHeight, halfLength, );
        mPath.rLineTo(, mHeight);
        mPath.rLineTo(-mWaveLength, );
        mPath.close();
    }
}
           
贝塞尔曲线基础使用

继续阅读