Android繪圖最終篇之大戰貝塞爾三次曲線
零、前言
1.可以說貝塞爾曲線是一把 "石中劍",能夠拔出它,會讓你的繪圖如虎添翼。
2.今天要與貝塞爾曲線大戰三百回合,將它加入我的繪圖大軍麾下。
3.自此Android繪圖五虎將: Canvas,Path,Paint,Color,貝塞爾
便集結完成。
4.本專案原始碼見文尾 捷文規範
第一條,檢視原始碼在 view包
,分析工具在 analyze包
一、貝塞爾三次曲線初體驗
1. 無網格,不曲線
,廢話不多說,上網格+座標系
/** * 作者:張風捷特烈<br/> * 時間:2018/11/16 0016:9:04<br/> * 郵箱:[email protected]<br/> * 說明:貝塞爾三次曲線初體驗 */ public class SimpleCubicView extends View { private Point mCoo = new Point(500, 500);//座標系 private Picture mCooPicture;//座標系canvas元件 private Picture mGridPicture;//網格canvas元件 private Paint mHelpPint;//輔助畫筆 private Paint mPaint;//主畫筆 private Path mPath;//主路徑 public SimpleCubicView(Context context) { this(context, null); } public SimpleCubicView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init();//初始化 } private void init() { //初始化主畫筆 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(Color.BLUE); mPaint.setStrokeWidth(5); //初始化主路徑 mPath = new Path(); //初始化輔助 mHelpPint = HelpDraw.getHelpPint(Color.RED); mCooPicture = HelpDraw.getCoo(getContext(), mCoo); mGridPicture = HelpDraw.getGrid(getContext()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCoo.x, mCoo.y); //TODO ----drawSomething canvas.restore(); HelpDraw.draw(canvas, mGridPicture, mCooPicture); } }
2.分析一段三次貝塞爾
一段三次貝塞爾曲線是由四個點控制的,四個點分別是幹嘛的,且看分析:
//準備成員變數---四個點 Point p0 = new Point(0, 0); Point p1 = new Point(200, 200); Point p2 = new Point(300, -100); Point p3 = new Point(500, 300); //onDraw中: mPath.moveTo(p0.x, p0.y); mPath.cubicTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); canvas.drawPath(mPath, mPaint);

結果1.png
也許這樣看不出什麼關係:現在把四個控制點也畫出來(紅色):
mHelpPint.setStrokeWidth(10); HelpDraw.drawPos(canvas, mHelpPint, p0, p1, p2, p3);

結果2.png
是不是有點意思了--在加兩條線:
mHelpPint.setStrokeWidth(2); HelpDraw.drawLines(canvas, mHelpPint, p0, p1, p2, p3);

結果3.png
小結: p0:第一點
, p3:最終點
, p1:控制點1
, p2:控制點2
二、動態效果:任意一段三次貝塞爾曲線的最優雅實現形式
以前看過別人的任意一段三次貝塞爾曲線,感覺體驗太差,切換個點還要點按鈕,
下面我實現四個點任意拖動的三次貝塞爾曲線,可謂是非常優雅的,讓你明白點域的判斷

三次貝塞爾測試.gif
1.判斷一個點是否在一個圓形區域
/** * 判斷出是否在某點的半徑為r圓範圍內 * * @param src 目標點 * @param dst 主動點 * @param r半徑 */ public static boolean judgeCircleArea(Point src, Point dst, float r) { return disPos2d(src.x, src.y, dst.x, dst.y) <= r; } /** * 兩點間距離函式 */ public static float disPos2d(float x1, float y1, float x2, float y2) { return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); }
2.觸控事件動態改變點位:
//新增成員變數 Point src = new Point(0, 0); @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: src.x = (int) event.getX() - mCoo.x; src.y = (int) event.getY() - mCoo.y; break; case MotionEvent.ACTION_MOVE: if (judgeCircleArea(src, p0, 30)) { setPos(event, p0); } if (judgeCircleArea(src, p1, 30)) { setPos(event, p1); } if (judgeCircleArea(src, p2, 30)) { setPos(event, p2); } if (judgeCircleArea(src, p3, 30)) { setPos(event, p3); } mPath.reset(); src.x = (int) event.getX() - mCoo.x; src.y = (int) event.getY() - mCoo.y; invalidate(); break; } return true; } /** * 設定點位 * @param event 事件 * @param p 點位 */ private void setPos(MotionEvent event, Point p) { p.x = (int) event.getX() - mCoo.x; p.y = (int) event.getY() - mCoo.y; }
好了,這樣就行了,是不是一種還沒開始就結束的感覺。
三、貝塞爾曲線實戰1:(初級:運動)
1.映象:
先選取感覺滿意的半邊,記錄四個點位:

