贝塞尔曲线开发的艺术

一句话概括贝塞尔曲线:将任意一条曲线转化为精确的数学公式。

很多绘图工具中的钢笔工具,就是典型的贝塞尔曲线的应用,这里的一个网站可以在线模拟钢笔工具的使用:

http://bezier.method.ac/

贝塞尔曲线中有一些比较关键的名词,解释如下:

  • 数据点:通常指一条路径的起始点和终止点
  • 控制点:控制点决定了一条路径的弯曲轨迹,根据控制点的个数,贝塞尔曲线被分为一阶贝塞尔曲线(0个控制点)、二阶贝塞尔曲线(1个控制点)、三阶贝塞尔曲线(2个控制点)等等。

要想对贝塞尔曲线有一个比较好的认识,可以参考WIKI上的链接:

https://en.wikipedia.org/wiki/B%C3%A9zier_curve

贝塞尔曲线模拟

在Android中,一般来说,开发者只考虑二阶贝塞尔曲线和三阶贝塞尔曲线,SDK也只提供了二阶和三阶的API调用。对于再高阶的贝塞尔曲线,通常可以将曲线拆分成多个低阶的贝塞尔曲线,也就是所谓的降阶操作。下面将通过代码来模拟二阶和三阶的贝塞尔曲线是如何绘制和控制的。

贝塞尔曲线的一个比较好的动态演示如下所示:

http://myst729.github.io/bezier-curve/

二阶模拟

二阶贝塞尔曲线在Android中的API为:quadTo()和rQuadTo(),这两个API在原理上是可以互相转换的——quadTo是基于绝对坐标,而rQuadTo是基于相对坐标,所以后面我都只以其中一个来进行讲解。

先来看下最终的效果:

从前面的介绍可以知道,二阶贝塞尔曲线有两个数据点和一个控制点,只需要在代码中绘制出这些辅助点和辅助线即可,同时,控制点可以通过onTouchEvent来进行传递。

package com.xys.animationart.views;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;/*** 二阶贝塞尔曲线* <p/>* Created by xuyisheng on 16/7/11.*/
public class SecondOrderBezier extends View {private Paint mPaintBezier;private Paint mPaintAuxiliary;private Paint mPaintAuxiliaryText;private float mAuxiliaryX;private float mAuxiliaryY;private float mStartPointX;private float mStartPointY;private float mEndPointX;private float mEndPointY;private Path mPath = new Path();public SecondOrderBezier(Context context) {super(context);}public SecondOrderBezier(Context context, AttributeSet attrs) {super(context, attrs);mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintBezier.setStyle(Paint.Style.STROKE);mPaintBezier.setStrokeWidth(8);mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintAuxiliary.setStyle(Paint.Style.STROKE);mPaintAuxiliary.setStrokeWidth(2);mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);mPaintAuxiliaryText.setTextSize(20);}public SecondOrderBezier(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mStartPointX = w / 4;mStartPointY = h / 2 - 200;mEndPointX = w / 4 * 3;mEndPointY = h / 2 - 200;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPath.reset();mPath.moveTo(mStartPointX, mStartPointY);// 辅助点canvas.drawPoint(mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);canvas.drawText("控制点", mAuxiliaryX, mAuxiliaryY, mPaintAuxiliaryText);canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);// 辅助线canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryX, mAuxiliaryY, mPaintAuxiliary);// 二阶贝塞尔曲线mPath.quadTo(mAuxiliaryX, mAuxiliaryY, mEndPointX, mEndPointY);canvas.drawPath(mPath, mPaintBezier);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_MOVE:mAuxiliaryX = event.getX();mAuxiliaryY = event.getY();invalidate();}return true;}
}

三阶模拟

三阶贝塞尔曲线在Android中的API为:cubicTo()和rCubicTo(),这两个API在原理上是可以互相转换的——quadTo是基于绝对坐标,而rCubicTo是基于相对坐标,所以后面我都只以其中一个来进行讲解。

有了二阶的基础,再来模拟三阶就非常简单了,无非是增加了一个控制点而已,先看下效果图:

