效果圖中咱們實現了一個簡單的隨手指滑動的二階貝塞爾曲線,還有一個複雜點的,穿越全部已知點的貝塞爾曲線。學會使用貝塞爾曲線後能夠實現例如QQ紅點滑動刪除啦,360動態球啦,bulabulabula~html
貝賽爾曲線(Bézier曲線)是電腦圖形學中至關重要的參數曲線。更高維度的普遍化貝塞爾曲線就稱做貝塞爾曲面,其中貝塞爾三角是一種特殊的實例。貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所普遍發表,他運用貝塞爾曲線來爲汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau算法開發,以穩定數值的方法求出貝塞爾曲線。java
讀完上述貝塞爾曲線簡介我仍是一頭霧水,來個示例唄。算法
給定點P0、P1,線性貝塞爾曲線只是一條兩點之間的直線。這條線由下式給出:canvas
二次方貝塞爾曲線的路徑由給定點P0、P一、P2的函數B(t)追蹤:app
P0、P一、P二、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始於P0走向P1,並從P2的方向來到P3。通常不會通過P1或P2;公式以下:ide
身爲三維生物超出三維我很方,這裏只給示例圖。想具體瞭解的同窗請左轉度娘。函數
Android在API=1的時候就提供了貝塞爾曲線的畫法,只是隱藏在Path#quadTo()和Path#cubicTo()方法中,一個是二階貝塞爾曲線,一個是三階貝塞爾曲線。固然,若是你想本身寫個方法,依照上面貝塞爾的表達式也是能夠的。不過通常沒有必要,由於Android已經在native層爲咱們封裝好了二階和三階的函數。佈局
初始化各個參數,花3s掃一下便可。this
private Paint mPaint; private Path mPath; private Point startPoint; private Point endPoint; // 輔助點 private Point assistPoint; public BezierView(Context context) { this(context, null); } public BezierView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public BezierView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { mPaint = new Paint(); mPath = new Path(); startPoint = new Point(300, 600); endPoint = new Point(900, 600); assistPoint = new Point(600, 900); // 抗鋸齒 mPaint.setAntiAlias(true); // 防抖動 mPaint.setDither(true); }
在onDraw中畫二階貝塞爾idea
// 畫筆顏色 mPaint.setColor(Color.BLACK); // 筆寬 mPaint.setStrokeWidth(POINTWIDTH); // 空心 mPaint.setStyle(Paint.Style.STROKE); // 重置路徑 mPath.reset(); // 起點 mPath.moveTo(startPoint.x, startPoint.y); // 重要的就是這句 mPath.quadTo(assistPoint.x, assistPoint.y, endPoint.x, endPoint.y); // 畫路徑 canvas.drawPath(mPath, mPaint); // 畫輔助點 canvas.drawPoint(assistPoint.x, assistPoint.y, mPaint);
上面註釋很清晰就不贅述了。示例中貝塞爾是能夠跟着手指的滑動而變化,我一拍榴蓮,確定是複寫了onTouchEvent()!
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: assistPoint.x = (int) event.getX(); assistPoint.y = (int) event.getY(); Log.i(TAG, "assistPoint.x = " + assistPoint.x); Log.i(TAG, "assistPoint.Y = " + assistPoint.y); invalidate(); break; } return true; }
最後將咱們自定義的BezierView添加到佈局文件中。至此一個簡單的二階貝塞爾曲線就完成了。假設一下,在向下拉動的過程當中,在曲線上增長一個「小超人」,360動態清理是否是就出來了呢?有興趣的能夠本身拓展下。
(圖一)
(圖二)
要想獲得上圖的效果,須要二階貝塞爾和三階貝塞爾配合。具體表現爲,第一段和最後一段曲線爲二階貝塞爾,中間N段都爲三階貝塞爾曲線。
先根據相鄰點(P1,P2, P3)計算出相鄰點的中點(P4, P5),而後再計算相鄰中點的中點(P6)。而後將(P4,P6, P5)組成的線段平移到通過P2的直線(P8,P2,P7)上。接着根據(P4,P6,P5,P2)的座標計算出(P7,P8)的座標。最後根據P7,P8等控制點畫出三階貝塞爾曲線。
爲了方便講解以及讀者的理解。本篇以圖一效果爲例進行講解。BezierView座標都是根據屏幕動態生成的,想要圖二的效果只需修改初始座標,不用對代碼作很大的修改便可實現。
private static final String TAG = "BIZIER"; private static final int LINEWIDTH = 5; private static final int POINTWIDTH = 10; private Context mContext; /** 即將要穿越的點集合 */ private List mPoints = new ArrayList<>(); /** 中點集合 */ private List mMidPoints = new ArrayList<>(); /** 中點的中點集合 */ private List mMidMidPoints = new ArrayList<>(); /** 移動後的點集合(控制點) */ private List mControlPoints = new ArrayList<>(); private int mScreenWidth; private int mScreenHeight; private void init(Context context) { mPaint = new Paint(); mPath = new Path(); // 抗鋸齒 mPaint.setAntiAlias(true); // 防抖動 mPaint.setDither(true); mContext = context; getScreenParams(); initPoints(); initMidPoints(this.mPoints); initMidMidPoints(this.mMidPoints); initControlPoints(this.mPoints, this.mMidPoints , this.mMidMidPoints); }
第一個函數獲取屏幕寬高就不說了。緊接着初始化了初始點、中點、中點的中點、控制點。咱們一個個的跟進。首先是初始點。
/** 添加即將要穿越的點 */ private void initPoints() { int pointWidthSpace = mScreenWidth / 5; int pointHeightSpace = 100; for (int i = 0; i < 5; i++) { Point point; // 一高一低五個點 if (i%2 != 0) { point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2 - pointHeightSpace); } else { point = new Point((int) (pointWidthSpace*(i + 0.5)), mScreenHeight/2); } mPoints.add(point); } }
這裏循環建立了一高一低五個點,並添加到List mPoints中。上文說道圖一到圖二隻需修改這裏的初始點便可。
/** 初始化中點集合 */ private void initMidPoints(List points) { for (int i = 0; i < points.size(); i++) { Point midPoint = null; if (i == points.size()-1){ return; }else { midPoint = new Point((points.get(i).x + points.get(i + 1).x)/2, (points.get(i).y + points.get(i + 1).y)/2); } mMidPoints.add(midPoint); } } /** 初始化中點的中點集合 */ private void initMidMidPoints(List midPoints){ for (int i = 0; i < midPoints.size(); i++) { Point midMidPoint = null; if (i == midPoints.size()-1){ return; }else { midMidPoint = new Point((midPoints.get(i).x + midPoints.get(i + 1).x)/2, (midPoints.get(i).y + midPoints.get(i + 1).y)/2); } mMidMidPoints.add(midMidPoint); } }
這裏算出中點集合以及中點的中點集合,小學數學題沒什麼好說的。惟一須要注意的是他們數量的差異。
/** 初始化控制點集合 */ private void initControlPoints(List points, List midPoints, List midMidPoints){ for (int i = 0; i < points.size(); i ++){ if (i ==0 || i == points.size()-1){ continue; }else{ Point before = new Point(); Point after = new Point(); before.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i - 1).x; before.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i - 1).y; after.x = points.get(i).x - midMidPoints.get(i - 1).x + midPoints.get(i).x; after.y = points.get(i).y - midMidPoints.get(i - 1).y + midPoints.get(i).y; mControlPoints.add(before); mControlPoints.add(after); } } }
你們須要注意下這個方法的計算過程。以圖一(P2,P4, P6,P8)爲例。如今P二、P四、P6的座標是已知的。根據因爲(P8, P2)線段由(P4, P6)線段平移而來,因此可得以下結論:P2 - P6 = P8 - P4 。即P8 = P2 - P6 + P4。其他同理。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // *********************************************************** // ************* 貝塞爾進階--曲滑穿越已知點 ********************** // *********************************************************** // 畫原始點 drawPoints(canvas); // 畫穿越原始點的折線 drawCrossPointsBrokenLine(canvas); // 畫中間點 drawMidPoints(canvas); // 畫中間點的中間點 drawMidMidPoints(canvas); // 畫控制點 drawControlPoints(canvas); // 畫貝塞爾曲線 drawBezier(canvas); }
能夠看到,在畫貝塞爾曲線以前咱們畫了一系列的輔助點,還有和貝塞爾曲線做對比的折線圖。效果如圖一。輔助點的座標全都獲得了,基本的畫畫就比較簡單了。有能力的可跳過下面這段,直接進入drawBezier(canvas)
方法。基本的畫畫這裏只貼代碼,若有疑問可評論或者私信。
/** 畫原始點 */ private void drawPoints(Canvas canvas) { mPaint.setStrokeWidth(POINTWIDTH); for (int i = 0; i < mPoints.size(); i++) { canvas.drawPoint(mPoints.get(i).x, mPoints.get(i).y, mPaint); } } /** 畫穿越原始點的折線 */ private void drawCrossPointsBrokenLine(Canvas canvas) { mPaint.setStrokeWidth(LINEWIDTH); mPaint.setColor(Color.RED); // 重置路徑 mPath.reset(); // 畫穿越原始點的折線 mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y); for (int i = 0; i < mPoints.size(); i++) { mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y); } canvas.drawPath(mPath, mPaint); } /** 畫中間點 */ private void drawMidPoints(Canvas canvas) { mPaint.setStrokeWidth(POINTWIDTH); mPaint.setColor(Color.BLUE); for (int i = 0; i < mMidPoints.size(); i++) { canvas.drawPoint(mMidPoints.get(i).x, mMidPoints.get(i).y, mPaint); } } /** 畫中間點的中間點 */ private void drawMidMidPoints(Canvas canvas) { mPaint.setColor(Color.YELLOW); for (int i = 0; i < mMidMidPoints.size(); i++) { canvas.drawPoint(mMidMidPoints.get(i).x, mMidMidPoints.get(i).y, mPaint); } } /** 畫控制點 */ private void drawControlPoints(Canvas canvas) { mPaint.setColor(Color.GRAY); // 畫控制點 for (int i = 0; i < mControlPoints.size(); i++) { canvas.drawPoint(mControlPoints.get(i).x, mControlPoints.get(i).y, mPaint); } }
/** 畫貝塞爾曲線 */ private void drawBezier(Canvas canvas) { mPaint.setStrokeWidth(LINEWIDTH); mPaint.setColor(Color.BLACK); // 重置路徑 mPath.reset(); for (int i = 0; i < mPoints.size(); i++){ if (i == 0){// 第一條爲二階貝塞爾 mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起點 mPath.quadTo(mControlPoints.get(i).x, mControlPoints.get(i).y,// 控制點 mPoints.get(i + 1).x,mPoints.get(i + 1).y); }else if(i < mPoints.size() - 2){// 三階貝塞爾 mPath.cubicTo(mControlPoints.get(2*i-1).x,mControlPoints.get(2*i-1).y,// 控制點 mControlPoints.get(2*i).x,mControlPoints.get(2*i).y,// 控制點 mPoints.get(i+1).x,mPoints.get(i+1).y);// 終點 }else if(i == mPoints.size() - 2){// 最後一條爲二階貝塞爾 mPath.moveTo(mPoints.get(i).x, mPoints.get(i).y);// 起點 mPath.quadTo(mControlPoints.get(mControlPoints.size()-1).x,mControlPoints.get(mControlPoints.size()-1).y, mPoints.get(i+1).x,mPoints.get(i+1).y);// 終點 } } canvas.drawPath(mPath,mPaint); }
註釋太詳細,都沒什麼好寫的了。不過這裏須要注意判斷裏面的條件,對起點和終點的判斷必定要理解。要否則極可能會送你一個ArrayIndexOutOfBoundsException。
貝塞爾曲線能夠實現不少絢麗的效果,難的不是貝塞爾,而是good idea。
參考:http://www.2cto.com/kf/201604/497130.html