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

贝塞尔曲线的原理

贝塞尔曲线是用一系列点来控制曲线状态的,这些点简单分为两类:

类型 作用
数据点 确定曲线的起始和结束位置
控制点 确定曲线的弯曲程度

一阶贝塞尔曲线
一阶曲线是没有控制点的,仅有两个数据点(A 和 B),最终效果一个线段。
动态过程可以参照下图(贝塞尔曲线相关的动态演示图片来自维基百科)。

一阶曲线其实就是lineTo方法。

二阶贝塞尔曲线
在平面内任选 3 个不共线的点,依次用线段连接。

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

连接这两点 DE。

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE = AD:AB = BE:BC。

到这里,我们就确定了贝塞尔曲线上的一个点 F。接下来,请稍微回想一下中学所学的极限知识,让选取的点 D 在第一条线段上从起点 A 移动到终点 B,找出所有的贝塞尔曲线上的点 F。所有的点找出来之后,我们也得到了这条贝塞尔曲线。

动态过程如下:

三阶贝塞尔曲线
控制点个数为 4 时,就是三阶的曲线

步骤都是相同的,只不过我们每确定一个贝塞尔曲线上的点,要进行三轮取点操作。如图,AE:AB = BF:BC = CG:CD = EH:EF = FI:FG = HJ:HI,其中点 J 就是最终得到的贝塞尔曲线上的一个点。

这样我们得到的是一条三次贝塞尔曲线。

动态图如下:

三阶曲线对应的方法是cubicTo

要绘制更复杂的曲线,控制点的增加也仅仅是线性的。这一特点使其不光在工业设计领域大展拳脚,就连数学基础不好的人也可以比较容易地掌握,比如大多数平面美术设计师们。

学习贝塞尔曲线函数

一阶曲线是一条线段,非常简单,不再进行介绍,都是path的基本用法。

二阶曲线:
首先,两个数据点是控制贝塞尔曲线开始和结束的位置,而控制点则是控制贝塞尔的弯曲状态

从上面的动态图可以看出,贝塞尔曲线在动态变化过程中有类似于橡皮筋一样的弹性效果,因此在制作一些弹性效果的时候很常用。

代码如下:

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);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);centerX = w/2;centerY = h/2;// 初始化数据点和控制点的位置start.x = centerX-200;start.y = centerY;end.x = centerX+200;end.y = centerY;control.x = centerX;control.y = centerY-100;}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 根据触摸位置更新控制点,并提示重绘control.x = event.getX();control.y = event.getY();invalidate();return true;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);// 绘制数据点和控制点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);}
}

三阶曲线:
三阶曲线由两个数据点和两个控制点来控制曲线状态。

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;}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);centerX = w / 2;centerY = h / 2;// 初始化数据点和控制点的位置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;}@Overridepublic boolean onTouchEvent(MotionEvent event) {// 根据触摸位置更新控制点,并提示重绘if (mode) {control1.x = event.getX();control1.y = event.getY();} else {control2.x = event.getX();control2.y = event.getY();}invalidate();return true;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//drawCoordinateSystem(canvas);// 绘制数据点和控制点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);}
}

三阶曲线相比于二阶曲线可以制作更加复杂的形状,但是对于高阶的曲线,用低阶的曲线组合也可达到相同的效果,就是传说中的降阶。因此我们对贝塞尔曲线的封装方法一般最高只到三阶曲线。

降阶与升阶

类型 释义 变化
降阶 在保持曲线形状与方向不变的情况下,减少控制点数量,即降低曲线阶数 方法变得简单,数据点变多,控制点可能减少,灵活性变弱
升阶 在保持曲线形状与方向不变的情况下,增加控制点数量,即升高曲线阶数 方法更加复杂,数据点不变,控制点增加,灵活性变强

贝塞尔曲线实例

一般使用贝塞尔曲线的情况如下:

序号 内容 用例
1 事先不知道曲线状态,需要实时计算时 方天气预报气温变化的平滑折线图
2 显示状态会根据用户操作改变时 QQ小红点,仿真翻书效果
3 一些比较复杂的运动状态(配合PathMeasure使用) 复杂运动状态的动画效果

至于只需要一个静态的曲线图形的情况,用图片岂不是更好,大量的计算会很不划算。

如果是显示SVG矢量图的话,已经有相关的解析工具了(内部依旧运用的有贝塞尔曲线),不需要手动计算。

贝塞尔曲线的主要优点是可以实时控制曲线状态,并可以通过改变控制点的状态实时让曲线进行平滑的状态变化。

QQ红点的实现效果

qq的红点去除效果,其实就是用了两条贝塞尔曲线。

基本理论:只要在拖动的时候 去改变辅助点的Y,和固定圆的半径, 就可以出来效果。

创建画笔

mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setColor(Color.RED);

