一.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不保留內部資料結構,但會保留FillType. rewind會保留內部的資料結構,但不保留FillType |
矩陣操作 | transform | 矩陣變換 |
二.Path詳解
上一次除了一些常用函式之外,講解的基本上都是直線,本次需要了解其中的曲線部分,說到曲線,就不得不提大名鼎鼎的貝塞爾曲線。它的發明者是下面這個人(法國數學家PierreBézier)。
貝塞爾曲線能幹什麼?
貝塞爾曲線的運用是十分廣泛的,可以說貝塞爾曲線奠定了計算機繪圖的基礎(因為它可以將任何複雜的圖形用精確的數學語言進行描述),在你不經意間就已經使用過它了。
你會使用Photoshop的話,你可能會注意到裡面有一個鋼筆工具,這個鋼筆工具核心就是貝塞爾曲線。
你說你不會PS? 沒關係,你如果看過前面的文章或者用過2D繪圖,肯定繪製過圓,圓弧,圓角矩形等這些東西。這裡面的圓弧部分全部都是貝塞爾曲線的運用。
貝塞爾曲線作用十分廣泛,簡單舉幾個的栗子:
- QQ小紅點拖拽效果
- 一些炫酷的下拉重新整理控制元件
- 閱讀軟體的翻書效果
- 一些平滑的折線圖的製作
- 很多炫酷的動畫效果
如何輕鬆入門貝塞爾曲線?
雖然貝塞爾曲線用途非常廣泛,然而目前貌似並沒有適合的中文教程,能夠搜尋出來Android關於貝塞爾曲線的中文文章基本可以分為以下幾種:
- 科普型(只是讓人瞭解貝塞爾,並沒有實質性的內容)
- 裝逼型(擺出來一大堆公式,引用一堆英文原文)
- 基礎型(僅僅是講解貝塞爾曲線的兩個函式用法)
- 實戰型(根據例項講解其中貝塞爾曲線的運用)
以上幾種型別中比較有用的就是基礎型和實戰型,但兩者各有不足,本文會綜合兩者內容,從零開始學習貝塞爾曲線。
第一步.理解貝塞爾曲線的原理
此處理解貝塞爾曲線並非是學會公式的推導過程(不是推倒(ノ*・ω・)ノ),而是要了解貝塞爾曲線是如何生成的。 貝塞爾曲線是用一系列點來控制曲線狀態的,我將這些點簡單分為兩類:
型別 | 作用 |
---|---|
資料點 | 確定曲線的起始和結束位置 |
控制點 | 確定曲線的彎曲程度 |
此處暫時僅作了解概念,接下來就會講解其中詳細的含義。
一階曲線原理:
一階曲線是沒有控制點的,僅有兩個資料點(A 和 B),最終效果一個線段。
上圖表示的是一階曲線生成過程中的某一個階段,動態過程可以參照下圖(本文中貝塞爾曲線相關的動態演示圖片來自維基百科)。
PS:一階曲線其實就是前面講解過的lineTo。
二階曲線原理:
二階曲線由兩個資料點(A 和 C),一個控制點(B)來描述曲線狀態,大致如下:
上圖中紅色曲線部分就是傳說中的二階貝塞爾曲線,那麼這條紅色曲線是如何生成的呢?接下來我們就以其中的一個狀態分析一下:
連線AB BC,並在AB上取點D,BC上取點E,使其滿足條件:
這樣獲取到的點F就是貝塞爾曲線上的一個點,動態過程如下:
PS: 二階曲線對應的方法是quadTo
三階曲線原理:
三階曲線由兩個資料點(A 和 D),兩個控制點(B 和 C)來描述曲線狀態,如下:
三階曲線計算過程與二階類似,具體可以見下圖動態效果:
PS: 三階曲線對應的方法是cubicTo
貝塞爾曲線速查表
強烈推薦點選這裡練習貝塞爾曲線,可以加深對貝塞爾曲線的理解程度。
第二步.瞭解貝塞爾曲線相關函式使用方法
一階曲線:
一階曲線是一條線段,非常簡單,可以參見上一篇文章Path之基本操作,此處就不詳細講解了。
二階曲線:
通過上面對二階曲線的簡單瞭解,我們知道二階曲線是由兩個資料點,一個控制點構成,接下來我們就用一個例項來演示二階曲線是如何運用的。
首先,兩個資料點是控制貝塞爾曲線開始和結束的位置,比較容易理解,而控制點則是控制貝塞爾的彎曲狀態,相對來說比較難以理解,所以本示例重點在於理解貝塞爾曲線彎曲狀態與控制點的關係,廢話不多說,先上效果圖:
為了更加容易看出控制點與曲線彎曲程度的關係,上圖中繪製出了輔助點和輔助線,從上面的動態圖可以看出,貝塞爾曲線在動態變化過程中有類似於橡皮筋一樣的彈性效果,因此在製作一些彈性效果的時候很常用。
主要程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
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); } } |
三階曲線:
三階曲線由兩個資料點和兩個控制點來控制曲線狀態。
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
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向量圖的話,已經有相關的解析工具了(內部依舊運用的有貝塞爾曲線),不需要手動計算。
貝塞爾曲線的主要優點是可以實時控制曲線狀態,並可以通過改變控制點的狀態實時讓曲線進行平滑的狀態變化。
接下來我們就用一個簡單的示例讓一個圓漸變成為心形:
效果圖:
思路分析:
我們最終的需要的效果是將一個圓轉變成一個心形,通過分析可知,圓可以由四段三階貝塞爾曲線組合而成,如下:
心形也可以由四段的三階的貝塞爾曲線組成,如下:
兩者的差別僅僅在於資料點和控制點位置不同,因此只需要調整資料點和控制點的位置,就能將圓形變為心形。
核心難點:
1.如何得到資料點和控制點的位置?
關於使用繪製圓形的資料點與控制點早就已經有人詳細的計算好了,可以參考stackoverflow的一個回答How to create circle with Bézier curves?其中的資料只需要拿來用即可。
而對於心形的資料點和控制點,可以由圓形的部分資料點和控制點平移後得到,具體引數可以自己慢慢調整到一個滿意的效果。
2.如何達到漸變效果?
漸變其實就是每次對資料點和控制點稍微移動一點,然後重繪介面,在短時間多次的調整資料點與控制點,使其逐漸接近目標值,通過不斷的重繪介面達到一種漸變的效果。過程可以參照下圖動態效果:
程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
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相關內容,教給大家一些更好玩的東西。
解鎖新的境界之【繪製一個彈性的圓】:
(,,• ₃ •,,)
PS: 由於本人水平有限,某些地方可能存在誤解或不準確,如果你對此有疑問可以提交Issues進行反饋。
參考資料
Path
Canvas
貝塞爾曲線掃盲
貝塞爾曲線-維基百科
How to create circle with Bézier curves?
三次貝塞爾曲線練習之彈性的圓
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式