代码只需要在二阶的基础上添加一些辅助点即可,下面只给出一些关键代码,详细代码请参考Github:

    @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPath.reset();mPath.moveTo(mStartPointX, mStartPointY);// 辅助点canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);canvas.drawText("控制点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);canvas.drawText("控制点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);// 辅助线canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);// 三阶贝塞尔曲线mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);canvas.drawPath(mPath, mPaintBezier);}

模拟网页

如下所示的网页,模拟了三阶贝塞尔曲线的绘制,可以通过拖动曲线来获取两个控制点的坐标,而起始点分别是(0,0)和(1,1)。

http://cubic-bezier.com/

通过这个网页,也可以比较方便的获取三阶贝塞尔曲线的控制点坐标。

贝塞尔曲线应用

圆滑绘图

当在屏幕上绘制路径时,例如手写板,最基本的方法是通过Path.lineTo将各个触点连接起来,而这种方式在很多时候会发现,两个点的连接是非常生硬的,因为它毕竟是通过直线来连接的,如果通过二阶贝塞尔曲线来将各个触点连接,就会圆滑的多,不会出现太多的生硬连接。

先来看下代码,非常简单的绘制路径代码:

package com.xys.animationart.views;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;/*** 圆滑路径* <p/>* Created by xuyisheng on 16/7/19.*/
public class DrawPadBezier extends View {private float mX;private float mY;private float offset = ViewConfiguration.get(getContext()).getScaledTouchSlop();private Paint mPaint;private Path mPath;public DrawPadBezier(Context context) {super(context);}public DrawPadBezier(Context context, AttributeSet attrs) {super(context, attrs);mPath = new Path();mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setStyle(Paint.Style.STROKE);mPaint.setStrokeWidth(5);mPaint.setColor(Color.RED);}public DrawPadBezier(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:mPath.reset();float x = event.getX();float y = event.getY();mX = x;mY = y;mPath.moveTo(x, y);break;case MotionEvent.ACTION_MOVE:float x1 = event.getX();float y1 = event.getY();float preX = mX;float preY = mY;float dx = Math.abs(x1 - preX);float dy = Math.abs(y1 - preY);if (dx >= offset || dy >= offset) {// 贝塞尔曲线的控制点为起点和终点的中点float cX = (x1 + preX) / 2;float cY = (y1 + preY) / 2;
//                    mPath.quadTo(preX, preY, cX, cY);mPath.lineTo(x1, y1);mX = x1;mY = y1;}}invalidate();return true;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawPath(mPath, mPaint);}
}

先来看下通过mPath.lineTo来实现的绘图,效果如下所示:

图片中的拐点有明显的锯齿效果,即通过直线的连接,再来看下通过贝塞尔曲线来连接的效果,通常情况下,贝塞尔曲线的控制点取两个连续点的中点:

mPath.quadTo(preX, preY, cX, cY);

通过二阶贝塞尔曲线的连接效果如图所示:

可以明显的发现,曲线变得更加圆滑了。

曲线变形

通过控制贝塞尔曲线的控制点,就可以实现对一条路径的修改。所以,利用贝塞尔曲线,可以实现很多的路径动画,例如:

package com.xys.animationart;import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.BounceInterpolator;/*** 曲线变形* <p/>* Created by xuyisheng on 16/7/11.*/
public class PathMorphBezier extends View implements View.OnClickListener{private Paint mPaintBezier;private Paint mPaintAuxiliary;private Paint mPaintAuxiliaryText;private float mAuxiliaryOneX;private float mAuxiliaryOneY;private float mAuxiliaryTwoX;private float mAuxiliaryTwoY;private float mStartPointX;private float mStartPointY;private float mEndPointX;private float mEndPointY;private Path mPath = new Path();private ValueAnimator mAnimator;public PathMorphBezier(Context context) {super(context);}public PathMorphBezier(Context context, AttributeSet attrs) {super(context, attrs);mPaintBezier = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintBezier.setStyle(Paint.Style.STROKE);mPaintBezier.setStrokeWidth(8);mPaintAuxiliary = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintAuxiliary.setStyle(Paint.Style.STROKE);mPaintAuxiliary.setStrokeWidth(2);mPaintAuxiliaryText = new Paint(Paint.ANTI_ALIAS_FLAG);mPaintAuxiliaryText.setStyle(Paint.Style.STROKE);mPaintAuxiliaryText.setTextSize(20);setOnClickListener(this);}public PathMorphBezier(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mStartPointX = w / 4;mStartPointY = h / 2 - 200;mEndPointX = w / 4 * 3;mEndPointY = h / 2 - 200;mAuxiliaryOneX = mStartPointX;mAuxiliaryOneY = mStartPointY;mAuxiliaryTwoX = mEndPointX;mAuxiliaryTwoY = mEndPointY;mAnimator = ValueAnimator.ofFloat(mStartPointY, (float) h);mAnimator.setInterpolator(new BounceInterpolator());mAnimator.setDuration(1000);mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {mAuxiliaryOneY = (float) valueAnimator.getAnimatedValue();mAuxiliaryTwoY = (float) valueAnimator.getAnimatedValue();invalidate();}});}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPath.reset();mPath.moveTo(mStartPointX, mStartPointY);// 辅助点canvas.drawPoint(mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);canvas.drawText("辅助点1", mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliaryText);canvas.drawText("辅助点2", mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliaryText);canvas.drawText("起始点", mStartPointX, mStartPointY, mPaintAuxiliaryText);canvas.drawText("终止点", mEndPointX, mEndPointY, mPaintAuxiliaryText);// 辅助线canvas.drawLine(mStartPointX, mStartPointY, mAuxiliaryOneX, mAuxiliaryOneY, mPaintAuxiliary);canvas.drawLine(mEndPointX, mEndPointY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);canvas.drawLine(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mPaintAuxiliary);// 三阶贝塞尔曲线mPath.cubicTo(mAuxiliaryOneX, mAuxiliaryOneY, mAuxiliaryTwoX, mAuxiliaryTwoY, mEndPointX, mEndPointY);canvas.drawPath(mPath, mPaintBezier);}@Overridepublic void onClick(View view) {mAnimator.start();}
}

