一、Path常用方法表
二.Path詳解
上一次除了一些常用函數(shù)之外啰脚,講解的基本上都是直線殷蛇,本次需要了解其中的曲線部分,說(shuō)到曲線,就不得不提大名鼎鼎的貝塞爾曲線橄浓。
貝塞爾曲線能干什么粒梦?
貝塞爾曲線的運(yùn)用是十分廣泛的,可以說(shuō)貝塞爾曲線奠定了計(jì)算機(jī)繪圖的基礎(chǔ)(因?yàn)樗梢詫⑷魏螐?fù)雜的圖形用精確的數(shù)學(xué)語(yǔ)言進(jìn)行描述)贮配,在你不經(jīng)意間就已經(jīng)使用過(guò)它了谍倦。
你會(huì)使用Photoshop的話,你可能會(huì)注意到里面有一個(gè)鋼筆工具泪勒,這個(gè)鋼筆工具核心就是貝塞爾曲線昼蛀。
你說(shuō)你不會(huì)PS宴猾? 沒(méi)關(guān)系,你如果看過(guò)前面的文章或者用過(guò)2D繪圖叼旋,肯定繪制過(guò)圓仇哆,圓弧,圓角矩形等這些東西夫植。這里面的圓弧部分全部都是貝塞爾曲線的運(yùn)用讹剔。
貝塞爾曲線作用十分廣泛,簡(jiǎn)單舉幾個(gè)的栗子:
- QQ小紅點(diǎn)拖拽效果
- 一些炫酷的下拉刷新控件
- 閱讀軟件的翻書(shū)效果
- 一些平滑的折線圖的制作
- 很多炫酷的動(dòng)畫(huà)效果
第一步.理解貝塞爾曲線的原理
此處理解貝塞爾曲線并非是學(xué)會(huì)公式的推導(dǎo)過(guò)程(推倒(?*?ω?)?)详民,而是要了解貝塞爾曲線是如何生成的延欠。
貝塞爾曲線是用一系列點(diǎn)來(lái)控制曲線狀態(tài)的,我將這些點(diǎn)簡(jiǎn)單分為兩類(lèi):
一階曲線原理:
一階曲線是沒(méi)有控制點(diǎn)的沈跨,僅有兩個(gè)數(shù)據(jù)點(diǎn)(A 和 B)由捎,最終效果一個(gè)線段。
上圖表示的是一階曲線生成過(guò)程中的某一個(gè)階段饿凛,動(dòng)態(tài)過(guò)程可以參照下圖(本文中貝塞爾曲線相關(guān)的動(dòng)態(tài)演示圖片來(lái)自維基百科)狞玛。
PS:一階曲線其實(shí)就是前面講解過(guò)的lineTo。
二階曲線原理:
二階曲線由兩個(gè)數(shù)據(jù)點(diǎn)(A 和 C)涧窒,一個(gè)控制點(diǎn)(B)來(lái)描述曲線狀態(tài)心肪,大致如下:
上圖中紅色曲線部分就是傳說(shuō)中的二階貝塞爾曲線,那么這條紅色曲線是如何生成的呢纠吴?接下來(lái)我們就以其中的一個(gè)狀態(tài)分析一下:
連接AB BC硬鞍,并在AB上取點(diǎn)D,BC上取點(diǎn)E呜象,使其滿足條件:
連接DE膳凝,取點(diǎn)F,使得:
這樣獲取到的點(diǎn)F就是貝塞爾曲線上的一個(gè)點(diǎn)恭陡,動(dòng)態(tài)過(guò)程如下:
PS: 二階曲線對(duì)應(yīng)的方法是quadTo
Path path = new Path();
path.moveTo(start.x,start.y);
path.quadTo(control.x,control.y,end.x,end.y);
canvas.drawPath(path, mPaint);
三階曲線原理:
三階曲線由兩個(gè)數(shù)據(jù)點(diǎn)(A 和 D)蹬音,兩個(gè)控制點(diǎn)(B 和 C)來(lái)描述曲線狀態(tài),如下:
三階曲線計(jì)算過(guò)程與二階類(lèi)似休玩,具體可以見(jiàn)下圖動(dòng)態(tài)效果:
PS: 三階曲線對(duì)應(yīng)的方法是cubicTo
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);
貝塞爾曲線速查表
強(qiáng)烈推薦點(diǎn)擊這里練習(xí)貝塞爾曲線著淆,可以加深對(duì)貝塞爾曲線的理解程度。
第二步.了解貝塞爾曲線相關(guān)函數(shù)使用方法
一階曲線:
一階曲線是一條線段拴疤,非常簡(jiǎn)單永部,可以參見(jiàn)上一篇文章Path之基本操作,此處就不詳細(xì)講解了呐矾。
二階曲線:
通過(guò)上面對(duì)二階曲線的簡(jiǎn)單了解苔埋,我們知道二階曲線是由兩個(gè)數(shù)據(jù)點(diǎn),一個(gè)控制點(diǎn)構(gòu)成蜒犯,接下來(lái)我們就用一個(gè)實(shí)例來(lái)演示二階曲線是如何運(yùn)用的组橄。
首先荞膘,兩個(gè)數(shù)據(jù)點(diǎn)是控制貝塞爾曲線開(kāi)始和結(jié)束的位置,比較容易理解玉工,而控制點(diǎn)則是控制貝塞爾的彎曲狀態(tài)羽资,相對(duì)來(lái)說(shuō)比較難以理解,所以本示例重點(diǎn)在于理解貝塞爾曲線彎曲狀態(tài)與控制點(diǎn)的關(guān)系遵班,廢話不多說(shuō)屠升,先上效果圖:
為了更加容易看出控制點(diǎn)與曲線彎曲程度的關(guān)系,上圖中繪制出了輔助點(diǎn)和輔助線狭郑,從上面的動(dòng)態(tài)圖可以看出腹暖,貝塞爾曲線在動(dòng)態(tài)變化過(guò)程中有類(lèi)似于橡皮筋一樣的彈性效果,因此在制作一些彈性效果的時(shí)候很常用愿阐。
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;
// 初始化數(shù)據(jù)點(diǎn)和控制點(diǎn)的位置
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) {
// 根據(jù)觸摸位置更新控制點(diǎn)微服,并提示重繪
control.x = event.getX();
control.y = event.getY();
invalidate();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪制數(shù)據(jù)點(diǎn)和控制點(diǎn)
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);
}
}
三階曲線:
三階曲線由兩個(gè)數(shù)據(jù)點(diǎn)和兩個(gè)控制點(diǎn)來(lái)控制曲線狀態(tài)。
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;
// 初始化數(shù)據(jù)點(diǎn)和控制點(diǎn)的位置
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) {
// 根據(jù)觸摸位置更新控制點(diǎn)缨历,并提示重繪
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);
// 繪制數(shù)據(jù)點(diǎn)和控制點(diǎn)
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);
}
}
三階曲線相比于二階曲線可以制作更加復(fù)雜的形狀,但是對(duì)于高階的曲線糙麦,用低階的曲線組合也可達(dá)到相同的效果辛孵,就是傳說(shuō)中的降階。因此我們對(duì)貝塞爾曲線的封裝方法一般最高只到三階曲線缠犀。
降階與升階
第三步.貝塞爾曲線使用實(shí)例
在制作這個(gè)實(shí)例之前欧瘪,首先要明確一個(gè)內(nèi)容框往,就是在什么情況下需要使用貝塞爾曲線?
至于只需要一個(gè)靜態(tài)的曲線圖形的情況冶匹,用圖片豈不是更好,大量的計(jì)算會(huì)很不劃算咆瘟。
如果是顯示SVG矢量圖的話嚼隘,已經(jīng)有相關(guān)的解析工具了(內(nèi)部依舊運(yùn)用的有貝塞爾曲線),不需要手動(dòng)計(jì)算袒餐。
貝塞爾曲線的主要優(yōu)點(diǎn)是可以實(shí)時(shí)控制曲線狀態(tài)飞蛹,并可以通過(guò)改變控制點(diǎn)的狀態(tài)實(shí)時(shí)讓曲線進(jìn)行平滑的狀態(tài)變化。
接下來(lái)我們就用一個(gè)簡(jiǎn)單的示例讓一個(gè)圓漸變成為心形:
思路分析:
我們最終的需要的效果是將一個(gè)圓轉(zhuǎn)變成一個(gè)心形灸眼,通過(guò)分析可知卧檐,圓可以由四段三階貝塞爾曲線組合而成,如下:
心形也可以由四段的三階的貝塞爾曲線組成焰宣,如下:
兩者的差別僅僅在于數(shù)據(jù)點(diǎn)和控制點(diǎn)位置不同霉囚,因此只需要調(diào)整數(shù)據(jù)點(diǎn)和控制點(diǎn)的位置,就能將圓形變?yōu)樾男巍?br> 核心難點(diǎn):
1.如何得到數(shù)據(jù)點(diǎn)和控制點(diǎn)的位置匕积?
關(guān)于使用繪制圓形的數(shù)據(jù)點(diǎn)與控制點(diǎn)早就已經(jīng)有人詳細(xì)的計(jì)算好了盈罐,可以參考stackoverflow的一個(gè)回答How to create circle with Bézier curves?其中的數(shù)據(jù)只需要拿來(lái)用即可逻澳。
而對(duì)于心形的數(shù)據(jù)點(diǎn)和控制點(diǎn),可以由圓形的部分?jǐn)?shù)據(jù)點(diǎn)和控制點(diǎn)平移后得到暖呕,具體參數(shù)可以自己慢慢調(diào)整到一個(gè)滿意的效果斜做。
2.如何達(dá)到漸變效果?
漸變其實(shí)就是每次對(duì)數(shù)據(jù)點(diǎn)和控制點(diǎn)稍微移動(dòng)一點(diǎn)湾揽,然后重繪界面瓤逼,在短時(shí)間多次的調(diào)整數(shù)據(jù)點(diǎn)與控制點(diǎn),使其逐漸接近目標(biāo)值库物,通過(guò)不斷的重繪界面達(dá)到一種漸變的效果霸旗。過(guò)程可以參照下圖動(dòng)態(tài)效果:
public class Bezier3 extends View {
private static final float C = 0.551915024494f; // 一個(gè)常量,用來(lái)計(jì)算繪制圓形貝塞爾曲線控制點(diǎn)的位置
private Paint mPaint;
private int mCenterX, mCenterY;
private PointF mCenter = new PointF(0,0);
private float mCircleRadius = 200; // 圓的半徑
private float mDifference = mCircleRadius*C; // 圓形的控制點(diǎn)與數(shù)據(jù)點(diǎn)的差值
private float[] mData = new float[8]; // 順時(shí)針記錄繪制圓形的四個(gè)數(shù)據(jù)點(diǎn)
private float[] mCtrl = new float[16]; // 順時(shí)針記錄繪制圓形的八個(gè)控制點(diǎn)
private float mDuration = 1000; // 變化總時(shí)長(zhǎng)
private float mCurrent = 0; // 當(dāng)前已進(jìn)行時(shí)長(zhǎng)
private float mCount = 100; // 將時(shí)長(zhǎng)總共劃分多少份
private float mPiece = mDuration/mCount; // 每一份的時(shí)長(zhǎng)
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);
// 初始化數(shù)據(jù)點(diǎn)
mData[0] = 0;
mData[1] = mCircleRadius;
mData[2] = mCircleRadius;
mData[3] = 0;
mData[4] = 0;
mData[5] = -mCircleRadius;
mData[6] = -mCircleRadius;
mData[7] = 0;
// 初始化控制點(diǎn)
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); // 繪制坐標(biāo)系
canvas.translate(mCenterX, mCenterY); // 將坐標(biāo)系移動(dòng)到畫(huà)布中央
canvas.scale(1,-1); // 翻轉(zhuǎn)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) {
// 繪制數(shù)據(jù)點(diǎn)和控制點(diǎn)
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);
}
// 繪制坐標(biāo)系
private void drawCoordinateSystem(Canvas canvas) {
canvas.save(); // 繪制做坐標(biāo)系
canvas.translate(mCenterX, mCenterY); // 將坐標(biāo)系移動(dòng)到畫(huà)布中央
canvas.scale(1,-1); // 翻轉(zhuǎn)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();
}
}
三戚揭、總結(jié)
其實(shí)關(guān)于貝塞爾曲線最重要的是核心理解貝塞爾曲線的生成方式诱告,只有理解了貝塞爾曲線的生成方式,才能更好的運(yùn)用貝塞爾曲線民晒。在上一篇末尾說(shuō)本篇可能涉及一點(diǎn)圖形渲染問(wèn)題精居,不幸的是,本篇沒(méi)有了潜必,請(qǐng)期待下一篇(可能會(huì)在下一篇中出現(xiàn)o( ̄︶ ̄)o)靴姿,下一篇依舊Path相關(guān)內(nèi)容,教給大家一些更好玩的東西磁滚。
解鎖新的境界之【繪制一個(gè)彈性的圓】: