前言
貝塞爾曲線于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();
}
}