这里就是简单的改变二阶贝塞尔曲线的控制点来实现曲线的变形。

网上一些比较复杂的变形动画效果,也是基于这种实现方式,其原理都是通过改变控制点的位置,从而达到对图形的变换,例如圆形到心形的变化、圆形到五角星的变换,等等。

波浪效果

波浪的绘制是贝塞尔曲线一个非常简单的应用,而让波浪进行波动,其实并不需要对控制点进行改变,而是可以通过位移来实现,这里我们是借助贝塞尔曲线来实现波浪的绘制效果,效果如图所示:

package com.xys.animationart.views;import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.LinearInterpolator;/*** 波浪图形* <p/>* Created by xuyisheng on 16/7/11.*/
public class WaveBezier extends View implements View.OnClickListener {private Paint mPaint;private Path mPath;private int mWaveLength = 1000;private int mOffset;private int mScreenHeight;private int mScreenWidth;private int mWaveCount;private int mCenterY;public WaveBezier(Context context) {super(context);}public WaveBezier(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public WaveBezier(Context context, AttributeSet attrs) {super(context, attrs);mPath = new Path();mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setColor(Color.LTGRAY);mPaint.setStyle(Paint.Style.FILL_AND_STROKE);setOnClickListener(this);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mScreenHeight = h;mScreenWidth = w;mWaveCount = (int) Math.round(mScreenWidth / mWaveLength + 1.5);mCenterY = mScreenHeight / 2;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPath.reset();mPath.moveTo(-mWaveLength + mOffset, mCenterY);for (int i = 0; i < mWaveCount; i++) {// + (i * mWaveLength)// + mOffsetmPath.quadTo((-mWaveLength * 3 / 4) + (i * mWaveLength) + mOffset, mCenterY + 60, (-mWaveLength / 2) + (i * mWaveLength) + mOffset, mCenterY);mPath.quadTo((-mWaveLength / 4) + (i * mWaveLength) + mOffset, mCenterY - 60, i * mWaveLength + mOffset, mCenterY);}mPath.lineTo(mScreenWidth, mScreenHeight);mPath.lineTo(0, mScreenHeight);mPath.close();canvas.drawPath(mPath, mPaint);}@Overridepublic void onClick(View view) {ValueAnimator animator = ValueAnimator.ofInt(0, mWaveLength);animator.setDuration(1000);animator.setRepeatCount(ValueAnimator.INFINITE);animator.setInterpolator(new LinearInterpolator());animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mOffset = (int) animation.getAnimatedValue();postInvalidate();}});animator.start();}
}

波浪动画实际上并不复杂,但三角函数确实对一些开发者比较困难,开发者可以通过下面的这个网站来模拟三角函数图像的绘制:

https://www.desmos.com/calculator

路径动画

贝塞尔曲线的另一个非常常用的功能,就是作为动画的运动轨迹,让动画目标能够沿曲线平滑的实现移动动画,也就是让物体沿着贝塞尔曲线运动,而不是机械的直线,本例实现效果如下所示:

package com.xys.animationart.views;import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;import com.xys.animationart.evaluator.BezierEvaluator;/*** 贝塞尔路径动画* <p/>* Created by xuyisheng on 16/7/12.*/
public class PathBezier extends View implements View.OnClickListener {private Paint mPathPaint;private Paint mCirclePaint;private int mStartPointX;private int mStartPointY;private int mEndPointX;private int mEndPointY;private int mMovePointX;private int mMovePointY;private int mControlPointX;private int mControlPointY;private Path mPath;public PathBezier(Context context) {super(context);}public PathBezier(Context context, AttributeSet attrs) {super(context, attrs);mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPathPaint.setStyle(Paint.Style.STROKE);mPathPaint.setStrokeWidth(5);mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);mStartPointX = 100;mStartPointY = 100;mEndPointX = 600;mEndPointY = 600;mMovePointX = mStartPointX;mMovePointY = mStartPointY;mControlPointX = 500;mControlPointY = 0;mPath = new Path();setOnClickListener(this);}public PathBezier(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPath.reset();canvas.drawCircle(mStartPointX, mStartPointY, 30, mCirclePaint);canvas.drawCircle(mEndPointX, mEndPointY, 30, mCirclePaint);mPath.moveTo(mStartPointX, mStartPointY);mPath.quadTo(mControlPointX, mControlPointY, mEndPointX, mEndPointY);canvas.drawPath(mPath, mPathPaint);canvas.drawCircle(mMovePointX, mMovePointY, 30, mCirclePaint);}@Overridepublic void onClick(View view) {BezierEvaluator bezierEvaluator = new BezierEvaluator(new PointF(mControlPointX, mControlPointY));ValueAnimator anim = ValueAnimator.ofObject(bezierEvaluator,new PointF(mStartPointX, mStartPointY),new PointF(mEndPointX, mEndPointY));anim.setDuration(600);anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {PointF point = (PointF) valueAnimator.getAnimatedValue();mMovePointX = (int) point.x;mMovePointY = (int) point.y;invalidate();}});anim.setInterpolator(new AccelerateDecelerateInterpolator());anim.start();}
}

