Path之貝塞爾曲線
作者微網誌: @GcsSloop
【本系列相關文章】
在上一篇文章Path之基本圖形中我們了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-貝塞爾曲線。
一.Path常用方法表
為了相容性(偷懶) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法。忍不住吐槽一下,為啥看起來有些順手就能寫的重載方法要等到API21才添加上啊。寶寶此刻内心也是崩潰的。
作用 | 相關方法 | 備注 |
---|---|---|
移動起點 | moveTo | 移動下一次操作的起點位置 |
設定終點 | setLastPoint | 重置目前path中最後一個點位置,如果在繪制之前調用,效果和moveTo相同 |
連接配接直線 | lineTo | 添加上一個點到目前點之間的直線到Path |
閉合路徑 | close | 連接配接第一個點連接配接到最後一個點,形成一個閉合區域 |
添加内容 | addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo | 添加(矩形, 圓角矩形, 橢圓, 圓, 路徑, 圓弧) 到目前Path (注意addArc和arcTo的差別) |
是否為空 | isEmpty | 判斷Path是否為空 |
是否為矩形 | isRect | 判斷path是否是一個矩形 |
替換路徑 | set | 用新的路徑替換到目前路徑所有内容 |
偏移路徑 | offset | 對目前路徑之前的操作進行偏移(不會影響之後的操作) |
貝塞爾曲線 | quadTo, cubicTo | 分别為二次和三次貝塞爾曲線的方法 |
rXxx方法 | rMoveTo, rLineTo, rQuadTo, rCubicTo | 不帶r的方法是基于原點的坐标系(偏移量),rXxx方法是基于目前點坐标系(偏移量) |
填充模式 | setFillType, getFillType, isInverseFillType, toggleInverseFillType | 設定,擷取,判斷和切換填充模式 |
提示方法 | incReserve | 提示Path還有多少個點等待加入(這個方法貌似會讓Path優化存儲結構) |
布爾操作(API19) | op | 對兩個Path進行布爾運算(即取交集、并集等操作) |
計算邊界 | computeBounds | 計算Path的邊界 |
重置路徑 | reset, rewind | 清除Path中的内容(reset相當于重置到new Path階段,rewind會保留Path的資料結構) |
矩陣操作 | transform | 矩陣變換 |
二.Path詳解
上一次除了一些常用函數之外,講解的基本上都是直線,本次需要了解其中的曲線部分,說到曲線,就不得不提大名鼎鼎的貝塞爾曲線。它的發明者是下面這個人(法國數學家PierreBézier)。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYWan5yb5NDa3Aza1AzMnBHOycnY1k3axYWM3pmMpRGdYVDMw8CXldmchx2Lc52YucWbpFmbpNnL0c3dvw1LcpDc0RHaiojIsJye.gif)
貝塞爾曲線能幹什麼?
貝塞爾曲線的運用是十分廣泛的,可以說貝塞爾曲線奠定了計算機繪圖的基礎(因為它可以将任何複雜的圖形用精确的數學語言進行描述),在你不經意間就已經使用過它了。
你會使用Photoshop的話,你可能會注意到裡面有一個鋼筆工具,這個鋼筆工具核心就是貝塞爾曲線。
你說你不會PS? 沒關系,你如果看過前面的文章或者用過2D繪圖,肯定繪制過圓,圓弧,圓角矩形等這些東西。這裡面的圓弧部分全部都是貝塞爾曲線的運用。
貝塞爾曲線作用十分廣泛,簡單舉幾個的栗子:
- QQ小紅點拖拽效果
- 一些炫酷的下拉重新整理控件
- 閱讀軟體的翻書效果
- 一些平滑的折線圖的制作
- 很多炫酷的動畫效果
如何輕松入門貝塞爾曲線?
雖然貝塞爾曲線用途非常廣泛,然而目前貌似并沒有适合的中文教程,能夠搜尋出來Android關于貝塞爾曲線的中文文章基本可以分為以下幾種:
* 科普型(隻是讓人了解貝塞爾,并沒有實質性的内容)
* 裝逼型(擺出來一大堆公式,引用一堆英文原文)
* 基礎型(僅僅是講解貝塞爾曲線的兩個函數用法)
* 實戰型(根據執行個體講解其中貝塞爾曲線的運用)
以上幾種類型中比較有用的就是基礎型和實戰型,但兩者各有不足,本文會綜合兩者内容,從零開始學習貝塞爾曲線。
第一步.了解貝塞爾曲線的原理
此處了解貝塞爾曲線并非是學會公式的推倒過程,而是要了解貝塞爾曲線是如何生成的。貝塞爾曲線是用一系列點來控制曲線狀态的,我将這些點簡單分為兩類:
類型 | 作用 |
---|---|
資料點 | 确定曲線的起始和結束位置 |
控制點 | 确定曲線的彎曲程度 |
此處暫時僅作了解概念,接下來就會講解其中詳細的含義。
一階曲線原理:
一階曲線是沒有控制點的,僅有兩個資料點(A 和 B),最終效果一個線段。
上圖表示的是一階曲線生成過程中的某一個階段,動态過程可以參照下圖。
PS:一階曲線其實就是前面講解過的lineTo。
二階曲線原理:
二階曲線由兩個資料點(A 和 C),一個控制點(B)來描述曲線狀态,大緻如下:
上圖中紅色曲線部分就是傳說中的二階貝塞爾曲線,那麼這條紅色曲線是如何生成的呢?接下來我們就以其中的一個狀态分析一下:
連接配接AB BC,并在AB上取點D,BC上取點E,使其滿足條件:
連接配接DE,取點F,使得:
這樣擷取到的點F就是貝塞爾曲線上的一個點,動态過程如下:
PS: 二階曲線對應的方法是quadTo
三階曲線原理:
三階曲線由兩個資料點(A 和 D),兩個控制點(B 和 C)來描述曲線狀态,如下:
三階曲線計算過程與二階類似,具體可以見下圖動态效果:
PS: 三階曲線對應的方法是cubicTo
貝塞爾曲線速查表
強烈推薦點選這裡練習貝塞爾曲線,可以加深對貝塞爾曲線的了解程度。
第二步.了解貝塞爾曲線相關函數使用方法
一階曲線:
一階曲線是一條線段,非常簡單,可以參見上一篇文章Path之基本操作,此處就不詳細講解了。
二階曲線:
通過上面對二階曲線的簡單了解,我們知道二階曲線是由兩個資料點,一個控制點構成,接下來我們就用一個執行個體來示範二階曲線是如何運用的。
首先,兩個資料點是控制貝塞爾曲線開始和結束的位置,比較容易了解,而控制點則是控制貝塞爾的彎曲狀态,相對來說比較難以了解,是以本示例重點在于了解貝塞爾曲線彎曲狀态與控制點的關系,廢話不多說,先上效果圖:
為了更加容易看出控制點與曲線彎曲程度的關系,上圖中繪制出了輔助點和輔助線,從上面的動态圖可以看出,貝塞爾曲線在動态變化過程中有類似于橡皮筋一樣的彈性效果,是以在制作一些彈性效果的時候很常用。
主要代碼如下:
public class Bezier extends View {
private Paint mPaint;
private int centerX, centerY;
private PointF start, end, control;
public Bessel1(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize();
start = new PointF(,);
end = new PointF(,);
control = new PointF(,);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w/;
centerY = h/;
// 初始化資料點和控制點的位置
start.x = centerX-;
start.y = centerY;
end.x = centerX+;
end.y = centerY;
control.x = centerX;
control.y = centerY-;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 根據觸摸位置更新控制點,并提示重繪
control.x = event.getX();
control.y = event.getY();
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制資料點和控制點
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth();
canvas.drawPoint(start.x,start.y,mPaint);
canvas.drawPoint(end.x,end.y,mPaint);
canvas.drawPoint(control.x,control.y,mPaint);
// 繪制輔助線
mPaint.setStrokeWidth();
canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);
// 繪制貝塞爾曲線
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth();
Path path = new Path();
path.moveTo(start.x,start.y);
path.quadTo(control.x,control.y,end.x,end.y);
canvas.drawPath(path, mPaint);
}
}
三階曲線:
三階曲線由兩個資料點和兩個控制點來控制曲線狀态。
代碼:
public class Bezier2 extends View {
private Paint mPaint;
private int centerX, centerY;
private PointF start, end, control1, control2;
private boolean mode = true;
public Bezier2(Context context) {
this(context, null);
}
public Bezier2(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize();
start = new PointF(, );
end = new PointF(, );
control1 = new PointF(, );
control2 = new PointF(, );
}
public void setMode(boolean mode) {
this.mode = mode;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / ;
centerY = h / ;
// 初始化資料點和控制點的位置
start.x = centerX - ;
start.y = centerY;
end.x = centerX + ;
end.y = centerY;
control1.x = centerX;
control1.y = centerY - ;
control2.x = centerX;
control2.y = centerY - ;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 根據觸摸位置更新控制點,并提示重繪
if (mode) {
control1.x = event.getX();
control1.y = event.getY();
} else {
control2.x = event.getX();
control2.y = event.getY();
}
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//drawCoordinateSystem(canvas);
// 繪制資料點和控制點
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth();
canvas.drawPoint(start.x, start.y, mPaint);
canvas.drawPoint(end.x, end.y, mPaint);
canvas.drawPoint(control1.x, control1.y, mPaint);
canvas.drawPoint(control2.x, control2.y, mPaint);
// 繪制輔助線
mPaint.setStrokeWidth();
canvas.drawLine(start.x, start.y, control1.x, control1.y, mPaint);
canvas.drawLine(control1.x, control1.y,control2.x, control2.y, mPaint);
canvas.drawLine(control2.x, control2.y,end.x, end.y, mPaint);
// 繪制貝塞爾曲線
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth();
Path path = new Path();
path.moveTo(start.x, start.y);
path.cubicTo(control1.x, control1.y, control2.x,control2.y, end.x, end.y);
canvas.drawPath(path, mPaint);
}
}
三階曲線相比于二階曲線可以制作更加複雜的形狀,但是對于高階的曲線,用低階的曲線組合也可達到相同的效果,就是傳說中的降階。是以我們對貝塞爾曲線的封裝方法一般最高隻到三階曲線。
降階與升階
類型 | 釋義 | 變化 |
---|---|---|
降階 | 在保持曲線形狀與方向不變的情況下,減少控制點數量,即降低曲線階數 | 方法變得簡單,資料點變多,控制點可能減少,靈活性變弱 |
升階 | 在保持曲線形狀與方向不變的情況下,增加控制點數量,即升高曲線階數 | 方法更加複雜,資料點不變,控制點增加,靈活性變強 |
第三步.貝塞爾曲線使用執行個體
在制作這個執行個體之前,首先要明确一個内容,就是在什麼情況下需要使用貝塞爾曲線?
需要繪制不規則圖形時? 當然不是!目前來說,我覺得使用貝塞爾曲線主要有以下幾個方面(僅個人拙見,可能存在錯誤,歡迎指正)
序号 | 内容 | 用例 |
---|---|---|
1 | 事先不知道曲線狀态,需要實時計算時 | 天氣預報氣溫變化的平滑折線圖 |
2 | 顯示狀态會根據使用者操作改變時 | QQ小紅點,仿真翻書效果 |
3 | 一些比較複雜的運動狀态(配合PathMeasure使用) | 複雜運動狀态的動畫效果 |
至于隻需要一個靜态的曲線圖形的情況,用圖檔豈不是更好,大量的計算會很不劃算。
如果是顯示SVG矢量圖的話,已經有相關的解析工具了(内部依舊運用的有貝塞爾曲線),不需要手動計算。
貝塞爾曲線的主要優點是可以實時控制曲線狀态,并可以通過改變控制點的狀态實時讓曲線進行平滑的狀态變化。
接下來我們就用一個簡單的示例讓一個圓漸變成為心形:
效果圖:
思路分析:
我們最終的需要的效果是将一個圓轉變成一個心形,通過分析可知,圓可以由四段三階貝塞爾曲線組合而成,如下:
心形也可以由四段的三階的貝塞爾曲線組成,如下:
兩者的差别僅僅在于資料點和控制點位置不同,是以隻需要調整資料點和控制點的位置,就能将圓形變為心形。
核心難點:
1.如何得到資料點和控制點的位置?
關于使用繪制圓形的資料點與控制點早就已經有人詳細的計算好了,可以參考stackoverflow的一個回答How to create circle with Bézier curves?其中的資料隻需要拿來用即可。
而對于心形的資料點和控制點,可以由圓形的部分資料點和控制點平移後得到,具體參數可以自己慢慢調整到一個滿意的效果。
2.如何達到漸變效果?
漸變其實就是每次對資料點和控制點稍微移動一點,然後重繪界面,在短時間多次的調整資料點與控制點,使其逐漸接近目标值,通過不斷的重繪界面達到一種漸變的效果。過程可以參照下圖動态效果:
代碼:
public class Bezier3 extends View {
private static final float C = f; // 一個常量,用來計算繪制圓形貝塞爾曲線控制點的位置
private Paint mPaint;
private int mCenterX, mCenterY;
private PointF mCenter = new PointF(,);
private float mCircleRadius = ; // 圓的半徑
private float mDifference = mCircleRadius*C; // 圓形的控制點與資料點的內插補點
private float[] mData = new float[]; // 順時針記錄繪制圓形的四個資料點
private float[] mCtrl = new float[]; // 順時針記錄繪制圓形的八個控制點
private float mDuration = ; // 變化總時長
private float mCurrent = ; // 目前已進行時長
private float mCount = ; // 将時長總共劃分多少份
private float mPiece = mDuration/mCount; // 每一份的時長
public Bezier3(Context context) {
this(context, null);
}
public Bezier3(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize();
// 初始化資料點
mData[] = ;
mData[] = mCircleRadius;
mData[] = mCircleRadius;
mData[] = ;
mData[] = ;
mData[] = -mCircleRadius;
mData[] = -mCircleRadius;
mData[] = ;
// 初始化控制點
mCtrl[] = mData[]+mDifference;
mCtrl[] = mData[];
mCtrl[] = mData[];
mCtrl[] = mData[]+mDifference;
mCtrl[] = mData[];
mCtrl[] = mData[]-mDifference;
mCtrl[] = mData[]+mDifference;
mCtrl[] = mData[];
mCtrl[] = mData[]-mDifference;
mCtrl[] = mData[];
mCtrl[] = mData[];
mCtrl[] = mData[]-mDifference;
mCtrl[] = mData[];
mCtrl[] = mData[]+mDifference;
mCtrl[] = mData[]-mDifference;
mCtrl[] = mData[];
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / ;
mCenterY = h / ;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCoordinateSystem(canvas); // 繪制坐标系
canvas.translate(mCenterX, mCenterY); // 将坐标系移動到畫布中央
canvas.scale(,-); // 翻轉Y軸
drawAuxiliaryLine(canvas);
// 繪制貝塞爾曲線
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth();
Path path = new Path();
path.moveTo(mData[],mData[]);
path.cubicTo(mCtrl[], mCtrl[], mCtrl[], mCtrl[], mData[], mData[]);
path.cubicTo(mCtrl[], mCtrl[], mCtrl[], mCtrl[], mData[], mData[]);
path.cubicTo(mCtrl[], mCtrl[], mCtrl[], mCtrl[], mData[], mData[]);
path.cubicTo(mCtrl[], mCtrl[], mCtrl[], mCtrl[], mData[], mData[]);
canvas.drawPath(path, mPaint);
mCurrent += mPiece;
if (mCurrent < mDuration){
mData[] -= /mCount;
mCtrl[] += /mCount;
mCtrl[] += /mCount;
mCtrl[] -= /mCount;
mCtrl[] += /mCount;
// postInvalidateDelayed((long) mPiece);
}
}
// 繪制輔助線
private void drawAuxiliaryLine(Canvas canvas) {
// 繪制資料點和控制點
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth();
for (int i=; i<; i+=){
canvas.drawPoint(mData[i],mData[i+], mPaint);
}
for (int i=; i<; i+=){
canvas.drawPoint(mCtrl[i], mCtrl[i+], mPaint);
}
// 繪制輔助線
mPaint.setStrokeWidth();
for (int i=, j=; i<; i+=, j+=){
canvas.drawLine(mData[i],mData[i+],mCtrl[j],mCtrl[j+],mPaint);
canvas.drawLine(mData[i],mData[i+],mCtrl[j+],mCtrl[j+],mPaint);
}
canvas.drawLine(mData[],mData[],mCtrl[],mCtrl[],mPaint);
canvas.drawLine(mData[],mData[],mCtrl[],mCtrl[],mPaint);
}
// 繪制坐标系
private void drawCoordinateSystem(Canvas canvas) {
canvas.save(); // 繪制做坐标系
canvas.translate(mCenterX, mCenterY); // 将坐标系移動到畫布中央
canvas.scale(,-); // 翻轉Y軸
Paint fuzhuPaint = new Paint();
fuzhuPaint.setColor(Color.RED);
fuzhuPaint.setStrokeWidth();
fuzhuPaint.setStyle(Paint.Style.STROKE);
canvas.drawLine(, -, , , fuzhuPaint);
canvas.drawLine(-, , , , fuzhuPaint);
canvas.restore();
}
}
三.總結
其實關于貝塞爾曲線最重要的是核心了解貝塞爾曲線的生成方式,隻有了解了貝塞爾曲線的生成方式,才能更好的運用貝塞爾曲線。在上一篇末尾說本篇要涉及一點自相交圖形渲染問題,不幸的是,本篇沒有了,請期待下一篇(可能會在下一篇中出現o( ̄︶ ̄)o),下一篇依舊Path相關内容,教給大家一些更好玩的東西。
解鎖新的境界之【繪制一個彈性的圓】:
(,,• ₃ •,,)
PS: 由于本人水準有限,某些地方可能存在誤解或不準确,如果你對此有疑問可以送出Issues進行回報。
About Me
作者微網誌: @GcsSloop
參考資料
Path
Canvas
貝塞爾曲線掃盲
貝塞爾曲線-維基百科
How to create circle with Bézier curves?
三次貝塞爾曲線練習之彈性的圓