?

?
目錄
1)什么是貝塞爾曲線
2)貝塞爾曲線圖解
3)Android繪制貝塞爾曲線
4)繪制水波紋效果
?
概述
什么是貝塞爾曲線?
貝塞爾曲線的數(shù)學(xué)基礎(chǔ)是早在 1912 年就廣為人知的伯恩斯坦多項(xiàng)式。但直到 1959 年,當(dāng)時(shí)就職于雪鐵龍的法國(guó)數(shù)學(xué)家 Paul de Casteljau 才開始對(duì)它進(jìn)行圖形化應(yīng)用的嘗試,并提出了一種數(shù)值穩(wěn)定的 de Casteljau 算法。然而貝塞爾曲線的得名,卻是由于 1962 年另一位就職于雷諾的法國(guó)工程師 Pierre Bézier 的廣泛宣傳。他使用這種只需要很少的控制點(diǎn)就能夠生成復(fù)雜平滑曲線的方法,來(lái)輔助汽車車體的工業(yè)設(shè)計(jì)。
只要你使用過(guò)圖像處理工具,肯定對(duì)“鋼筆”這個(gè)詞不陌生,它本質(zhì)上就是運(yùn)用貝塞爾曲線來(lái)作為計(jì)算基礎(chǔ)繪制出來(lái)的路徑:

?
貝塞爾曲線原理
貝塞爾曲線是怎么描繪出這樣一條弧線的呢,其實(shí)主要是依靠頂點(diǎn)間的比例來(lái)計(jì)算,以二階貝塞爾為例,示意圖如下:

可以看到一共有6個(gè)點(diǎn),假設(shè)此時(shí)AD占AB的25%,那么在BC上也有這么一個(gè)點(diǎn)F,使得BF:BC也是25%,連接DF,在DF上面再找出使得DG:DF=25%的點(diǎn)G,以這個(gè)為基本公式,繪制出D點(diǎn)從A運(yùn)動(dòng)到B的過(guò)程中,計(jì)算出來(lái)一系列的G點(diǎn)形成的弧線,即為二階貝塞爾曲線的路徑,動(dòng)態(tài)效果圖如下:

三階貝塞爾曲線其實(shí)就是在二階的基礎(chǔ)上,再增加一條邊線,如下:

可以看到,多出了一條CD線,同樣是需要滿足AE:AB = BF:BC = CG:CD,計(jì)算出E、F、G之后,連接EF和FG,可以得到兩條直線,接著就按照二階的計(jì)算方式繼續(xù)計(jì)算,得到點(diǎn)O的位置,可以看出三階是在二階的基礎(chǔ)上再套一層,所以才稱之為三階貝塞爾,動(dòng)態(tài)效果圖如下:

