天天看點

貝塞爾曲線基礎使用

前言

貝塞爾曲線于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();
    }
}
           
貝塞爾曲線基礎使用

繼續閱讀