左半.png
Point c1p0 = new Point(0, 0); Point c1p1 = new Point(300, 0); Point c1p2 = new Point(150, -200); Point c1p3 = new Point(300, -200);
2.如何實現下面的效果呢?

貝塞爾單段映象.gif
在原來的基礎上在畫一段貝塞爾曲線,要求:新控制點1(記為:c2p1)和c1p2關於c1p3.x對稱
點關於豎線對稱的原理: (c2p1.x+c1p2.x)/2 = c1p3.x
c2p1.y = c1p2.y
,轉換一下: c2p1.x=c1p3.x*2-c1p2.x
新控制點2(記為:c2p2)和c1p1關於對稱c1p3.x以及新結尾點(記為:c2p3)和c1p0關於c1p3.x對稱即可
private void reflectY( Point p0, Point p1, Point p2, Point p3, Path path) { path.cubicTo(p3.x * 2 - p2.x, p2.y, p3.x * 2 - p1.x, p1.y, p3.x * 2 - p0.x, p0.y); }
3.凸出來的一塊慢慢變平的動畫
想象一下,只需要才c1p2和c1p3一起向下移動就行了,要運動,二話不說,ValueAnimator走起
好吧,有點像做俯臥撐,實現起來也挺簡單的:

動態修改.gif
//數字時間流 mAnimator = ValueAnimator.ofFloat(1, 0); mAnimator.setDuration(2000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setRepeatCount(-1); mAnimator.addUpdateListener(a -> { float rate = (float) a.getAnimatedValue(); c1p2.y = -(int) (rate * 200); c1p3.y = -(int) (rate * 200); mPath.reset(); invalidate(); });
4.隨便玩玩
原始碼在文尾,檔案是 Lever1CubicView.java
,大家可以下載,執行自己玩玩,加深一下對貝塞爾三次曲線的感覺

隨便玩玩.gif
好了,開胃菜結束了,下面進入正餐,你沒看錯,好戲才剛剛開始。
四、高階:三階貝塞爾的優雅使用:
注意:
前方高能,非戰鬥人員請儘快準備瓜子,飲料,花生米...
1.三階貝塞爾畫圓:
看下圖,你可能會滿臉不屑地說:"切,我用canvas分分秒描畫你信不信?"
老大,我信...且往下看

圓.png
2.如何優雅地繪製多條貝塞爾曲線
下面是四條貝塞爾曲線繪製的圓,看圖就知道優勢在於任意改變形狀
但如果把點位都放在mPath.cubicTo()裡,多幾條線就亂成一鍋粥了,最好統一管理一下
第一個想到的是每條線的三個點都抽成三個成員變數,不過還是很難維護,這個問題一直困擾我
今天突然想到二維陣列不是挺好嗎?二維每個裡面兩個點。

圓分析.png
//單位圓(即半徑為1)控制線長 private static float rate = 0.551915024494f; /** * 單位圓(即半徑為1)的貝塞爾曲線點位 */ private static final float[][] CIRCLE_ARRAY = { //0---第一段線 {-1, rate},//控制點1 {1 - rate, 1},//控制點2 {1, 1},//終點 //1---第二段線 {1 + rate, 1},//控制點1 {2, rate},//控制點2 {2, 0},//終點 //2---第二段線 {2, -rate},//控制點1 {1 + rate, -1},//控制點2 {1, -1},//終點 //3---第四段線 {1 - rate, -1},//控制點1 {0, -rate},//控制點2 {0, 0}//終點 };
2.繪製迴圈一下就行了
看網上一些繪製方法,點都很亂,看著費勁也晦澀。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.save(); canvas.translate(mCoo.x, mCoo.y); mPaint.setStyle(Paint.Style.STROKE); mPath.lineTo(0, 0); for (int i = 0; i < CIRCLE_ARRAY.length / 3; i++) { mPath.cubicTo( r * CIRCLE_ARRAY[3*i][0], r * CIRCLE_ARRAY[3*i][1], r * CIRCLE_ARRAY[3*i + 1][0], r * CIRCLE_ARRAY[3*i + 1][1], r * CIRCLE_ARRAY[3*i + 2][0], r * CIRCLE_ARRAY[3*i + 2][1]); } canvas.drawPath(mPath, mPaint); canvas.restore(); }
3.既然能控制,那來玩玩唄
讓它變形倒不是什麼難事,關鍵是為了明顯些新增輔助點線真是要命,總算是完美展現給大家了