依此類推,還會(huì)有四階、五階等等更復(fù)雜的貝塞爾曲線,但在Android開發(fā)中只提供了二階和三階的API,因此我們只探討這兩種的繪制方式。
?
繪制貝塞爾曲線
在Android中,Path類提供了四個(gè)繪制貝塞爾曲線的方法:
二階貝塞爾繪制API:
public void quadTo(float x1, float y1, float x2, float y2)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
三階貝塞爾繪制API:
public void quadTo(float x1, float y1, float x2, float y2, float x3, float y3)
public void rQuadTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3)
可以看到二階與三階的區(qū)別就在于多了一組參數(shù),首先看下二階,剛才已經(jīng)分析了二階貝塞爾的繪制一共有3個(gè)重要的頂點(diǎn),可以理解為起始點(diǎn)(示意圖中的A),控制點(diǎn)(示意圖中的B),終點(diǎn)(示意圖中的C),這里傳入兩個(gè)頂點(diǎn),分別代表著控制點(diǎn)和終點(diǎn),那么問(wèn)題來(lái)了,起始點(diǎn)呢?起始點(diǎn)就是Path上一次的終點(diǎn)(比如moveTo移動(dòng)到的點(diǎn)),如果沒(méi)有指定(即之前還從未移動(dòng)過(guò)Path),則默認(rèn)以控件左上角為起始點(diǎn),舉個(gè)例子,我們繪制一段簡(jiǎn)單的貝塞爾:
/**
* Created by YANG on 2019/2/23.
*/
public class BezierView extends View {
private Paint mPaint;
private Path mBezierPath;
private Path mPointPath;
private Point mStartPoint;
private Point mControlPoint;
private Point mEndPoint;
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) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
mBezierPath = new Path();
mPointPath = new Path();
mStartPoint = new Point();
mStartPoint.set(100, 300);
mControlPoint = new Point();
mControlPoint.set(300, 100);
mEndPoint = new Point();
mEndPoint.set(500, 500);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//貝塞爾
mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);
//連接線
mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
mPointPath.lineTo(mEndPoint.x, mEndPoint.y);
//繪制起始點(diǎn)、控制點(diǎn)、終點(diǎn)的連線
canvas.drawPath(mPointPath, mPaint);
//繪制貝塞爾
mPaint.setColor(Color.RED);
canvas.drawPath(mBezierPath, mPaint);
}
}
一共聲明了3個(gè)點(diǎn),且首先調(diào)用moveTo(mStartPoint.x, mStartPoint.x)將起始點(diǎn)移動(dòng)到(300,300),接著調(diào)用quadTo將控制點(diǎn)和終點(diǎn)傳進(jìn)去,就可以得到一條貝塞爾曲線(紅色部分):
因此quadTo傳進(jìn)去的參數(shù)是控制點(diǎn)和終點(diǎn)的坐標(biāo)位置,那Path的rQuadTo又有什么用呢?其實(shí)rQuadTo功能上跟quadTo是一樣的,但是傳進(jìn)去的是相對(duì)距離,也就是說(shuō)相對(duì)于起始點(diǎn)的位移,比如我們?cè)趧偛诺睦又性偌狱c(diǎn)東西,追加一段曲線mBezierPath.rQuadTo(200, 300, 400, -200);:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//貝塞爾
mBezierPath.moveTo(mStartPoint.x, mStartPoint.y);
mBezierPath.quadTo(mControlPoint.x, mControlPoint.y, mEndPoint.x, mEndPoint.y);
mBezierPath.rQuadTo(200, 300, 400, -200);
//連接線
mPointPath.moveTo(mStartPoint.x, mStartPoint.y);
mPointPath.lineTo(mControlPoint.x, mControlPoint.y);
mPointPath.lineTo(mEndPoint.x, mEndPoint.y);
//繪制起始點(diǎn)、控制點(diǎn)、終點(diǎn)的連線
canvas.drawPath(mPointPath, mPaint);
//繪制貝塞爾
mPaint.setColor(Color.RED);
canvas.drawPath(mBezierPath, mPaint);
}
效果如圖:
可以看到第二段曲線的起點(diǎn)是第一段曲線的終點(diǎn),且可以發(fā)現(xiàn),rQuadTo傳的控制點(diǎn)(200,300)并非坐標(biāo),而是相對(duì)于第一段曲線的終點(diǎn)(500,500)來(lái)計(jì)算,即(500+200, 500+300)才是第二段曲線控制點(diǎn)的真正坐標(biāo),同理第二段曲線終點(diǎn)的坐標(biāo)是(500+40, 500-200)。
三階貝塞爾曲線的方法的使用方法跟二階貝塞爾曲線差不多,就不再?gòu)?fù)述了。
?
繪制水波紋效果
上面的例子繪制了一段簡(jiǎn)單的波浪線,這其實(shí)就是我們繪制水波紋效果的基礎(chǔ),就相當(dāng)于完成了一個(gè)浪,如果有很多個(gè)水浪就可以組合成此起彼伏的效果:
public class BezierView extends View{
private Paint paint;
private Path mPath;
private int mItemWidth = 600;
public BezierView2(Context context) {
this(context, null);
}
public BezierView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
int halfItem = mItemWidth / 2;
mPath.moveTo(0, 300);
for(int i=0; i<mItemWidth + getWidth(); i+=mItemWidth){
mPath.rQuadTo(halfItem/2, -100, halfItem, 0);
mPath.rQuadTo(halfItem/2, 100, halfItem, 0);
}
canvas.drawPath(mPath, paint);
}
}
我們將每段波浪的寬度定義為600,因此每半段波浪的高度為300,首先將起點(diǎn)移動(dòng)到(0,300)處,即整個(gè)View最左側(cè)的一個(gè)點(diǎn),接著開始遍歷繪制后續(xù)多段波浪,mPath.rQuadTo(halfItem/2, -100, halfItem, 0);表示右移半個(gè)波浪, 并且上移100,即一個(gè)浪的最高點(diǎn),接著mPath.rQuadTo(halfItem/2, 100, halfItem, 0);再右移半個(gè)波浪,并且下移100,即一個(gè)浪的最低點(diǎn),這就形成了一段完整的波浪,然后以此循環(huán),直到超過(guò)View的最大寬度:
?
?
靜態(tài)效果完成了,如何讓它動(dòng)起來(lái)呢?就要結(jié)合ValueAnimator了,不斷改變整個(gè)波浪的起始點(diǎn):
/**
* Created by YANG on 2019/2/23.
*/
public class BezierView2 extends View {
private Paint paint;
private Path mPath;
private int mItemWidth = 600;
private ValueAnimator mAnimator;
private int mOffsetX;
public BezierView2(Context context) {
this(context, null);
}
public BezierView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BezierView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
paint = new Paint();
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
mPath = new Path();
mAnimator = ValueAnimator.ofInt(0, mItemWidth);
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffsetX = (int) animation.getAnimatedValue();
invalidate();
}
});
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.setDuration(1000);
mAnimator.setRepeatCount(-1);
mAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
int halfItem = mItemWidth / 2;
//必須先減去一個(gè)浪的寬度,以便第一遍動(dòng)畫能夠剛好位移出一個(gè)波浪,形成無(wú)限波浪的效果
mPath.moveTo(-mItemWidth + mOffsetX, halfItem);
for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
}
canvas.drawPath(mPath, paint);
}
}
注意,起點(diǎn)改為了mPath.moveTo(-mItemWidth + mOffsetX, halfItem),為何不是(mOffsetX, halfItem)呢,因?yàn)閙OffsetX的變化范圍是從0到mItemWidth,如果一開始不減去mItemWidth,就會(huì)導(dǎo)致啟動(dòng)動(dòng)畫之后波浪左邊總是會(huì)露出一段空白區(qū)域,整個(gè)動(dòng)畫銜接不起來(lái),無(wú)法形成無(wú)限循環(huán)的視覺(jué)效果。
最后,再給這個(gè)水波紋效果填充上“水”,設(shè)置畫筆填充模式為Paint.FILL,換個(gè)顏色,然后將我們的路徑閉合起來(lái):
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
int halfItem = mItemWidth / 2;
//必須先減去一個(gè)浪的寬度,以便第一遍動(dòng)畫能夠剛好位移出一個(gè)波浪,形成無(wú)限波浪的效果
mPath.moveTo(-mItemWidth + mOffsetX, halfItem);
for (int i = -mItemWidth; i < mItemWidth + getWidth(); i += mItemWidth) {
mPath.rQuadTo(halfItem / 2, -100, halfItem, 0);
mPath.rQuadTo(halfItem / 2, 100, halfItem, 0);
}
//閉合路徑波浪以下區(qū)域
mPath.lineTo(getWidth(), getHeight());
mPath.lineTo(0, getHeight());
mPath.close();
canvas.drawPath(mPath, paint);
}
?
最終效果如下:

