上一篇我們說了 Path 的基本操作,這一篇讓我們來說一下 Path 的進階用法——貝塞爾曲線。
那什麼是貝塞爾曲線?貝塞爾曲線能在 Android 中實現什麼效果?以及如何做到的?這篇文章都會告訴你。
什麼是貝塞爾曲線?
貝塞爾曲線是由皮埃爾·貝塞爾發表的,他主要應用於汽車的主體進行設計,後來成為計算機圖形學相當重要的引數曲線。
貝塞爾曲線由什麼組成的?它通常由資料點和控制點兩個部分組成的。那什麼是資料點和控制點呢?請看下錶:
型別 | 作用 |
---|---|
資料點 | 曲線的起點和終點 |
控制點 | 控制曲線的彎曲程度 |
這樣聽起來可能還有點抽象,我們直接上圖來看看。
一階貝塞爾曲線:
一階貝塞爾曲線其實就是一條直線,沒有控制點,只有資料點 P0,P1,如下圖:
![一階貝塞爾曲線](https://i.iter01.com/images/0d52d706721ac0098b45d31f340a682163651e6a50a8ed5dda80aff515de71c4.jpg)
Android提供方法:lineTo()
二階貝塞爾曲線:
二階貝塞爾曲線有一個控制點 P1 和兩個資料點 P0,P2。如下圖:
![二階貝塞爾曲線](https://i.iter01.com/images/a65d4cdd6c6e0c60e98eb22bdf85c437d186d808cd579e3176032cb6826e0648.png)
Android 提供方法:quadTo()
三階貝塞爾曲線:
三階貝塞爾曲線有兩個控制點 P1,P2 和兩個資料點 P0,P3。如下圖:
![三階貝塞爾曲線](https://i.iter01.com/images/1e111a31e89aac48dc9473be8178996073336a4a2ea6bdf95b54ab7d7a7311d4.png)
Android 提供方法:cubicTo()
更高階的曲線 Android 並沒有提供 API ,所以在這隻會介紹二階和三階曲線,如果對更高階的曲線有興趣的話,可以去貝塞爾曲線———維基百科和貝塞爾曲線動態演示這兩個網站多瞭解一下。
貝塞爾曲線是怎麼形成的
那麼這條曲線究竟是怎麼形成的呢?先從二階曲線分析一下:
二階貝塞爾曲線形成原理:
1.連線 A,B 形成 AB 線段,連線 B,C 形成 BC 線段。
![連成AB,BC線段](https://i.iter01.com/images/1b82b03bfbd05054f3724cd77038ea7c2151097186c97d361464fba1f0e7c4d1.png)
2.在 AB 線段取一個點 D,BC 線段取一個點 E ,使其滿足條件: AD/AB = BE/BC,連線 D,E 形成線段 DE。
![連線DE](https://i.iter01.com/images/9787894fcbf9f220fc4376616429b2b5e1aea54e1c97c677a81efe0be9b0dd30.png)
3.在 DE 取一個點 F,使其滿足條件:AD/AB = BE/BC = DF/DE。
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/69fdd632202f412853a4372be86ea57104f8fc927507bfeb396ba6b7e215ea67.png)
4.而滿足這些條件的所有的F點所形成的軌跡就是二階貝塞爾曲線,動態過程如下:
![二階貝塞爾曲線](https://i.iter01.com/images/5304041254572bb071bd12fe596e742150a4b8414a220deb074b5c1187026128.gif)
三階貝塞爾曲線形成原理:
1.連線 A,B 形成 AB 線段,連線 B,C 形成 BC 線段,連線 C,D 形成 CD 線段。
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/b0baffbdb9d01e4959159a1b8f7fdcf17304840367fd7f03f92c1b497c983477.png)
2.在AB線段取一個點 E,BC 線段取一個點 F,CD 線段取一個點 G,使其滿足條件: AE/AB = BF/BE = CG/CD。連線 E,F 形成線段 EF,連線 F,G 形成線段 FG。
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/d220e3919e3bce1320e37351828f4fe0e2c061ad3b2795533380401609816782.png)
3.在EF線段取一個點 H,FG 線段取一個點 I,使其滿足條件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG。連線 H,I 形成線段 HI。
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/237b3095ae769e8999baffe32cc2e5b97da626a1dd81a9c3f89e67b96e0697b1.png)
4.在 HI 線段取一個點 J,使其滿足條件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG = HJ/HI。
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/b6acdec1249ec4a1b70317256a455e76bb84f8cab4c3273af465af90b335ff81.png)
5.而滿足這些條件的所有的J點所形成的軌跡就是三階貝塞爾曲線,動態過程如下:
![三階貝塞爾曲線](https://i.iter01.com/images/5fb5cfc99092f0b822c23a909ce8622c625cb02e350c834255324944d46bcfd2.gif)
在 Android 中使用貝塞爾曲線
說了這麼多原理,是時候要知道要怎麼運用貝塞爾曲線了。這裡我會用兩個例子來說明二階和三階貝塞爾曲線的運用:
二階曲貝塞爾曲線的應用:
方法預覽:
public void quadTo (float x1, float y1, float x2, float y2)
複製程式碼
有什麼用:
畫出二階貝塞爾曲線
怎麼用:
因為二階貝塞爾曲線需要三個點才能確定,所以quadTo方法中的四個引數分別是確定第二,第三的點的。第一個點就是path上次操作的點。 現在用一個例項來練習下這個方法:
水波紋效果:
效果圖:
![水波紋效果](https://i.iter01.com/images/25eafd88223c87d71a1662d4edb1a49e4e424eeeddefd30dcd582287962aa3bf.gif)
實現思路:
- 畫出至少兩段的波紋
- 使用 ValueAnimator 不斷獲取平移的值 offset
- 利用 offset 不斷的改變波紋的位置
現在分步驟來說明:
1. 畫出至少兩段波紋
我們首先要畫出兩段波紋。一段波紋就包含兩條曲線。每條曲線我們可以使用 quadTo() 方法來畫。
為了更容易理解,請看下圖:
![水波紋座標圖](https://i.iter01.com/images/89c6dcbde1bc71cf979dfc6fa46ef2083244943f539c24611c0157b438fbc869.png)
mWL 是一段波紋的長度,mCenterY 是螢幕高度的一半。
- 畫第一段波紋的第一條曲線:
mPath.moveTo(-mWL, mCenterY); //將path操作的起點移動到(-mWL,mCenterY)
mPath.quadTo((-mWL * 3 / 4) , mCenterY + 60, (-mWL / 2), mCenterY); //畫出第一段波紋的第一條曲線
複製程式碼
- 畫出第一段波紋的第二條曲線:
mPath.quadTo((-mWL / 4) , mCenterY - 60, 0, mCenterY); //畫出第一段波紋的第二條曲線
複製程式碼
- 畫出第二段波紋的第一條曲線:
mPath.quadTo((mWL /4) , mCenterY + 60, (mWL / 2), mCenterY); //畫出第二段波紋的第一條曲線
複製程式碼
- 畫出第二段波紋的第二條曲線:
mPath.quadTo((mWL * 3/ 4) , mCenterY - 60, mWL, mCenterY); //畫出第二段波紋的第二條曲線
複製程式碼
2. 使用 ValueAnimator 不斷獲取平移的值 offset
那麼現在來想一下應該怎麼讓這幾段波紋動起來呢?我們需要一個 offset 的平移值。這個值應該加在每個點的x座標上,並且 offset 是不斷變化的,這樣才會形成一個向右平移的效果。那怎麼才能獲取到這個變化的offset的值呢?答案就是要使用 ValueAnimator 。用法如下:
ValueAnimator animator = ValueAnimator.ofInt(0, mWL); //mWL是一段波紋的長度
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setInterpolator(new LinearInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
offset = (Integer) animation.getAnimatedValue(); //offset 的值的範圍在[0,mWL]之間。
postInvalidate();
}
});
animator.start();
}
複製程式碼
這樣只要動畫開始,offset 就會不斷從 0~mWL 變化。
3. 利用offset不斷的改變波紋的位置
現在為曲線的所有 X 座標都加上 offset 值。這樣就會產生平移的效果,為了簡化程式碼,這裡使用的 for 迴圈來畫曲線。
mPath.moveTo(-mWL + mOffset, mCenterY);
for (int i = 0; i < 2; i++) {
mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
}
複製程式碼
接下來為了適配各種螢幕,需要根據手機的寬度來計算出所需要的波紋的數目:
mWaveCount = (int) Math.round(mScreenWidth / mWL + 1.5); //這樣就保證波紋能覆蓋整個螢幕
複製程式碼
上面的 for 迴圈也可以改為:
mPath.moveTo(-mWL + mOffset, mCenterY);
for (int i = 0; i < mWaveCount; i++) {
mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
}
複製程式碼
三階貝塞爾曲線的應用:
彈性的圓:
效果圖:
![彈性的圓](https://i.iter01.com/images/f1642ae4695e9ccff6bce6e1c56f0a8d59d25ca4c4d7c7e86a638311723d89fe.gif)
實現思路:
- 用貝塞爾曲線畫出正圓
- 通過修改座標的大小來形成圓收縮的效果
1. 用貝塞爾曲線畫出正圓
我們首先要知道怎麼使用 cubicTo() 方法來畫個半徑為 r 的正圓。其實使用 cubicTo() 來畫正圓就需要 4 條三階貝塞爾曲線組合而成。如圖所示:
![三階貝塞爾曲線形成的圓](https://i.iter01.com/images/fe46d08c0718047865a7ae2a428fe83501e01ac34a9101cc11bee43c1e21ba95.png)
如果要畫 P0P3 那道曲線應該怎麼畫呢?我們就要知道 P0,P1,P2,P3 這四個點的座標。P0,P3 的座標我們已經知道了,分別是 (0,-r),(r,0)。那麼 P1 和 P2 的座標是什麼呢?其實這裡有個論證的過程,這個過程在這篇文章就有:Approximate a circle with cubic Bézier curves,感興趣的可以看看。這裡只說結果,最後得到一個數,這個數就是 c = 0.551915024494。也就是說 P1,P2 的座標就是 (cr,-r),(r,-cr)。其他點的座標也是用同樣的方法得出的,這裡就不細說了。
為了更方便管理這幾個點,我將這幾個點封裝分成兩個類。分別是 HorizontalLine 和 VerticalLine 。圓的上下兩條線屬於 HorizontalLine,圓的左右兩條線屬於 VerticalLine。
以下是這兩個類的程式碼:
private float c = 0.551915024494f;
class HorizontalLine {
public PointF left = new PointF(); //P7 P11
public PointF middle = new PointF(); //P0 P6
public PointF right = new PointF(); //P1 P5
public HorizontalLine(float x,float y) {
left.x = -radius*c;
left.y = y;
middle.x = x;
middle.y = y;
right.x = radius*c;
right.y = y;
}
public void setY(float y) {
left.y = y;
middle.y = y;
right.y = y;
}
}
class VerticalLine {
public PointF top = new PointF(); //P2 P10
public PointF middle = new PointF(); //P3 P9
public PointF bottom = new PointF(); //P4 P8
public VerticalLine(float x,float y) {
top.x = x;
top.y = -radius*c;
middle.x = x;
middle.y = y;
bottom.x = x;
bottom.y = radius*c;
}
public void setX(float x) {
top.x = x;
middle.x = x;
bottom.x = x;
}
}
複製程式碼
以下是用cubicTo()方法畫圓的程式碼:
private Paint mPaint;
private Path mPath;
private int mScreenHeight;//螢幕高度
private int mScreenWidth;//螢幕寬度
private float radius = 100;
private void initPaint() {
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#59c3e2"));
mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
private void initPoint() {
mTopLine = new HorizontalLine(0,-radius);
mBottomLine = new HorizontalLine(0,radius);
mLeftLine = new VerticalLine(-radius,0);
mRightLine = new VerticalLine(radius,0);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//將畫布移動到螢幕正中間
canvas.translate(mScreenWidth / 2, mScreenHeight / 2);
mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
mRightLine.middle.x,mRightLine.middle.y);
mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
mBottomLine.middle.x,mBottomLine.middle.y);
mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
mLeftLine.middle.x,mLeftLine.middle.y);
mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
mTopLine.middle.x,mTopLine.middle.y);
canvas.drawPath(mPath,mPaint);
}
複製程式碼
效果如下:
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/3def46cd8e436c268732d90055f7abffc7548e49e33ced51dbd62aae30db289c.png)
2.通過修改座標的大小來形成圓收縮的效果
想要達到圓收縮的效果只要增加和減少某些座標就可以了。比如我要達成如圖的這種效果,應該怎麼做呢?
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/f0ab7a9429a1577afab6994fd05b5cb5e6b412a73f50f33b56979961b8dab544.png)
只要增加 P2,P3,P4 的橫座標,就可以達到這種效果。
現在試試把圓收縮起來:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPath.reset();
//將畫布移動到手機螢幕的正中間
canvas.translate(mScreenWidth / 2, mScreenHeight / 2);
//將右邊的線的點的橫座標都增大
mRightLine.setX(radius * 1.5f);
mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
mRightLine.middle.x,mRightLine.middle.y);
mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
mBottomLine.middle.x,mBottomLine.middle.y);
mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
mLeftLine.middle.x,mLeftLine.middle.y);
mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
mTopLine.middle.x,mTopLine.middle.y);
canvas.drawPath(mPath,mPaint);
}
複製程式碼
效果如下:
![Path從懵逼到精通(2)——貝塞爾曲線](https://i.iter01.com/images/43d00942bc65d0741e3387f4700684966766c9ab250d6eb1dde778ea24e3bb9c.png)
以此類推,如果要達到剛剛那個動圖的效果,就要減少上下兩條線的點的縱座標,然後不斷平移畫布就可以了。具體程式碼可以下載原始碼來看。
原始碼下載:
參考資料: