爲了相容性(偷懶) 本表格中去除了在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不保留內部數據結構,但會保留FillType. rewind會保留內部的數據結構,但不保留FillType |
矩陣操作 | transform | 矩陣變換 |
上一次除了一些常用函數之外,講解的基本上都是直線,本次需要瞭解其中的曲線部分,說到曲線,就不得不提大名鼎鼎的貝塞爾曲線。它的發明者是下面 下麪這個人(法國數學家PierreBézier)。
貝塞爾曲線的運用是十分廣泛的,可以說貝塞爾曲線奠定了計算機繪圖的基礎(因爲它可以將任何複雜的圖形用精確的數學語言進行描述),在你不經意間就已經使用過它了。
你會使用Photoshop的話,你可能會注意到裏面有一個鋼筆工具,這個鋼筆工具核心就是貝塞爾曲線。
你說你不會PS? 沒關係,你如果看過前面的文章或者用過2D繪圖,肯定繪製過圓,圓弧,圓角矩形等這些東西。這裏面的圓弧部分全部都是貝塞爾曲線的運用。
雖然貝塞爾曲線用途非常廣泛,然而目前貌似並沒有適合的中文教學,能夠搜尋出來Android關於貝塞爾曲線的中文文章基本可以分爲以下幾種:
以上幾種型別中比較有用的就是基礎型和實戰型,但兩者各有不足,本文會綜合兩者內容,從零開始學習貝塞爾曲線。
此處理解貝塞爾曲線並非是學會公式的推導過程(推倒(ノ*・ω・)ノ),而是要瞭解貝塞爾曲線是如何生成的。
貝塞爾曲線是用一系列點來控制曲線狀態的,我將這些點簡單分爲兩類:
型別 | 作用 |
---|---|
數據點 | 確定曲線的起始和結束位置 |
控制點 | 確定曲線的彎曲程度 |
此處暫時僅作瞭解概念,接下來就會講解其中詳細的含義。
一階曲線原理:
一階曲線是沒有控制點的,僅有兩個數據點(A 和 B),最終效果一個線段。
上圖表示的是一階曲線生成過程中的某一個階段,動態過程可以參照下圖(本文中貝塞爾曲線相關的動態演示圖片來自維基百科)。
PS:一階曲線其實就是前面講解過的lineTo。
二階曲線原理:
二階曲線由兩個數據點(A 和 C),一個控制點(B)來描述曲線狀態,大致如下:
上圖中紅色曲線部分就是傳說中的二階貝塞爾曲線,那麼這條紅色曲線是如何生成的呢?接下來我們就以其中的一個狀態分析一下:
連線AB BC,並在AB上取點D,BC上取點E,使其滿足條件:[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-hB2IlzWx-1597155272636)(http://chart.googleapis.com/chart?cht=tx&chl=%5Cfrac%7BAD%7D%7BAB%7D%20%3D%20%5Cfrac%7BBE%7D%7BBC%7D)]
連線DE,取點F,使得:[外連圖片轉存失敗,源站可能有防盜鏈機制 機製,建議將圖片儲存下來直接上傳(img-FZii3eZf-1597155272663)(http://chart.googleapis.com/chart?cht=tx&chl=%5Cfrac%7BAD%7D%7BAB%7D%20%3D%20%5Cfrac%7BBE%7D%7BBC%7D%20%3D%20%5Cfrac%7BDF%7D%7BDE%7D)]
這樣獲取到的點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(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(60);
start = new PointF(0,0);
end = new PointF(0,0);
control = new PointF(0,0);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w/2;
centerY = h/2;
// 初始化數據點和控制點的位置
start.x = centerX-200;
start.y = centerY;
end.x = centerX+200;
end.y = centerY;
control.x = centerX;
control.y = centerY-100;
}
@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(20);
canvas.drawPoint(start.x,start.y,mPaint);
canvas.drawPoint(end.x,end.y,mPaint);
canvas.drawPoint(control.x,control.y,mPaint);
// 繪製輔助線
mPaint.setStrokeWidth(4);
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(8);
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(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(60);
start = new PointF(0, 0);
end = new PointF(0, 0);
control1 = new PointF(0, 0);
control2 = new PointF(0, 0);
}
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 / 2;
centerY = h / 2;
// 初始化數據點和控制點的位置
start.x = centerX - 200;
start.y = centerY;
end.x = centerX + 200;
end.y = centerY;
control1.x = centerX;
control1.y = centerY - 100;
control2.x = centerX;
control2.y = centerY - 100;
}
@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(20);
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(4);
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(8);
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向量圖的話,已經有相關的解析工具了(內部依舊運用的有貝塞爾曲線),不需要手動計算。
貝塞爾曲線的主要優點是可以實時控制曲線狀態,並可以通過改變控制點的狀態實時讓曲線進行平滑的狀態變化。
我們最終的需要的效果是將一個圓轉變成一個心形,通過分析可知,圓可以由四段三階貝塞爾曲線組合而成,如下:
心形也可以由四段的三階的貝塞爾曲線組成,如下:
兩者的差別僅僅在於數據點和控制點位置不同,因此只需要調整數據點和控制點的位置,就能將圓形變爲心形。
關於使用繪製圓形的數據點與控制點早就已經有人詳細的計算好了,可以參考stackoverflow的一個回答How to create circle with Bézier curves?其中的數據只需要拿來用即可。
而對於心形的數據點和控制點,可以由圓形的部分數據點和控制點平移後得到,具體參數可以自己慢慢調整到一個滿意的效果。
漸變其實就是每次對數據點和控制點稍微移動一點,然後重繪介面,在短時間多次的調整數據點與控制點,使其逐漸接近目標值,通過不斷的重繪介面達到一種漸變的效果。過程可以參照下圖動態效果:
public class Bezier3 extends View {
private static final float C = 0.551915024494f; // 一個常數,用來計算繪製圓形貝塞爾曲線控制點的位置
private Paint mPaint;
private int mCenterX, mCenterY;
private PointF mCenter = new PointF(0,0);
private float mCircleRadius = 200; // 圓的半徑
private float mDifference = mCircleRadius*C; // 圓形的控制點與數據點的差值
private float[] mData = new float[8]; // 順時針記錄繪製圓形的四個數據點
private float[] mCtrl = new float[16]; // 順時針記錄繪製圓形的八個控制點
private float mDuration = 1000; // 變化總時長
private float mCurrent = 0; // 當前已進行時長
private float mCount = 100; // 將時長總共劃分多少份
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(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setTextSize(60);
// 初始化數據點
mData[0] = 0;
mData[1] = mCircleRadius;
mData[2] = mCircleRadius;
mData[3] = 0;
mData[4] = 0;
mData[5] = -mCircleRadius;
mData[6] = -mCircleRadius;
mData[7] = 0;
// 初始化控制點
mCtrl[0] = mData[0]+mDifference;
mCtrl[1] = mData[1];
mCtrl[2] = mData[2];
mCtrl[3] = mData[3]+mDifference;
mCtrl[4] = mData[2];
mCtrl[5] = mData[3]-mDifference;
mCtrl[6] = mData[4]+mDifference;
mCtrl[7] = mData[5];
mCtrl[8] = mData[4]-mDifference;
mCtrl[9] = mData[5];
mCtrl[10] = mData[6];
mCtrl[11] = mData[7]-mDifference;
mCtrl[12] = mData[6];
mCtrl[13] = mData[7]+mDifference;
mCtrl[14] = mData[0]-mDifference;
mCtrl[15] = mData[1];
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mCenterX = w / 2;
mCenterY = h / 2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawCoordinateSystem(canvas); // 繪製座標系
canvas.translate(mCenterX, mCenterY); // 將座標系移動到畫布中央
canvas.scale(1,-1); // 翻轉Y軸
drawAuxiliaryLine(canvas);
// 繪製貝塞爾曲線
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
Path path = new Path();
path.moveTo(mData[0],mData[1]);
path.cubicTo(mCtrl[0], mCtrl[1], mCtrl[2], mCtrl[3], mData[2], mData[3]);
path.cubicTo(mCtrl[4], mCtrl[5], mCtrl[6], mCtrl[7], mData[4], mData[5]);
path.cubicTo(mCtrl[8], mCtrl[9], mCtrl[10], mCtrl[11], mData[6], mData[7]);
path.cubicTo(mCtrl[12], mCtrl[13], mCtrl[14], mCtrl[15], mData[0], mData[1]);
canvas.drawPath(path, mPaint);
mCurrent += mPiece;
if (mCurrent < mDuration){
mData[1] -= 120/mCount;
mCtrl[7] += 80/mCount;
mCtrl[9] += 80/mCount;
mCtrl[4] -= 20/mCount;
mCtrl[10] += 20/mCount;
postInvalidateDelayed((long) mPiece);
}
}
// 繪製輔助線
private void drawAuxiliaryLine(Canvas canvas) {
// 繪製數據點和控制點
mPaint.setColor(Color.GRAY);
mPaint.setStrokeWidth(20);
for (int i=0; i<8; i+=2){
canvas.drawPoint(mData[i],mData[i+1], mPaint);
}
for (int i=0; i<16; i+=2){
canvas.drawPoint(mCtrl[i], mCtrl[i+1], mPaint);
}
// 繪製輔助線
mPaint.setStrokeWidth(4);
for (int i=2, j=2; i<8; i+=2, j+=4){
canvas.drawLine(mData[i],mData[i+1],mCtrl[j],mCtrl[j+1],mPaint);
canvas.drawLine(mData[i],mData[i+1],mCtrl[j+2],mCtrl[j+3],mPaint);
}
canvas.drawLine(mData[0],mData[1],mCtrl[0],mCtrl[1],mPaint);
canvas.drawLine(mData[0],mData[1],mCtrl[14],mCtrl[15],mPaint);
}
// 繪製座標系
private void drawCoordinateSystem(Canvas canvas) {
canvas.save(); // 繪製做座標系
canvas.translate(mCenterX, mCenterY); // 將座標系移動到畫布中央
canvas.scale(1,-1); // 翻轉Y軸
Paint fuzhuPaint = new Paint();
fuzhuPaint.setColor(Color.RED);
fuzhuPaint.setStrokeWidth(5);
fuzhuPaint.setStyle(Paint.Style.STROKE);
canvas.drawLine(0, -2000, 0, 2000, fuzhuPaint);
canvas.drawLine(-2000, 0, 2000, 0, fuzhuPaint);
canvas.restore();
}
}
其實關於貝塞爾曲線最重要的是核心理解貝塞爾曲線的生成方式,只有理解了貝塞爾曲線的生成方式,才能 纔能更好的運用貝塞爾曲線。在上一篇末尾說本篇可能涉及一點圖形渲染問題,不幸的是,本篇沒有了,請期待下一篇(可能會在下一篇中出現o( ̄︶ ̄)o),下一篇依舊Path相關內容,教給大家一些更好玩的東西。
解鎖新的境界之【繪製一個彈性的圓】:
(,• ₃ •,)
粉絲技術交流扣裙