?
總結(jié)
之前一直有看到水波紋的效果,沒(méi)來(lái)得及細(xì)細(xì)研究,這次終于總結(jié)在一塊了,這里只是水波紋的基礎(chǔ)效果,還有很多拓展的方式,比如可以有多條水波紋疊加,水波紋進(jìn)度球等更炫酷的效果。當(dāng)然,貝塞爾曲線不單單可以做水波紋效果,還有很多其他的用法,類似于QQ的拖動(dòng)取消新消息提醒的效果,手寫路徑優(yōu)化等等,由于篇幅有限,下次再敘。
歡迎關(guān)注 Android小Y 的簡(jiǎn)書,更多Android精選自定義View
Android 玩轉(zhuǎn)PathMeasure之自定義支付結(jié)果動(dòng)畫
Android 自定義弧形旋轉(zhuǎn)菜單欄——衛(wèi)星菜單
Android 自定義帶入場(chǎng)動(dòng)畫的弧形百分比進(jìn)度條
GitHub:GitHub-ZJYWidget
CSDN博客:IT_ZJYANG
簡(jiǎn) 書:Android小Y
在Github上建了一個(gè)集合炫酷自定義View的項(xiàng)目,里面有很多實(shí)用的自定義View源碼及demo,會(huì)長(zhǎng)期維護(hù),歡迎Star~ 如有不足之處或建議還望指正,相互學(xué)習(xí),相互進(jìn)步,如果覺(jué)得不錯(cuò)動(dòng)動(dòng)小手給個(gè)Star, 謝謝~