绘制动圆和固定圆

 /*** 固定圆  并且初始化*/private PointF mFixedCircle = new PointF(150f, 150f);/*** 固定圆的半径*/float mFixedRadius = 14f;/*** 动圆  并且初始化*/private PointF mDragCircle = new PointF(80f, 80f);/*** 动圆半径*/float mDragRadius = 20f;/*** 动圆两个焦点的坐标*/private PointF[] mDragPoints;/*** 固定圆的两个焦点坐标*/private PointF[] mFixedPoints;/*** 控制焦点*/private PointF mControlPoint;

获取两个圆之间的距离:

/*** 获取临时的固定圆的半径** @return*/private float getTempFiexdCircle() {//获取到两个圆心之间的距离float instance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);//这个是在连个圆之间的实际距离和我们定义的距离之间取得最小值instance = Math.min(instance, farestDistance);//0.0f--->1.0f>>>>>1.0f---》0.0ffloat percent = instance / farestDistance;return evaluate(percent, mFixedRadius, mFixedRadius * 0.2);}/*** 估值器** @param fraction* @param startValue* @param endValue* @return*/public Float evaluate(float fraction, Number startValue, Number endValue) {float startFloat = startValue.floatValue();return startFloat + fraction * (endValue.floatValue() - startFloat);}

onDraw()方法绘制圆

//根据两个圆的圆心的距离获取固定圆的半径float distance = getTempFiexdCircle();//计算连接部分//1、获取直线与圆的焦点float yOffset = mFixedCircle.y - mDragCircle.y;float xOffset = mFixedCircle.x - mDragCircle.x;/*** 获取斜率*/Double lineK = null;if (xOffset != 0) {lineK = (double) yOffset / xOffset;}//通过几何工具获取焦点坐标this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);//2、获取控制点坐标this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);//绘制动圆canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);//画一个固定圆//canvas.drawCircle(150f,150f,14f,mPaint);canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);//canvas.drawCircle(150f,150f,14f,mPaint);canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);//画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分Path path = new Path();//跳到某个点1path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);//画曲线 1--->2path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);//画直线2---->3path.lineTo(mDragPoints[1].x, mDragPoints[1].y);//画曲线3---->4path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);path.close();canvas.drawPath(path, mPaint);//恢复画布canvas.restore();

处理onTouch()方法,让红点随手势动起来

 @Overridepublic boolean onTouchEvent(MotionEvent event) {float x = 0;float y = 0;switch (event.getAction()) {case MotionEvent.ACTION_DOWN://获取到按下的时候的坐标(因为我们已经把画布往上移动了状态栏的高度了,或者是我们在这里做判断)x = event.getRawX();y = event.getRawY();//更新动圆的坐标updataDragCircle(x, y);break;case MotionEvent.ACTION_MOVE://移动的时候获取坐标x = event.getRawX();y = event.getRawY();updataDragCircle(x, y);//处理断开float distance = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);if (distance > farestDistance) {  //如果获取到的距离大于我们定义的最大的距离isOutToRange = true;  //断开设置为trueinvalidate();  //重绘}break;case MotionEvent.ACTION_UP:if (isOutToRange) {  //如果是断开isOutToRange = false;  //设置为false//处理断开float d = GeometryUtil.getDistanceBetween2Points(mDragCircle, mFixedCircle);if (d > farestDistance) {// * a、拖拽超出范围,断开-->松手-->消失//松手还没有放回去isDisappear = true;//重绘一下invalidate();} else {//    * b、拖拽超出范围,断开---->放回去了--->恢复updataDragCircle(mFixedCircle.x, mFixedCircle.y);isDisappear = false;}} else {final PointF tempDragCircle = new PointF(mDragCircle.x, mDragCircle.y);//    * c、拖拽没有超出范围,断开--->恢复final ValueAnimator mAnim = ValueAnimator.ofFloat(1.0f);mAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {float percent = mAnim.getAnimatedFraction();PointF p = GeometryUtil.getPointByPercent(tempDragCircle, mFixedCircle, percent);updataDragCircle(p.x, p.y);}});//差之器,这个是设置弹性的mAnim.setInterpolator(new OvershootInterpolator(4));mAnim.setDuration(500);mAnim.start();}break;}return true;}/*** 更新拖拽圆的圆心坐标** @param rawX* @param rawY*/private void updataDragCircle(float rawX, float rawY) {//更新的坐标mDragCircle.set(rawX, rawY);invalidate();}

修改onDraw()判断
isOutToRange和isDisappear分别为true和false的情况

@Overrideprotected void onDraw(Canvas canvas) {//保持当前画布的状态canvas.save();//移动画布canvas.translate(0, -mStatusBarHeight);//根据两个圆的圆心的距离获取固定圆的半径float distance = getTempFiexdCircle();//计算连接部分//1、获取直线与圆的焦点float yOffset = mFixedCircle.y - mDragCircle.y;float xOffset = mFixedCircle.x - mDragCircle.x;/*** 获取斜率*/Double lineK = null;if (xOffset != 0) {lineK = (double) yOffset / xOffset;}//通过几何工具获取焦点坐标this.mFixedPoints = GeometryUtil.getIntersectionPoints(mFixedCircle, distance, lineK);this.mDragPoints = GeometryUtil.getIntersectionPoints(mDragCircle, mDragRadius, lineK);//2、获取控制点坐标this.mControlPoint = GeometryUtil.getMiddlePoint(mDragCircle, mFixedCircle);if (!isDisappear) {//画拖拽圆//canvas.drawCircle(80f,80f,20f,mPaint);canvas.drawCircle(mDragCircle.x, mDragCircle.y, mDragRadius, mPaint);if (!isOutToRange) {//画一个固定圆//canvas.drawCircle(150f,150f,14f,mPaint);canvas.drawCircle(mFixedCircle.x, mFixedCircle.y, distance, mPaint);//画连接部分   这个是用的那个贝塞尔曲线绘制的连接部分Path path = new Path();//跳到某个点1path.moveTo(mFixedPoints[0].x, mFixedPoints[0].y);//画曲线 1--->2path.quadTo(mControlPoint.x, mControlPoint.y, mDragPoints[0].x, mDragPoints[0].y);//画直线2---->3path.lineTo(mDragPoints[1].x, mDragPoints[1].y);//画曲线3---->4path.quadTo(mControlPoint.x, mControlPoint.y, mFixedPoints[1].x, mFixedPoints[1].y);path.close();canvas.drawPath(path, mPaint);}}//恢复canvas.restore();}

漂浮的心

漂浮轨迹就是一条三阶贝塞尔曲线,结合属性动画中的估值器进行设置。

首先定义一个属性动画的估值器

public class BezierEvaluator implements TypeEvaluator<PointF> {private PointF mControlP1;private PointF mControlP2;public BezierEvaluator(PointF controlP1, PointF controlP2) {this.mControlP1 = controlP1;this.mControlP2 = controlP2;}@Overridepublic PointF evaluate(float time, PointF start, PointF end) {float timeLeft = 1.0f - time;PointF point = new PointF();point.x = timeLeft * timeLeft * timeLeft * (start.x) + 3 * timeLeft * timeLeft * time *(mControlP1.x) + 3 * timeLeft * time *time * (mControlP2.x) + time * time * time * (end.x);point.y = timeLeft * timeLeft * timeLeft * (start.y) + 3 * timeLeft * timeLeft * time *(mControlP1.y) + 3 * timeLeft * time *time * (mControlP2.y) + time * time * time * (end.y);return point;}
}

之后自定义一个view可以生成爱心,添加透明度,缩放等动画和根据贝塞尔曲线改变其位置的属性动画。
初始化爱心图片和多个插值器等,到时随即选取

private void init() {// 初始化显示的图片drawables = new Drawable[3];drawables[0] = getResources().getDrawable(R.drawable.red);drawables[1] = getResources().getDrawable(R.drawable.yellow);drawables[2] = getResources().getDrawable(R.drawable.green);// 初始化插补器mInterpolators = new Interpolator[4];mInterpolators[0] = new LinearInterpolator();// 线性mInterpolators[1] = new AccelerateInterpolator();// 加速mInterpolators[2] = new DecelerateInterpolator();// 减速mInterpolators[3] = new AccelerateDecelerateInterpolator();// 先加速后减速// 底部 并且 水平居中dWidth = drawables[0].getIntrinsicWidth();dHeight = drawables[0].getIntrinsicHeight();lp = new LayoutParams(dWidth, dHeight);lp.addRule(CENTER_HORIZONTAL, TRUE);// 这里的TRUE 要注意 不是truelp.addRule(ALIGN_PARENT_BOTTOM, TRUE);}

入场动画

private AnimatorSet getEnterAnimator(final View target) {ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);AnimatorSet enter = new AnimatorSet();enter.setTarget(target);enter.setInterpolator(new LinearInterpolator());enter.setDuration(500).playTogether(alpha, scaleX, scaleY);return enter;}

贝塞尔曲线动画

private ValueAnimator getBezierValueAnimator(final View target) {// 初始化贝塞尔估值器BezierEvaluator evaluator = new BezierEvaluator(getPointF(2), getPointF(1));// 起点在底部中心位置,终点在底部随机一个位置ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF((mWidth - dWidth) /2, mHeight - dHeight), new PointF(random.nextInt(getWidth()), 0));animator.setTarget(target);animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator valueAnimator) {// 这里获取到贝塞尔曲线计算出来的的x y值 赋值给view 这样就能让爱心随着曲线走啦PointF pointF = (PointF) valueAnimator.getAnimatedValue();target.setX(pointF.x);target.setY(pointF.y);// alpha动画target.setAlpha(1 - valueAnimator.getAnimatedFraction());}});animator.setDuration(3000);return animator;}

结合动画添加爱心

public void addHeart() {final ImageView imageView = new ImageView(getContext());// 随机选一个爱心imageView.setImageDrawable(drawables[random.nextInt(3)]);imageView.setLayoutParams(lp);addView(imageView);AnimatorSet finalSet = new AnimatorSet();AnimatorSet enterAnimatorSet = getEnterAnimator(imageView);//入场动画ValueAnimator bezierValueAnimator = getBezierValueAnimator(imageView);//贝塞尔曲线路径动画finalSet.playSequentially(enterAnimatorSet, bezierValueAnimator);finalSet.setInterpolator(mInterpolators[random.nextInt(4)]);finalSet.setTarget(imageView);finalSet.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {super.onAnimationEnd(animation);removeView((imageView));//删除爱心}});finalSet.start();}

弹性的圆

还有一个实例,就是特别出名的弹性的圆

将这个圆的动画效果拆解开看的画,可以分为5个状态。

这个动画效果的实现就是不同状态之间的转化加上水平位移的实现。

我们需要先了解一下如何用贝塞尔曲线画一个圆,因为我的做法是通过贝塞尔曲线来实现的。

就是所需要的数值c约等于0.551915024494f,具体可以参考这篇文章,http://spencermortensen.com/articles/bezier-circle/,那么这个c的值的作用,就是把图中的1理解为圆的半径,那么对应的另外个值就应该是半径乘以0.551915024494f。

坐标轴也就是Android中的坐标轴了,如果我们打算用贝塞尔曲线来画这么一个圆的话,我们需要知道这个圆的半径,以及图中的M的值,知道这两个值的话就能够知道图中12个点的坐标,知道坐标就能够用Path的cubicTo方法来使用贝塞尔曲线画出圆了。

public class BezierDemo3 extends View {private static final float C = 0.551915024494f;     // 一个常量,用来计算绘制圆形贝塞尔曲线控制点的位置private Paint mPaint;private int mCenterX, mCenterY;private PointF mCenter = new PointF(0,0);private float mCircleRadius = 200;                  // 圆的半径private float mDifference = mCircleRadius*C;        // 圆形的控制点与数据点的差值private float[] mData = new float[8];               // 顺时针记录绘制圆形的四个数据点private float[] mCtrl = new float[16];              // 顺时针记录绘制圆形的八个控制点private float mDuration = 1000;                     // 变化总时长private float mCurrent = 0;                         // 当前已进行时长private float mCount = 100;                         // 将时长总共划分多少份private float mPiece = mDuration/mCount;            // 每一份的时长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);// 初始化数据点mData[0] = 0;mData[1] = mCircleRadius;mData[2] = mCircleRadius;mData[3] = 0;mData[4] = 0;mData[5] = -mCircleRadius;mData[6] = -mCircleRadius;mData[7] = 0;// 初始化控制点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];}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);mCenterX = w / 2;mCenterY = h / 2;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);drawCoordinateSystem(canvas);       // 绘制坐标系canvas.translate(mCenterX, mCenterY); // 将坐标系移动到画布中央canvas.scale(1,-1);                 // 翻转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) {// 绘制数据点和控制点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);}// 绘制坐标系private void drawCoordinateSystem(Canvas canvas) {canvas.save();                      // 绘制做坐标系canvas.translate(mCenterX, mCenterY); // 将坐标系移动到画布中央canvas.scale(1,-1);                 // 翻转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();}
}

这样我们就知道如何使用贝塞尔曲线来绘制一个圆了。也就是状态1和状态5我们都会绘制了,接下来看看状态2如何绘制。

状态2其实就是把右边的点向右移动点距离

状态3的实现就是在状态2的基础上修改了个值,一个是M的值加大,让圆看起来跟肥一点,还有就是圈住的那些点向右移动,做到居中。

实现如下:

public class Ball {/*** 圆心横坐标*/public float x;/*** 圆心纵坐标*/public float y;/*** 半径*/public float radius;/*** 构造方法* @param x* @param y* @param radius*/public Ball(float x, float y, float radius) {this.x = x;this.y = y;this.radius = radius;this.topX = x;this.topY = y - radius;this.bottomX = x;this.bottomY = y + radius;this.leftX = x - radius;this.leftY = y;this.rightX = x + radius;this.rightY = y;}public void refresh(float x, float y, float topX, float topY, float bottomX, float bottomY,float leftX, float leftY, float rightX, float rightY){this.x = x;this.y = y;this.topX = topX;this.topY = topY;this.bottomX = bottomX;this.bottomY = bottomY;this.leftX = leftX;this.leftY = leftY;this.rightX = rightX;this.rightY = rightY;}/*** 球左边点的坐标*/public float leftX;public float leftY;/*** 球右边点的坐标*/public float rightX;public float rightY;/*** 球顶点的坐标*/public float topX;public float topY;/*** 球底部点的坐标*/public float bottomX;public float bottomY;
}
public class MagicBall extends Ball {/*** 向上运动*/private static final int DIRECTION_UP = 1;/*** 向下运动*/private static final int DIRECTION_DOWN = 2;/*** 向左运动*/private static final int DIRECTION_LEFT = 3;/*** 向右运动*/private static final int DIRECTION_RIGHT = 4;/*** 动画消费时间*/private long mDuration = 1200;/*** 偏移值*/private float offsetTop, offsetBottom, offsetLeft, offsetRight;/*** 运动方向*/private int mDirection;/*** 动画完成百分比(0~1)*/private float mAnimPercent;/*** 弹性距离*/private float mElasticDistance;/*** 弹性比例*/private float mElasticPercent = 0.8f;/*** 位移距离*/private float mMoveDistance;/*** 圆形偏移比例*/private float c = 0.551915024494f;private float c2 = 0.65f;/*** 动画开始点*/private Ball mStartPoint;/*** 动画结束点*/private Ball mEndPoint;/*** 构造方法** @param x 圆心横坐标* @param y 圆心纵坐标* @param radius 圆半径*/public MagicBall(float x, float y, float radius) {super(x, y, radius);init();}private void init() {mElasticDistance = mElasticPercent * radius;offsetTop = c * radius;offsetBottom = c * radius;offsetLeft = c * radius;offsetRight = c * radius;}public interface ElasticBallInterface{void onChange(Path path);void onFinish();}private ElasticBallInterface mElasticBallInterface;/*** 对外公布方法,设置弹性比例 (0~1)* @param elasticPercent*/public void setElasticPercent(float elasticPercent) {}/*** 对外公布方法,设置动画时间* @param duration*/public void setDuration(long duration) {this.mDuration = duration;}/*** 对外公布方法, 开启动画* @param endPoint*/public void startElasticAnim(PointF endPoint, ElasticBallInterface elasticBallInterface) {this.mEndPoint = new MagicBall(endPoint.x, endPoint.y, radius);this.mStartPoint = new MagicBall(x, y, radius);this.mStatusPoint1 = new MagicBall(x, y, radius);this.mStatusPoint2 = new MagicBall(x, y, radius);this.mStatusPoint3 = new MagicBall(x, y, radius);this.mStatusPoint4 = new MagicBall(x, y, radius);this.mStatusPoint5 = new MagicBall(x, y, radius);this.mElasticBallInterface = elasticBallInterface;calculateDirection();mMoveDistance = getDistance(mStartPoint.x, mStatusPoint1.y, endPoint.x, endPoint.y);animStatus0();ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);valueAnimator.setDuration(mDuration);valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());valueAnimator.start();valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {mAnimPercent = (float) animation.getAnimatedValue();if(mAnimPercent>=0 && mAnimPercent <= 0.2){animStatus1();}else if(mAnimPercent > 0.2 && mAnimPercent <= 0.5){animStatus2();}else if(mAnimPercent > 0.5 && mAnimPercent <= 0.8){animStatus3();}else if(mAnimPercent > 0.8 && mAnimPercent <= 0.9){animStatus4();}else if(mAnimPercent > 0.9&&mAnimPercent <= 1){animStatus5();}if (mElasticBallInterface != null) {mElasticBallInterface.onChange(drawMagicCircle(topX, topY, offsetTop, offsetTop,bottomX, bottomY, offsetBottom, offsetBottom,leftX, leftY, offsetLeft, offsetLeft,rightX, rightY, offsetRight, offsetRight));}}});valueAnimator.addListener(new Animator.AnimatorListener() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic void onAnimationEnd(Animator animation) {if (mElasticBallInterface != null) {mElasticBallInterface.onFinish();}}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic void onAnimationRepeat(Animator animation) {}});}private void calculateDirection() {if (mEndPoint.x - mStartPoint.x > 0) {mDirection = DIRECTION_RIGHT;}else if (mEndPoint.x - mStartPoint.x < 0) {mDirection = DIRECTION_LEFT;}else if (mEndPoint.y - mStartPoint.x > 0) {mDirection = DIRECTION_DOWN;}else if (mEndPoint.y - mStartPoint.y < 0){mDirection = DIRECTION_UP;}}/*** 动画状态0 (初始状态:圆形)*/private void animStatus0() {offsetTop = c * radius;offsetBottom = c * radius;offsetLeft = c * radius;offsetRight = c * radius;}private Ball mStatusPoint1;/*** 动画状态1 (0~0.2)*/private void animStatus1() {float percent = mAnimPercent * 5f;if (mDirection == DIRECTION_LEFT) {leftX = mStartPoint.leftX - percent * mElasticDistance;} else if (mDirection == DIRECTION_RIGHT) {rightX = mStartPoint.rightX + percent * mElasticDistance;} else if (mDirection == DIRECTION_UP) {topY = mStartPoint.topY - percent * mElasticDistance;} else if (mDirection == DIRECTION_DOWN) {bottomY = mStartPoint.bottomY + percent * mElasticDistance;}mStatusPoint1.refresh(x, y, topX, topY, bottomX, bottomY,leftX, leftY, rightX, rightY);}private Ball mStatusPoint2;/*** 动画状态2 (0.2~0.5)*/private void animStatus2() {float percent = (float) ((mAnimPercent - 0.2) * (10f / 3));if (mDirection == DIRECTION_LEFT) {leftX = mStatusPoint1.leftX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );x = mStatusPoint1.x - percent * (mMoveDistance / 2);rightX = mStatusPoint1.rightX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );topX = x;bottomX = x;//偏移值稍作变化offsetTop = radius * c + radius * ( c2 - c ) * percent;offsetBottom = radius * c + radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_RIGHT) {rightX = mStatusPoint1.rightX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );x = mStatusPoint1.x + percent * (mMoveDistance / 2);leftX = mStatusPoint1.leftX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );topX = x;bottomX = x;//偏移值稍作变化offsetTop = radius * c + radius * ( c2 - c ) * percent;offsetBottom = radius * c + radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_UP) {topY = mStatusPoint1.topY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );y = mStatusPoint1.y - percent * (mMoveDistance / 2);bottomY = mStatusPoint1.bottomY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );leftY = y;rightY = y;//偏移值稍作变化offsetLeft = radius * c + radius * ( c2 - c ) * percent;offsetRight = radius * c + radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_DOWN) {bottomY = mStatusPoint1.bottomY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );y = mStatusPoint1.y + percent * (mMoveDistance / 2);topY = mStatusPoint1.topY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );leftY = y;rightY = y;//偏移值稍作变化offsetLeft = radius * c + radius * ( c2 - c ) * percent;offsetRight = radius * c + radius * ( c2 - c ) * percent;}mStatusPoint2.refresh(x, y, topX, topY, bottomX, bottomY,leftX, leftY, rightX, rightY);}private Ball mStatusPoint3;/*** 动画状态3 (0.5~0.8)*/private void animStatus3() {float percent = (mAnimPercent - 0.5f) * (10f / 3f);if (mDirection == DIRECTION_LEFT) {leftX = mStatusPoint2.leftX - Math.abs(percent * (mEndPoint.rightX - mStatusPoint2.rightX));x = mStatusPoint2.x - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));rightX = mStatusPoint2.rightX - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));topX = x;bottomX = x;//偏移值稍作变化offsetTop = radius * c2 - radius * ( c2 - c ) * percent;offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_RIGHT) {rightX = mStatusPoint2.rightX + percent * (mEndPoint.rightX - mStatusPoint2.rightX);x = mStatusPoint2.x + percent * (mEndPoint.x - mStatusPoint2.x);leftX = mStatusPoint2.leftX + percent * (mEndPoint.x - mStatusPoint2.x);topX = x;bottomX = x;//偏移值稍作变化offsetTop = radius * c2 - radius * ( c2 - c ) * percent;offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_UP) {topY = mStatusPoint2.topY - Math.abs(percent * (mEndPoint.topY - mStatusPoint2.topY));y = mStatusPoint2.y - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));bottomY = mStatusPoint2.bottomY - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));leftY = y;rightY = y;//偏移值稍作变化offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;offsetRight = radius * c2 - radius * ( c2 - c ) * percent;} else if (mDirection == DIRECTION_DOWN) {bottomY = mStatusPoint2.bottomY + percent * (mEndPoint.bottomY - mStatusPoint2.bottomY);y = mStatusPoint2.y + percent * (mEndPoint.y - mStatusPoint2.y);topY = mStatusPoint2.topY + percent * (mEndPoint.y - mStatusPoint2.y);leftY = y;rightY = y;//偏移值稍作变化offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;offsetRight = radius * c2 - radius * ( c2 - c ) * percent;}mStatusPoint3.refresh(x, y, topX, topY, bottomX, bottomY,leftX, leftY, rightX, rightY);}private Ball mStatusPoint4;/*** 动画状态4 (0.8~0.9)*/private void animStatus4() {float percent = (float) (mAnimPercent - 0.8) * 10;if (mDirection == DIRECTION_LEFT) {rightX = mStatusPoint3.rightX - percent * (Math.abs(mEndPoint.rightX - mStatusPoint3.rightX) + mElasticDistance/2);//再做一次赋值,防止和终点不重合leftX = mEndPoint.leftX;x = mEndPoint.x;bottomX = mEndPoint.bottomX;topX = mEndPoint.topX;} else if (mDirection == DIRECTION_RIGHT) {leftX = mStatusPoint3.leftX + percent * (mEndPoint.leftX - mStatusPoint3.leftX +mElasticDistance/2);//再做一次赋值,防止和终点不重合rightX = mEndPoint.rightX;x = mEndPoint.x;bottomX = mEndPoint.bottomX;topX = mEndPoint.topX;} else if (mDirection == DIRECTION_UP) {bottomY = mStatusPoint3.bottomY - percent * (Math.abs(mEndPoint.bottomY - mStatusPoint3.bottomY) + mElasticDistance/2);//再做一次赋值,防止和终点不重合topY = mEndPoint.topY;y = mEndPoint.y;leftY = mEndPoint.leftY;rightY = mEndPoint.rightY;} else if (mDirection == DIRECTION_DOWN) {topY = mStatusPoint3.topY + percent * (mEndPoint.topY - mStatusPoint3.topY + mElasticDistance/2);//再做一次赋值,防止和终点不重合bottomY = mEndPoint.bottomY;y = mEndPoint.y;leftY = mEndPoint.leftY;rightY = mEndPoint.rightY;}mStatusPoint4.refresh(x, y, topX, topY, bottomX, bottomY,leftX, leftY, rightX, rightY);}private Ball mStatusPoint5;/*** 动画状态5 (0.9~1)回弹*/private void animStatus5() {float percent = (float) (mAnimPercent - 0.9) * 10;if (mDirection == DIRECTION_LEFT) {rightX = mStatusPoint4.rightX + percent * (mEndPoint.rightX - mStatusPoint4.rightX);} else if (mDirection == DIRECTION_RIGHT) {leftX = mStatusPoint4.leftX + percent * (mEndPoint.leftX - mStatusPoint4.leftX);} else if (mDirection == DIRECTION_UP) {bottomY = mStatusPoint4.bottomY + percent * (mEndPoint.bottomY - mStatusPoint4.bottomY);} else if (mDirection == DIRECTION_DOWN) {topY = mStatusPoint4.topY + percent * (mEndPoint.topY - mStatusPoint4.topY);}mStatusPoint5.refresh(x, y, topX, topY, bottomX, bottomY,leftX, leftY, rightX, rightY);}/*** 绘制弹性圆* 通过绘制四段三阶贝塞尔曲线,来实现有弹性变化的圆* @param topX* @param topY* @param offsetTop1* @param offsetTop2* @param bottomX* @param bottomY* @param offsetBottom1* @param offsetBottom2* @param leftX* @param leftY* @param offsetLeft1* @param offsetLeft2* @param rightX* @param rightY* @param offsetRight1* @param offsetRight2* @return*/private Path drawMagicCircle(float topX, float topY, float offsetTop1, float offsetTop2,float bottomX, float bottomY, float offsetBottom1, float offsetBottom2,float leftX, float leftY, float offsetLeft1, float offsetLeft2,float rightX, float rightY, float offsetRight1, float offsetRight2) {/*** 绘制每一段三阶贝塞尔曲线需要两个控制点*/PointF controlTop1, controlTop2, controlBottom1, controlBottom2,controlLeft1, controlLeft2, controlRight1, controlRight2;controlTop1 = new PointF();controlTop1.x = topX - offsetTop1;controlTop1.y = topY;controlTop2 = new PointF();controlTop2.x = topX + offsetTop2;controlTop2.y = topY;controlBottom1 = new PointF();controlBottom1.x = bottomX - offsetBottom1;controlBottom1.y = bottomY;controlBottom2 = new PointF();controlBottom2.x = bottomX + offsetBottom2;controlBottom2.y = bottomY;controlLeft1 = new PointF();controlLeft1.x = leftX;controlLeft1.y = leftY - offsetLeft1;controlLeft2 = new PointF();controlLeft2.x = leftX;controlLeft2.y = leftY + offsetLeft2;controlRight1 = new PointF();controlRight1.x = rightX;controlRight1.y = rightY - offsetRight1;controlRight2 = new PointF();controlRight2.x = rightX;controlRight2.y = rightY + offsetRight2;Path path = new Path();/*** 绘制top到left的圆弧*/path.moveTo(topX, topY);path.cubicTo(controlTop1.x, controlTop1.y, controlLeft1.x, controlLeft1.y, leftX, leftY);/*** 绘制left到bottom的圆弧*/path.cubicTo(controlLeft2.x ,controlLeft2.y, controlBottom1.x, controlBottom1.y, bottomX,bottomY);/*** 绘制bottom到right的圆弧*/path.cubicTo(controlBottom2.x, controlBottom2.y, controlRight2.x, controlRight2.y,rightX, rightY);/*** 绘制right到top的圆弧*/path.cubicTo(controlRight1.x, controlRight1.y, controlTop2.x, controlTop2.y, topX, topY);return path;}/*** 求两点之间的距离* @param x1 第一个点的横坐标* @param y1 第一个点的纵坐标* @param x2 第二个点的横坐标* @param y2 第二个点的纵坐标* @return 两点距离*/private float getDistance(float x1, float y1, float x2, float y2) {return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));}}

Android 贝塞尔曲线解析相关推荐

  1. android贝塞尔曲线,一文解析 Android 贝塞尔曲线

    原标题:一文解析 Android 贝塞尔曲线 相信很多同学都知道"贝塞尔曲线"这个词,我们在很多地方都能经常看到.利用"贝塞尔曲线"可以做出很多好看的UI效果, ...

  2. Android 贝塞尔曲线实战之网易云音乐鲸云特效,2021程序员进阶宝典

    一阶这个比较简单,因为没有在网上找到可以直接输入数学公式的工具,就手工推导了下. ![在这里插入图片描述](https://img- <Android学习笔记总结+最新移动架构视频+大厂安卓面试 ...

  3. Android 贝塞尔曲线实战之网易云音乐鲸云特效,apm性能监控系统

    小说阅读 APP 的翻页效果. 简介 ===================================================================== 在开始实战之前,我们还 ...

  4. Android 贝塞尔曲线实战之网易云音乐鲸云特效

    作者:哈哈将 -个推 Android 高级开发工程师 前言 APP开发市场已经告别"野蛮生长"时代,人们不再满足于APP外形创新,而将目光转向全方面的用户体验上.在这过程中,动效化 ...

  5. android动态波浪效果,android贝塞尔曲线实现波浪效果

    本文实例为大家分享了android贝塞尔曲线实现波浪效果的具体代码,供大家参考,具体内容如下 因为手机录制gif不知道下什么软件好,所以暂时就先忽略效果图了 我在屏幕外多画了1.5个波浪,延伸至屏幕内 ...

  6. android波浪动画简书,Android贝塞尔曲线————波浪效果(大波浪)

    Hello大家好,很高兴又一次与大家见面,今天是农历丁酉鸡年(大年初四),现在跟大家拜年有点晚,算是拜晚年,祝大家晚年幸福. 这么快大伙都到了晚年了,Android贝塞尔曲线我也准备以一个大波浪来结束 ...

  7. android圆形贝塞尔 菜单,Android 贝塞尔曲线——圆渐变心

    大家好!我是一名执着的Android开发攻城狮,第一次写简书,没有写好的希望大家多多包涵,万事开头难,从去年开始我就想写点自己的东西,但是一直没有写下去的勇气和毅力,希望这是我一个好的习惯开始.在这我 ...

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

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

  9. android 贝塞尔曲线点击区域,白话经典贝塞尔曲线及其在 Android 中的应用

    一.前言 谈到贝塞尔曲线可能不少人会浮现它高大上的数学公式.然而,在实际应用中,并不需要我们去完全理解或者推导出公式才能应用得上.实际情况是,即使真的只是一个学渣,我们应该也能很轻松的掌握贝塞尔曲线的 ...

最新文章

  1. 三栏布局(两边固定,中间自适应)
  2. Java语言实现插值查找
  3. python odoo_odoo开发学习 -- Python2 or Python3 ?
  4. Python使用openpyxl和pandas处理学生成绩Excel文件实用案例
  5. 使用GeoTools创建一个具有纬度,经度和半径的圆
  6. VISSIM二次开发(Python)大作业总结2
  7. 单片机万年历阴阳历c语言,自己制作的单片机万年历 程序+原理图
  8. php是一种通用开源,caozha-admin(PHP网站后台管理框架)
  9. 如何在html中加入下划线,文档中加入下划线
  10. 巧用Excel按照某一列进行整体排序
  11. 读《期货市场计算机分析指南》笔记
  12. 初学原生ajax(1):原生AJAX的简单介绍与使用
  13. CMD窗口下进入PowerShell和退出PowerShell
  14. 统一告警平台设计方案
  15. Understanding parameters:理解参数(Parameter)
  16. 动态选择屏幕(FREE_SELECTIONS_INIT)
  17. µGo语言实现——从头开发迷你Go语言编译器
  18. 关于 两个 datetime 列的差别导致了运行时溢出
  19. java计算年龄_JAVA根据生日计算年龄(年月日)
  20. 诙谐有趣的《UVM实战》笔记——第二章 一个简单的UVM验证平台

热门文章

  1. 2021智博会圆满落幕,全迹科技载誉而归
  2. 再无需从头训练迁移学习模型!亚马逊开源迁移学习数据库 Xfer
  3. 贝壳采集器:西江传媒网 数据采集
  4. CUDA sample volumeRender
  5. 中国糖尿病诊断市场深度研究分析报告
  6. 刚刚,中国移动在中国移动大会上发布《中国移动算力网络白皮书》!
  7. 缺少移动驾驶舱构建的经验?这家银行清算中心这样选择
  8. JavaWeb 数据源总结
  9. 细粒度,图片上传,POI技术
  10. Android插件化原理解析