圓的形變.gif
//數字時間流 mAnimator = ValueAnimator.ofFloat(1, 0); mAnimator.setDuration(2000); mAnimator.setRepeatMode(ValueAnimator.REVERSE); mAnimator.setRepeatCount(-1); mAnimator.addUpdateListener(a -> { runNum = (float) a.getAnimatedValue(); mPath.reset(); invalidate(); }); //繪製時動態改變 for (int i = 0; i < CIRCLE_ARRAY.length / 3; i++) { mPath.cubicTo( r * runNum * CIRCLE_ARRAY[3 * i][0], r * runNum * CIRCLE_ARRAY[3 * i][1], r * runNum * CIRCLE_ARRAY[3 * i + 1][0], r * runNum * CIRCLE_ARRAY[3 * i + 1][1], r * CIRCLE_ARRAY[3 * i + 2][0], r * CIRCLE_ARRAY[3 * i + 2][1]); }
4.愛心---剛才是瞎玩的,現在要認真了:
只要控制第三段線的尾部,向下移的話,你應該能想到什麼吧

心形.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1], r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1], r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1], r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1], r * CIRCLE_ARRAY[8][0], r * (runNum) * CIRCLE_ARRAY[8][1]);//<----動我試試 mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
你也許會說好胖啊,瘦一點可以不
將第一段的控制點2和第二段的控制點1往上移動一點
一共就這麼九個主要點位,任你擺弄,你get到了嗎?

心形優化.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1] - ((1 - runNum) * 0.3f) * r, r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1] - ((1 - runNum) * 0.3f) * r, r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1], r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1], r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1] + ((1 - runNum) * 0.6f) * r); mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
4.想變扁/寬怎麼辦?
下側三個點一起平移

三點下移.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1]+ (1 - runNum) * 0.6f * r, r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]+ (1 - runNum) * 0.6f * r); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1]+ (1 - runNum) * 0.6f * r, r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1] , r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1] , r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]) ; mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
再讓下面變尖一點呢

三點下移尖底.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1]+ (1 - runNum) * 0.6f * r - ((1 - runNum) * 0.3f) * r, r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]+ (1 - runNum) * 0.6f * r); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1]+ (1 - runNum) * 0.6f * r - ((1 - runNum) * 0.3f) * r, r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1] , r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1] , r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]) ; mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
5.控制點長度變化:UFO的由來...
改變座標,將線1控制點2和線2的控制點1加長

加長控制點.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0] - (1 - runNum) * 4f * r, r * CIRCLE_ARRAY[1][1], r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0]+ (1 - runNum) * 4f * r, r * CIRCLE_ARRAY[3][1], r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0], r * CIRCLE_ARRAY[5][1]); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1], r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1], r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]); mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
6.觸控事件小試
當然你也可以不用ValueAnimate,用觸控事件來控制這些點也是相同的道理。

觸控事件.gif
mPath.cubicTo(//第一段 r * CIRCLE_ARRAY[0][0], r * CIRCLE_ARRAY[0][1], r * CIRCLE_ARRAY[1][0], r * CIRCLE_ARRAY[1][1], r * CIRCLE_ARRAY[2][0], r * CIRCLE_ARRAY[2][1]); mPath.cubicTo(//第二段 r * CIRCLE_ARRAY[3][0], r * CIRCLE_ARRAY[3][1], r * CIRCLE_ARRAY[4][0], r * CIRCLE_ARRAY[4][1], r * CIRCLE_ARRAY[5][0] + src.x - 2*r, r * CIRCLE_ARRAY[5][1]+ src.y); mPath.cubicTo(//第三段 r * CIRCLE_ARRAY[6][0], r * CIRCLE_ARRAY[6][1], r * CIRCLE_ARRAY[7][0], r * CIRCLE_ARRAY[7][1], r * CIRCLE_ARRAY[8][0], r * CIRCLE_ARRAY[8][1]); mPath.cubicTo(//第四段 r * CIRCLE_ARRAY[9][0], r * CIRCLE_ARRAY[9][1], r * CIRCLE_ARRAY[10][0], r * CIRCLE_ARRAY[10][1], r * CIRCLE_ARRAY[11][0], r * CIRCLE_ARRAY[11][1]);
好了,就演示這麼多,你可以把原始碼拷過去自己玩玩,原始碼檔案 Lever2CubicView.java
總結一下,一條貝塞爾曲線關鍵就是那三個點,能控制住,貝塞爾曲線可就在你股掌之間。
貝塞爾三次曲線還有很多逆天級別的操作,能力有限,日後有需求再研究吧
把圓形貝塞爾玩轉之後,基本上就能對付了。貝塞爾曲線水很深,只有你想不到,沒有它做不到。
後記:捷文規範
1.本文成長記錄及勘誤表
ofollow,noindex">專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-20 | Android繪圖最終篇之大戰貝塞爾三次曲線 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援

icon_wx_200.png