转自:http://www.2cto.com/kf/201604/497130.html
Android 自定义View高级特效,神奇的贝塞尔曲线
-
效果图

效果图中我们实现了一个简单的随手指滑动的二阶贝塞尔曲线,还有一个复杂点的,穿越所有已知点的贝塞尔曲线。学会使用贝塞尔曲线后可以实现例如QQ红点滑动删除啦,360动态球啦,bulabulabula~
什么是贝塞尔曲线?
贝赛尔曲线(Bézier曲线)是电脑图形学中相当重要的参数曲线。更高维度的广泛化贝塞尔曲线就称作贝塞尔曲面,其中贝塞尔三角是一种特殊的实例。贝塞尔曲线于1962年,由法国工程师皮埃尔·贝塞尔(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。贝塞尔曲线最初由Paul de Casteljau于1959年运用de Casteljau算法开发,以稳定数值的方法求出贝塞尔曲线。
读完上述贝塞尔曲线简介我还是一头雾水,来个示例呗。
示例
线性贝塞尔曲线
给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:


二次方贝塞尔曲线
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:



三次方贝塞尔曲线
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:



N次方贝塞尔曲线
身为三维生物超出三维我很方,这里只给示例图。想具体了解的同学请左转度娘。


就当没看过上面
Android在API=1的时候就提供了贝塞尔曲线的画法,只是隐藏在Path#quadTo()和Path#cubicTo()方法中,一个是二阶贝塞尔曲线,一个是三阶贝塞尔曲线。当然,如果你想自己写个方法,依照上面贝塞尔的表达式也是可以的。不过一般没有必要,因为Android已经在native层为我们封装好了二阶和三阶的函数。
从一个二阶贝塞尔开始
自定义一个BezierView
初始化各个参数,花3s扫一下即可。
123456789101112131415161718192021222324252627282930<codeclass="hljs java">privatePaint mPaint;privatePath mPath;privatePoint startPoint;privatePoint endPoint;// 辅助点privatePoint assistPoint;publicBezierView(Context context) {this(context,null);}publicBezierView(Context context, AttributeSet attrs) {this(context, attrs,0);}publicBezierView(Context context, AttributeSet attrs,intdefStyleAttr) {super(context, attrs, defStyleAttr);init(context);}privatevoidinit(Context context) {mPaint =newPaint();mPath =newPath();startPoint =newPoint(300,600);endPoint =newPoint(900,600);assistPoint =newPoint(600,900);// 抗锯齿mPaint.setAntiAlias(true);// 防抖动mPaint.setDither(true);}</code>在onDraw中画二阶贝塞尔
12345678910111213141516<codeclass="hljs avrasm">// 画笔颜色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);</code>上面注释很清晰就不赘述了。示例中贝塞尔是可以跟着手指的滑动而变化,我一拍榴莲,肯定是复写了onTouchEvent()!
1234567891011121314<codeclass="hljs cs">@OverridepublicbooleanonTouchEvent(MotionEvent event) {switch(event.getAction()) {caseMotionEvent.ACTION_DOWN:caseMotionEvent.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;}returntrue;}</code>最后将我们自定义的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坐标都是根据屏幕动态生成的,想要图二的效果只需修改初始坐标,不用对代码做很大的修改即可实现。
那么,开始吧!
初始化参数
1234567891011121314151617181920212223242526272829303132<codeclass="hljs java">privatestaticfinalString TAG ="BIZIER";privatestaticfinalintLINEWIDTH =5;privatestaticfinalintPOINTWIDTH =10;privateContext mContext;/** 即将要穿越的点集合 */privateList<point> mPoints =newArrayList<>();/** 中点集合 */privateList<point> mMidPoints =newArrayList<>();/** 中点的中点集合 */privateList<point> mMidMidPoints =newArrayList<>();/** 移动后的点集合(控制点) */privateList<point> mControlPoints =newArrayList<>();privateintmScreenWidth;privateintmScreenHeight;privatevoidinit(Context context) {mPaint =newPaint();mPath =newPath();// 抗锯齿mPaint.setAntiAlias(true);// 防抖动mPaint.setDither(true);mContext = context;getScreenParams();initPoints();initMidPoints(this.mPoints);initMidMidPoints(this.mMidPoints);initControlPoints(this.mPoints,this.mMidPoints ,this.mMidMidPoints);}</point></point></point></point></code>第一个函数获取屏幕宽高就不说了。紧接着初始化了初始点、中点、中点的中点、控制点。我们一个个的跟进。首先是初始点。
123456789101112131415<codeclass="hljs java">/** 添加即将要穿越的点 */privatevoidinitPoints() {intpointWidthSpace = mScreenWidth /5;intpointHeightSpace =100;for(inti =0; i <5; i++) {Point point;// 一高一低五个点if(i%2!=0) {point =newPoint((int) (pointWidthSpace*(i +0.5)), mScreenHeight/2- pointHeightSpace);}else{point =newPoint((int) (pointWidthSpace*(i +0.5)), mScreenHeight/2);}mPoints.add(point);}}</code>这里循环创建了一高一低五个点,并添加到List mPoints中。上文说道图一到图二只需修改这里的初始点即可。
12345678910111213141516171819202122232425<codeclass="hljs java">/** 初始化中点集合 */privatevoidinitMidPoints(List<point> points) {for(inti =0; i < points.size(); i++) {Point midPoint =null;if(i == points.size()-1){return;}else{midPoint =newPoint((points.get(i).x + points.get(i +1).x)/2, (points.get(i).y + points.get(i +1).y)/2);}mMidPoints.add(midPoint);}}/** 初始化中点的中点集合 */privatevoidinitMidMidPoints(List<point> midPoints){for(inti =0; i < midPoints.size(); i++) {Point midMidPoint =null;if(i == midPoints.size()-1){return;}else{midMidPoint =newPoint((midPoints.get(i).x + midPoints.get(i +1).x)/2, (midPoints.get(i).y + midPoints.get(i +1).y)/2);}mMidMidPoints.add(midMidPoint);}}</point></point></code>这里算出中点集合以及中点的中点集合,小学数学题没什么好说的。唯一需要注意的是他们数量的差别。
1234567891011121314151617<codeclass="hljs avrasm">/** 初始化控制点集合 */privatevoidinitControlPoints(List<point> points, List<point> midPoints, List<point> midMidPoints){for(inti =0; i < points.size(); i ++){if(i ==0|| i == points.size()-1){continue;}else{Point before =newPoint();Point after =newPoint();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);}}}</point></point></point></code>大家需要注意下这个方法的计算过程。以图一(P2,P4, P6,P8)为例。现在P2、P4、P6的坐标是已知的。根据由于(P8, P2)线段由(P4, P6)线段平移而来,所以可得如下结论:P2 - P6 = P8 - P4 。即P8 = P2 - P6 + P4。其余同理。
画辅助点以及对比折线图
123456789101112131415161718192021<codeclass="hljs mel">@OverrideprotectedvoidonDraw(Canvas canvas) {super.onDraw(canvas);// ***********************************************************// ************* 贝塞尔进阶--曲滑穿越已知点 **********************// ***********************************************************// 画原始点drawPoints(canvas);// 画穿越原始点的折线drawCrossPointsBrokenLine(canvas);// 画中间点drawMidPoints(canvas);// 画中间点的中间点drawMidMidPoints(canvas);// 画控制点drawControlPoints(canvas);// 画贝塞尔曲线drawBezier(canvas);}</code>可以看到,在画贝塞尔曲线之前我们画了一系列的辅助点,还有和贝塞尔曲线作对比的折线图。效果如图一。辅助点的坐标全都得到了,基本的画画就比较简单了。有能力的可跳过下面这段,直接进入
drawBezier(canvas)方法。基本的画画这里只贴代码,如有疑问可评论或者私信。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849<codeclass="hljs java">/** 画原始点 */privatevoiddrawPoints(Canvas canvas) {mPaint.setStrokeWidth(POINTWIDTH);for(inti =0; i < mPoints.size(); i++) {canvas.drawPoint(mPoints.get(i).x, mPoints.get(i).y, mPaint);}}/** 画穿越原始点的折线 */privatevoiddrawCrossPointsBrokenLine(Canvas canvas) {mPaint.setStrokeWidth(LINEWIDTH);mPaint.setColor(Color.RED);// 重置路径mPath.reset();// 画穿越原始点的折线mPath.moveTo(mPoints.get(0).x, mPoints.get(0).y);for(inti =0; i < mPoints.size(); i++) {mPath.lineTo(mPoints.get(i).x, mPoints.get(i).y);}canvas.drawPath(mPath, mPaint);}/** 画中间点 */privatevoiddrawMidPoints(Canvas canvas) {mPaint.setStrokeWidth(POINTWIDTH);mPaint.setColor(Color.BLUE);for(inti =0; i < mMidPoints.size(); i++) {canvas.drawPoint(mMidPoints.get(i).x, mMidPoints.get(i).y, mPaint);}}/** 画中间点的中间点 */privatevoiddrawMidMidPoints(Canvas canvas) {mPaint.setColor(Color.YELLOW);for(inti =0; i < mMidMidPoints.size(); i++) {canvas.drawPoint(mMidMidPoints.get(i).x, mMidMidPoints.get(i).y, mPaint);}}/** 画控制点 */privatevoiddrawControlPoints(Canvas canvas) {mPaint.setColor(Color.GRAY);// 画控制点for(inti =0; i < mControlPoints.size(); i++) {canvas.drawPoint(mControlPoints.get(i).x, mControlPoints.get(i).y, mPaint);}}</code>画贝塞尔曲线
123456789101112131415161718192021222324<codeclass="hljs avrasm">/** 画贝塞尔曲线 */privatevoiddrawBezier(Canvas canvas) {mPaint.setStrokeWidth(LINEWIDTH);mPaint.setColor(Color.BLACK);// 重置路径mPath.reset();for(inti =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);}elseif(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);// 终点}elseif(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);}</code>注释太详细,都没什么好写的了。不过这里需要注意判断里面的条件,对起点和终点的判断一定要理解。要不然很可能会送你一个ArrayIndexOutOfBoundsException。
结束
贝塞尔曲线可以实现很多绚丽的效果,难的不是贝塞尔,而是good idea。
7617

被折叠的 条评论
为什么被折叠?



