在开发一些特效或者控件的时候常常会用到一些数学函数,除了三角数学函数外,使用最多的就是Bezier了,也就是我们常说的贝塞尔曲线。
Android开发中常常会使用Bezier曲线绘制一些图形,或者使用Bezier曲线生成一些动画的运动轨迹,亦或者使用贝塞尔曲线制作动画的差值器。
总之,这是一个作用非常大的东西,它不像三角数学函数那样有固定的曲线特性,而是根据参数的变更而呈现出不同的曲线,可以这样说,Bezier曲线可以根据用户的需求调整自身的曲线形式,可以满足绝大部分用户对曲线的需求,如果不能,那就再加一阶。
本篇的初衷是因为贝塞尔函数看起来是非常抽象的,使用时则需要不断调整参数来尝试出合适的结果,而本工具则是根据调整出的曲线逆向显示出函数的参数,废话不多说,上图:
前言:本篇不描述Bezier算法的实现,只基于AndroidSDK记录二阶Bezier曲线函数的API使用。
Android为我们提供了二阶和三阶Bezier函数的API,更高阶的Bezier函数可以使用github上的扩展实现
二阶和三阶的AndroidAPI分别是:
1 2 3 4
| path.quadTo(anchorPoint.x, anchorPoint.y, endPoint.x, endPoint.y); path.cubicTo(x1, y1, x2, y2, x3, y3);
|
上面两个是Path类提供的方法,而我们如果需要绘制的话则可以使用canvas的drawPath方法进行使用。
为了实现开篇的效果,我们首先需要定义一个View,并初始化一些Paint。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class BezierDrawer extends View { private Paint paint; private Paint lineEndPaint; private Paint pointPaint; private Paint linePaint; private void init() { paint = new Paint(); paint.setColor(Color.BLUE); paint.setStrokeWidth(20); paint.setStyle(Paint.Style.STROKE); paint.setAntiAlias(true); lineEndPaint = new Paint(); lineEndPaint.setColor(Color.BLUE); lineEndPaint.setStyle(Paint.Style.FILL); lineEndPaint.setAntiAlias(true); pointPaint = new Paint(); pointPaint.setStrokeWidth(8); pointPaint.setColor(Color.DKGRAY); pointPaint.setStyle(Paint.Style.STROKE); pointPaint.setAntiAlias(true); linePaint = new Paint(); linePaint.setStrokeWidth(2); linePaint.setColor(Color.DKGRAY); linePaint.setStyle(Paint.Style.STROKE); linePaint.setAntiAlias(true); }
|
在创建过画笔之后我们需要知道绘制二阶Bezier曲线需要的三个点,我们初始化一个List<Point>来存贮这些点,并在控件被首次测量的时候初始化这些位置。
1 2 3 4 5 6 7 8 9 10 11 12
| @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMeasuredWidth(); int height = getMeasuredHeight(); if (pointList.size() == 0) { pointList.add(new Point(width / 5, height / 2)); pointList.add(new Point(width / 2, height / 4)); pointList.add(new Point(width / 5 * 4, height / 2)); if (onChangeListener != null) onChangeListener.onchange(pointList); } }
|
这样我们有了一些固定的点,那么接下来我们在ondraw方法中讲这些点使用Bezier的API把曲线画出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Path path = new Path(); Point startPoint = pointList.get(0); Point anchorPoint = pointList.get(1); Point endPoint = pointList.get(2); path.moveTo(startPoint.x, startPoint.y); path.quadTo(anchorPoint.x, anchorPoint.y, endPoint.x, endPoint.y); canvas.drawPath(path, paint); canvas.drawCircle(startPoint.x, startPoint.y, paint.getStrokeWidth() / 2, lineEndPaint); canvas.drawCircle(endPoint.x, endPoint.y, paint.getStrokeWidth() / 2, lineEndPaint); canvas.drawLine(startPoint.x, startPoint.y, anchorPoint.x, anchorPoint.y, linePaint); canvas.drawLine(anchorPoint.x, anchorPoint.y, endPoint.x, endPoint.y, linePaint); for (Point point : pointList) { canvas.drawCircle(point.x, point.y, 20, pointPaint); } }
|
这时候一个最简单的二阶Bezier曲线以及被我们画出来了,那么接下来要做的就是让这个曲线可以根据我们的触摸滑动操作来进行变化,复写onTouchEvent方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Point point = new Point((int) event.getX(), (int) event.getY()); touchedPoint = getNearestPoint(point); break; case MotionEvent.ACTION_MOVE: if (touchedPoint != -1) { Point pointMove = new Point((int) event.getX(), (int) event.getY()); pointList.set(touchedPoint, pointMove); invalidate(); if (onChangeListener != null) onChangeListener.onchange(pointList); } break; case MotionEvent.ACTION_UP: touchedPoint = -1; break; default: } return true; } int touchDist = 50; private int getNearestPoint(Point point) { for (int i = 0; i < pointList.size(); i++) { Point p = pointList.get(i); if (Math.abs(p.x - point.x) < touchDist && Math.abs(p.y - point.y) < touchDist) { return i; } } return -1; }
|
补上监听相关的代码
1 2 3 4 5 6 7
| interface OnChangeListener { void onchange(List<Point> pointList); } private OnChangeListener onChangeListener; public void setOnChangeListener(OnChangeListener onChangeListener) { this.onChangeListener = onChangeListener; }
|
完工
完整的工程代码及示例见github:https://github.com/NightFarmer/BezierDemo