零、前言
1.可以說貝塞爾曲線是一把 "石中劍",能夠拔出它,會讓你的繪圖如虎添翼。
2.今天要與貝塞爾曲線大戰三百回合,將它加入我的繪圖大軍麾下。
3.自此Android繪圖五虎將:Canvas,Path,Paint,Color,貝塞爾
便集結完成。
4.本專案原始碼見文尾捷文規範
第一條,檢視原始碼在view包
,分析工具在analyze包
一、貝塞爾三次曲線初體驗
1.無網格,不曲線
,廢話不多說,上網格+座標系
/**
* 作者:張風捷特烈<br/>
* 時間:2018/11/16 0016:9:04<br/>
* 郵箱:1981462002@qq.com<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);
複製程式碼
也許這樣看不出什麼關係:現在把四個控制點也畫出來(紅色):
mHelpPint.setStrokeWidth(10);
HelpDraw.drawPos(canvas, mHelpPint, p0, p1, p2, p3);
複製程式碼
是不是有點意思了--在加兩條線:
mHelpPint.setStrokeWidth(2);
HelpDraw.drawLines(canvas, mHelpPint, p0, p1, p2, p3);
複製程式碼
小結:
p0:第一點
,p3:最終點
,p1:控制點1
,p2:控制點2
二、動態效果:任意一段三次貝塞爾曲線的最優雅實現形式
以前看過別人的任意一段三次貝塞爾曲線,感覺體驗太差,切換個點還要點按鈕,
下面我實現四個點任意拖動的三次貝塞爾曲線,可謂是非常優雅的,讓你明白點域的判斷
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.映象:
先選取感覺滿意的半邊,記錄四個點位:
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.如何實現下面的效果呢?
在原來的基礎上在畫一段貝塞爾曲線,要求:新控制點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走起
好吧,有點像做俯臥撐,實現起來也挺簡單的:
//數字時間流
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
,大家可以下載,執行自己玩玩,加深一下對貝塞爾三次曲線的感覺
好了,開胃菜結束了,下面進入正餐,你沒看錯,好戲才剛剛開始。
四、高階:三階貝塞爾的優雅使用:
注意:
前方高能,非戰鬥人員請儘快準備瓜子,飲料,花生米...
1.三階貝塞爾畫圓:
看下圖,你可能會滿臉不屑地說:"切,我用canvas分分秒描畫你信不信?"
老大,我信...且往下看
2.如何優雅地繪製多條貝塞爾曲線
下面是四條貝塞爾曲線繪製的圓,看圖就知道優勢在於任意改變形狀
但如果把點位都放在mPath.cubicTo()裡,多幾條線就亂成一鍋粥了,最好統一管理一下
第一個想到的是每條線的三個點都抽成三個成員變數,不過還是很難維護,這個問題一直困擾我
今天突然想到二維陣列不是挺好嗎?二維每個裡面兩個點。
//單位圓(即半徑為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.既然能控制,那來玩玩唄
讓它變形倒不是什麼難事,關鍵是為了明顯些新增輔助點線真是要命,總算是完美展現給大家了
//數字時間流
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.愛心---剛才是瞎玩的,現在要認真了:
只要控制第三段線的尾部,向下移的話,你應該能想到什麼吧
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到了嗎?
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.想變扁/寬怎麼辦?
下側三個點一起平移
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]);
複製程式碼
再讓下面變尖一點呢
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加長
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,用觸控事件來控制這些點也是相同的道理。
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.本文成長記錄及勘誤表
專案原始碼 | 日期 | 備註 |
---|---|---|
V0.1--github | 2018-11-20 | Android繪圖最終篇之大戰貝塞爾三次曲線 |
2.更多關於我
筆名 | 微信 | 愛好 | |
---|---|---|---|
張風捷特烈 | 1981462002 | zdl1994328 | 語言 |
我的github | 我的簡書 | 我的掘金 | 個人網站 |
3.宣告
1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援