其中,用于改变运动点坐标的关键evaluator如下所示:

package com.xys.animationart.evaluator;import android.animation.TypeEvaluator;
import android.graphics.PointF;import com.xys.animationart.util.BezierUtil;public class BezierEvaluator implements TypeEvaluator<PointF> {private PointF mControlPoint;public BezierEvaluator(PointF controlPoint) {this.mControlPoint = controlPoint;}@Overridepublic PointF evaluate(float t, PointF startValue, PointF endValue) {return BezierUtil.CalculateBezierPointForQuadratic(t, startValue, mControlPoint, endValue);}
}

这里的TypeEvaluator计算用到了计算贝塞尔曲线上点的计算算法,这个会在后面继续讲解。

贝塞尔曲线进阶

求贝塞尔曲线上任意一点的坐标

求贝塞尔曲线上任意一点的坐标,这一过程,就是利用了De Casteljau算法。

http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/de-casteljau.html

利用这一算法,有开发者开发了一个演示多阶贝塞尔曲线的效果的App,其原理就是通过绘制贝塞尔曲线上的点来进行绘制的,地址如下所示:

https://github.com/venshine/BezierMaker

下面这篇文章就详细的讲解了该算法的应用,我的代码也从这里提取而来:

http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/

计算

有了公式,只需要代码实现就OK了,我们先写两个公式:

package com.xys.animationart.util;import android.graphics.PointF;/*** 计算贝塞尔曲线上的点坐标* <p/>* Created by xuyisheng on 16/7/13.*/
public class BezierUtil {/*** B(t) = (1 - t)^2 * P0 + 2t * (1 - t) * P1 + t^2 * P2, t ∈ [0,1]** @param t  曲线长度比例* @param p0 起始点* @param p1 控制点* @param p2 终止点* @return t对应的点*/public static PointF CalculateBezierPointForQuadratic(float t, PointF p0, PointF p1, PointF p2) {PointF point = new PointF();float temp = 1 - t;point.x = temp * temp * p0.x + 2 * t * temp * p1.x + t * t * p2.x;point.y = temp * temp * p0.y + 2 * t * temp * p1.y + t * t * p2.y;return point;}/*** B(t) = P0 * (1-t)^3 + 3 * P1 * t * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3, t ∈ [0,1]** @param t  曲线长度比例* @param p0 起始点* @param p1 控制点1* @param p2 控制点2* @param p3 终止点* @return t对应的点*/public static PointF CalculateBezierPointForCubic(float t, PointF p0, PointF p1, PointF p2, PointF p3) {PointF point = new PointF();float temp = 1 - t;point.x = p0.x * temp * temp * temp + 3 * p1.x * t * temp * temp + 3 * p2.x * t * t * temp + p3.x * t * t * t;point.y = p0.y * temp * temp * temp + 3 * p1.y * t * temp * temp + 3 * p2.y * t * t * temp + p3.y * t * t * t;return point;}
}

我们来将路径绘制到View中,看是否正确:

package com.xys.animationart.views;import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;import com.xys.animationart.util.BezierUtil;/*** 通过计算模拟二阶、三阶贝塞尔曲线* <p/>* Created by xuyisheng on 16/7/13.*/
public class CalculateBezierPointView extends View implements View.OnClickListener {private Paint mPaint;private ValueAnimator mAnimatorQuadratic;private ValueAnimator mAnimatorCubic;private PointF mPointQuadratic;private PointF mPointCubic;public CalculateBezierPointView(Context context) {super(context);}public CalculateBezierPointView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public CalculateBezierPointView(Context context, AttributeSet attrs) {super(context, attrs);mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mAnimatorQuadratic = ValueAnimator.ofFloat(0, 1);mAnimatorQuadratic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {PointF point = BezierUtil.CalculateBezierPointForQuadratic(valueAnimator.getAnimatedFraction(),new PointF(100, 100), new PointF(500, 100), new PointF(500, 500));mPointQuadratic.x = point.x;mPointQuadratic.y = point.y;invalidate();}});mAnimatorCubic = ValueAnimator.ofFloat(0, 1);mAnimatorCubic.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {PointF point = BezierUtil.CalculateBezierPointForCubic(valueAnimator.getAnimatedFraction(),new PointF(100, 600), new PointF(100, 1100), new PointF(500, 1000), new PointF(500, 600));mPointCubic.x = point.x;mPointCubic.y = point.y;invalidate();}});mPointQuadratic = new PointF();mPointQuadratic.x = 100;mPointQuadratic.y = 100;mPointCubic = new PointF();mPointCubic.x = 100;mPointCubic.y = 600;setOnClickListener(this);}@Overrideprotected void onDraw(final Canvas canvas) {super.onDraw(canvas);canvas.drawCircle(mPointQuadratic.x, mPointQuadratic.y, 10, mPaint);canvas.drawCircle(mPointCubic.x, mPointCubic.y, 10, mPaint);}@Overridepublic void onClick(View view) {AnimatorSet set = new AnimatorSet();set.playTogether(mAnimatorQuadratic, mAnimatorCubic);set.setDuration(2000);set.start();}
}

这次我们并没有通过API提供的贝塞尔曲线绘制方法来绘制二阶、三阶贝塞尔曲线,而是通过时间t和起始点来计算一条贝塞尔曲线上的所有点,可以发现,通过算法计算出来的点,与通过API所绘制出来的点,是完全吻合的。

贝塞尔曲线拟合计算

贝塞尔曲线有一个非常常用的动画效果——MetaBall算法。相信很多开发者都见过类似的动画,例如QQ的小红点消除,UC浏览器的下拉刷新loading等等。要做好这个动画,实际上最重要的就是通过贝塞尔曲线来拟合两个图形。

效果如图所示:

矩形拟合

我们来看一下拟合的原理,实际上就是通过贝塞尔曲线来连接两个圆上的四个点,当我们调整下画笔的填充方式,并绘制一些辅助线,我们来看具体是如何进行拟合的,如图所示:

可以发现,控制点为两圆圆心连线的中点,连接线为图中的这样一个矩形,当圆比较小时,这种通过矩形来拟合的方式几乎是没有问题的,但我们把圆放大,再来看下这种拟合,如图所示:

当圆的半径扩大之后,就可以非常明显的发现拟合的连接点与圆有一定相交的区域,这样的拟合效果就不好了,我们将画笔模式调整回来,如图所示:

所以,简单的矩形拟合,在圆半径小的时候,是可以的,但当圆半径变大之后,就需要更加严格的拟合了。

这里我们先来讲解下,如何计算矩形拟合的几个关键点。

从前面那张线图可以看出,标红的两个角是相等的,而这个角可以通过两个圆心的坐标来算出,有了这样一个角度,通过R x cos和 R x sin来计算矩形的一个顶点的坐标,类似的,其它坐标可求,关键代码如下所示:

private void metaBallVersion1(Canvas canvas) {float x = mCircleTwoX;float y = mCircleTwoY;float startX = mCircleOneX;float startY = mCircleOneY;float dx = x - startX;float dy = y - startY;double a = Math.atan(dx / dy);float offsetX = (float) (mCircleOneRadius * Math.cos(a));float offsetY = (float) (mCircleOneRadius * Math.sin(a));float x1 = startX + offsetX;float y1 = startY - offsetY;float x2 = x + offsetX;float y2 = y - offsetY;float x3 = x - offsetX;float y3 = y + offsetY;float x4 = startX - offsetX;float y4 = startY + offsetY;float controlX = (startX + x) / 2;float controlY = (startY + y) / 2;mPath.reset();mPath.moveTo(x1, y1);mPath.quadTo(controlX, controlY, x2, y2);mPath.lineTo(x3, y3);mPath.quadTo(controlX, controlY, x4, y4);mPath.lineTo(x1, y1);// 辅助线canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);canvas.drawLine(x1, y1, x2, y2, mPaint);canvas.drawLine(x3, y3, x4, y4, mPaint);canvas.drawCircle(controlX, controlY, 5, mPaint);canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);canvas.drawLine(x1, y1, x1, mCircleOneY, mPaint);canvas.drawPath(mPath, mPaint);}

切线拟合

如前面所说,矩形拟合在半径较小的情况下,是可以实现完美拟合的,而当半径变大后,就会出现贝塞尔曲线与圆相交的情况,导致拟合失败。

那么如何来实现完美的拟合呢?实际上,也就是说贝塞尔曲线与圆的连接点到贝塞尔曲线的控制点的连线,一定是圆的切线,这样的话,无论圆的半径如何变化,贝塞尔曲线一定是与圆拟合的,具体效果如图所示:

这时候我们把画笔模式调整回来看下填充效果,如图所示:

这样拟合是非常完美的。那么要如何来计算这些拟合的关键点呢?在前面的线图中,我标记出了两个角,这两个角分别可以求出,相减,就可以获取切点与圆心的夹角了,这样,通过R x cos和R x sin就可以求出切点的坐标了。

其中,小的角可以通过两个圆心的坐标来求出,而大的角,可以通过直角三角形(圆心、切点、控制点)来求出,即控制点到圆心的距离/半径。

关键代码如下所示:

private void metaBallVersion2(Canvas canvas) {float x = mCircleTwoX;float y = mCircleTwoY;float startX = mCircleOneX;float startY = mCircleOneY;float controlX = (startX + x) / 2;float controlY = (startY + y) / 2;float distance = (float) Math.sqrt((controlX - startX) * (controlX - startX) + (controlY - startY) * (controlY - startY));double a = Math.acos(mRadiusNormal / distance);double b = Math.acos((controlX - startX) / distance);float offsetX1 = (float) (mRadiusNormal * Math.cos(a - b));float offsetY1 = (float) (mRadiusNormal * Math.sin(a - b));float tanX1 = startX + offsetX1;float tanY1 = startY - offsetY1;double c = Math.acos((controlY - startY) / distance);float offsetX2 = (float) (mRadiusNormal * Math.sin(a - c));float offsetY2 = (float) (mRadiusNormal * Math.cos(a - c));float tanX2 = startX - offsetX2;float tanY2 = startY + offsetY2;double d = Math.acos((y - controlY) / distance);float offsetX3 = (float) (mRadiusNormal * Math.sin(a - d));float offsetY3 = (float) (mRadiusNormal * Math.cos(a - d));float tanX3 = x + offsetX3;float tanY3 = y - offsetY3;double e = Math.acos((x - controlX) / distance);float offsetX4 = (float) (mRadiusNormal * Math.cos(a - e));float offsetY4 = (float) (mRadiusNormal * Math.sin(a - e));float tanX4 = x - offsetX4;float tanY4 = y + offsetY4;mPath.reset();mPath.moveTo(tanX1, tanY1);mPath.quadTo(controlX, controlY, tanX3, tanY3);mPath.lineTo(tanX4, tanY4);mPath.quadTo(controlX, controlY, tanX2, tanY2);canvas.drawPath(mPath, mPaint);// 辅助线canvas.drawCircle(tanX1, tanY1, 5, mPaint);canvas.drawCircle(tanX2, tanY2, 5, mPaint);canvas.drawCircle(tanX3, tanY3, 5, mPaint);canvas.drawCircle(tanX4, tanY4, 5, mPaint);canvas.drawLine(mCircleOneX, mCircleOneY, mCircleTwoX, mCircleTwoY, mPaint);canvas.drawLine(0, mCircleOneY, mCircleOneX + mRadiusNormal + 400, mCircleOneY, mPaint);canvas.drawLine(mCircleOneX, 0, mCircleOneX, mCircleOneY + mRadiusNormal + 50, mPaint);canvas.drawLine(mCircleTwoX, mCircleTwoY, mCircleTwoX, 0, mPaint);canvas.drawCircle(controlX, controlY, 5, mPaint);canvas.drawLine(startX, startY, tanX1, tanY1, mPaint);canvas.drawLine(tanX1, tanY1, controlX, controlY, mPaint);}

圆的拟合

贝塞尔曲线做动画,很多时候都需要使用到圆的特效,而通过二阶、三阶贝塞尔曲线来拟合圆,也不是一个非常简单的事情,所以,我直接把结论拿出来了,具体的算法地址如下所示:

http://spencermortensen.com/articles/bezier-circle/

http://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves

有了贝塞尔曲线的控制点,再对其实现动画,就非常简单了,与之前的动画没有太大的区别。

源代码

本次的讲解代码已经全部上传到Github :

https://github.com/xuyisheng/BezierArt

欢迎大家提issue。

贝塞尔曲线开发的艺术相关推荐

  1. Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发鼻涕下拉粘连效果

    前言 接着上一期 Android仿苹果版QQ下拉刷新实现(一) --打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果: 下面上一下本章需要实现的效果图: 大家看到这个效 ...

  2. Android开发之使用贝塞尔曲线实现黏性水珠下拉效果

    Android开发之使用贝塞尔曲线实现黏性水珠下拉效果 标签: 贝塞尔曲线 简介 网上关于贝塞尔曲线的博客和教程很多,通常讲到的三点确定一条曲线:起点,终点,辅助点. 常见的贝塞尔黏性效果 常见的各阶 ...

  3. Android开发 之 曲线运动动画(贝塞尔曲线)

    曲线运动动画(贝塞尔曲线) 贝塞尔曲线:维基百科中这样说到:在数学的数值分析领域中,贝塞尔曲线(英语:Bézier curve)是计算机图形学中相当重要的参数曲线.更高维度的广泛化贝塞尔曲线就称作贝塞 ...

  4. dotween曲线运动 unity_【Unity3d游戏开发】游戏中的贝塞尔曲线以及其在Unity中的实现...

    RT,马三最近在参与一款足球游戏的开发,其中涉及到足球的各种运动轨迹和路径,比如射门的轨迹,高吊球,香蕉球的轨迹.最早的版本中马三是使用物理引擎加力的方式实现的足球各种运动,后来的版本中使用了根据物理 ...

  5. iOS开发 贝塞尔曲线UIBezierPath

    2019独角兽企业重金招聘Python工程师标准>>> UIBezierPath基础 UIBezierPath对象是CGPathRef数据类型的封装.每一个直线段或者曲线段的结束的地 ...

  6. iOS开发 贝塞尔曲线UIBezierPath(后记)

    使用CAShapeLayer与UIBezierPath可以实现不在view的drawRect方法中就画出一些想要的图形 . 1:UIBezierPath: UIBezierPath是在 UIKit 中 ...

  7. Android开发——贝塞尔曲线解析

    相信很多同学都知道"贝塞尔曲线"这个词,我们在很多地方都能经常看到.利用"贝塞尔曲线"可以做出很多好看的UI效果,本篇博客就让我们一起学习"贝塞尔曲线 ...

  8. 音视频开发之旅(40)-贝塞尔曲线和曲面

    目录 贝塞尔曲线基本知识 画贝塞尔曲线 让曲线动起来 画贝塞尔曲面 资料 收获 本篇最终实现效果如下: 篇外说明:由于有必要学习使用下kotlin,后续的java层代码实现尽量采用kotlin 一.贝 ...

  9. 自定义View合辑(8)-跳跃的小球(贝塞尔曲线)

    为了加强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,所有的代码也都会开源,也希望读者能给个 star 哈 GitH ...

最新文章

  1. 字符串补充知识及列表类型
  2. Rera1N环境Linux,降级工具ReRa1n发布,降级真的来了?
  3. 【Android RTMP】音频数据采集编码 ( FAAC 编码器编码 AAC 音频解码信息 | 封装 RTMP 音频数据头 | 设置 AAC 音频数据类型 | 封装 RTMP 数据包 )
  4. spring cloud构建互联网分布式微服务云平台-docker部署spring cloud项目
  5. 开发效率提升15倍!批流融合实时平台在好未来的应用实践
  6. C++ Primer 7.33 练习编写成员函数
  7. Swift - 将String类型的数字转换成数字类型
  8. 设计模式之——静态代理模式
  9. php 除数 保留两位小数,c语言除法怎么保留小数
  10. miui系统精简工具(免root删除内置软件)
  11. android建脚本,Android Studio Gradle构建脚本(示例代码)
  12. 每天都使用微信语音,但你必须晓得互联网语音协议(VoIP)
  13. 卫星遥感—地块/边界提取相关论文
  14. 【大厂面试】智力题怎么破?
  15. 楚留香服务器维护时间,2019年8月9日官方维护公告
  16. 痛与快乐有一个代码是什么_痛苦与快乐
  17. Postman使用newman命令执行
  18. java线程知识总结
  19. BLE Peripheral设备的名字显示错误
  20. 哈希存储、哈希表原理

热门文章

  1. docker 安装svn
  2. pyhton 反转单词顺序
  3. sparkstreaming监听hdfs目录如何终止_Spark笔试题:Spark Streaming 反压机制
  4. matlab/simulink PMSM电机DTC控制
  5. php json 转 xml格式,PHP中如何将JSON文件转XML格式
  6. python中global的用法
  7. concat函数java_MySql中CONCAT(str1,str2,...)函数
  8. linux abs 和 屏幕 大小关系,观点|一个 Linux 菜鸟的回忆录
  9. Dubbo 源码分析 - 服务导出
  10. 深入理解MySQL的外连接、内连接、交叉连接