—— 前言
—— 什么是贝塞尔曲线
—— 贝塞尔曲线的分类
—— 贝塞尔曲线代码实现
—— 贝塞尔曲线的应用
前言:
从去年开始了解贝塞尔曲线之后,发现开发中,不管是Android/Ios平台,还是web前端等,都有贝塞尔曲线的应用,通过绘制贝塞尔曲线,可以帮助开发者实现很多效果,例如一段时间内很流行的粘合型的下拉刷新、又如天气曲线图,同时,以贝塞尔曲线为基础的贝塞尔工具是所有绘图软件的最常用最实用的工具。
什么是贝塞尔曲线:
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。一般的矢量图形软件通过它来精确画出曲线,贝兹曲线由线段与节点组成,节点是可拖动的支点,线段像可伸缩的皮筋,我们在绘图工具上看到的钢笔工具就是来做这种矢量曲线的。主要结构:起始点、终止点(也称锚点)、控制点。通过调整控制点,贝塞尔曲线的形状会发生变化。
贝塞尔曲线的分类:
一阶贝塞尔曲线(线段):
公式:
意义:由 P0 至 P1 的连续点, 描述的一条线段
二阶贝塞尔曲线(抛物线):
公式:
原理: 由 P0 至 P1 的连续点 Q0,描述一条线段。
由 P1 至 P2 的连续点 Q1,描述一条线段。
由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。
三阶贝塞尔曲线:
当然还有四阶曲线、五阶曲线......
一篇帮助理解贝塞尔曲线的文章:http://www.html-js.com/article/1628
一个帮助绘制贝塞尔曲线的网址:http://bezier.method.ac/#
贝塞尔曲线代码实现:
我们一般使用的是二阶贝塞尔曲线和三阶贝塞尔曲线,从动态图和公式我们可以看出,贝塞尔曲线主要由于三个部分控制:起点,终点,中间的辅助控制点。如何利用这三个点画出贝塞尔曲线,在android自带的Path类中自带了方法,可以帮助我们实现贝塞尔曲线:
/** * Add a quadratic bezier from the last point, approaching control point * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for * this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the control point on a quadratic curve * @param y1 The y-coordinate of the control point on a quadratic curve * @param x2 The x-coordinate of the end point on a quadratic curve * @param y2 The y-coordinate of the end point on a quadratic curve */ public void quadTo(float x1, float y1, float x2, float y2) { isSimplePath = false; native_quadTo(mNativePath, x1, y1, x2, y2); }
quadTo()方法从上一个点为起点开始绘制贝塞尔曲线,其中(x1,y1)为辅助控制点,(x2,y2)为终点。
Path mPath = new Path();
mPath.moveTo(x0,y0);
mPath.quadTo(x1,y1,x2,y2);
如调用以上代码,即绘制起点(x0,y0),终点(x2,y2),辅助控制点(x1,y1)的贝塞尔曲线。因此,通过不断改变这三个点的位置,我们可以绘制出各种各样的曲线
/** * Add a cubic bezier from the last point, approaching control points * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been * made for this contour, the first point is automatically set to (0,0). * * @param x1 The x-coordinate of the 1st control point on a cubic curve * @param y1 The y-coordinate of the 1st control point on a cubic curve * @param x2 The x-coordinate of the 2nd control point on a cubic curve * @param y2 The y-coordinate of the 2nd control point on a cubic curve * @param x3 The x-coordinate of the end point on a cubic curve * @param y3 The y-coordinate of the end point on a cubic curve */ public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { isSimplePath = false; native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3); }
cubicTo()方法从上一个点为起点开始绘制三阶贝塞尔曲线,其中(x1,y1),( x2, y2 )为辅助控制点,(x3,y3)为终点。
贝塞尔曲线的应用
(1)二阶贝塞尔曲线——波浪
要实现一个波浪不断涌动的效果,这种效果在很多手机应用中可见,例如手机电量,内存剩余等。我们需要绘制带有平滑自然效果的曲线,这时候就需要贝塞尔曲线来辅助了。
动态图:
原理图:
图中的矩阵即为视图的可见范围,也就是我们手机常见的区域。通过属性动画类ValueAnimator不断改变点1的横坐标,随着点1横坐标向右移动,点2,点3,点4,点5,以及四个控制点的坐标随着点1的移动同时位移相同距离,每一次坐标点更新,我们调用一次invalidate()方法,调用draw重新绘制视图,绘制四段贝塞尔曲线。最后点1移动到原先点3的位置,这样就完成了一次动画。
这样,通过循环不断的动画效果,我们就实现了波浪的效果。
onDraw() 代码:
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!mIsRunning || !mHasInit) return; mPath.reset(); mPath.moveTo(mLeft1.x, mLeft1.y); mPath.quadTo(mControlLeft1.x, mControlLeft1.y, mLeft2.x, mLeft2.y); mPath.quadTo(mControlLeft2.x, mControlLeft2.y, mFirst.x, mFirst.y); mPath.quadTo(mControlFirst.x, mControlFirst.y, mSecond.x, mSecond.y); mPath.quadTo(mControlSecond.x, mControlSecond.y, mRight.x, mRight.y); mPath.lineTo(mRight.x, mHeight); mPath.lineTo(mLeft1.x, mHeight); mPath.lineTo(mLeft1.x, mLeft1.y); canvas.drawPath(mPath, mPaint); }
(2)二阶贝塞尔曲线——粘连体
利用二阶贝塞尔曲线还可以实现,类似两种物体粘合在一起的效果,比如我们常用的qq,在qq聊天列表上有一个非常有意思的功能,就是当我们用手指移动聊天列表上的未读消息标志的时候,它与聊天列表会产生粘连的效果:
现在,我们来模拟这种效果,利用学到的二阶贝塞尔曲线来绘制。
我们看到原理图,基本构造为两个圆,和两端贝塞尔曲线,绘制贝塞尔曲线,由于这是一个二阶的贝塞尔曲线,我们只需要一个控制点,在这个图里,我们的两条贝塞尔曲线的两个控制点分别为(x1,y1)(x4, y4)的中点,(x2, y2)(x3, y3)的中点。
从图中可以看出,我们的贝塞尔曲线由我们的控制点控制,控制点又是被起点和终点控制着,因此,当两个圆距离越大,曲线越趋于平缓,当两个圆距离越小,曲线的波动度越大,这样,我们想要的粘连的效果就实现了。另外,这里有一个还有角度(图中的45度角)可以用来控制,也可以作为控制曲线波动度的参数。
通过以上分析,我们通过一个方法来绘制两个圆之间的粘连体路径:
/** * 画粘连体 * @param cx1 圆心x1 * @param cy1 圆心y1 * @param r1 圆半径r1 * @param offset1 贝塞尔曲线偏移角度offset1 * @param cx2 圆心x2 * @param cy2 圆心y2 * @param r2 圆半径r2 * @param offset2 贝塞尔曲线偏移角度offset2 * @return */ public static Path drawAdhesionBody(float cx1, float cy1, float r1, float offset1, float cx2, float cy2, float r2, float offset2) { /* 求三角函数 */ float degrees =(float) Math.toDegrees(Math.atan(Math.abs(cy2 - cy1) / Math.abs(cx2 - cx1))); /* 根据圆1与圆2的相对位置求四个点 */ float differenceX = cx1 - cx2; float differenceY = cy1 - cy2; /* 两条贝塞尔曲线的四个端点 */ float x1,y1,x2,y2,x3,y3,x4,y4; /* 圆1在圆2的下边 */ if (differenceX == 0 && differenceY > 0) { x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2)); y2 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2)); x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2)); y4 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2)); x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1)); y1 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1)); x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1)); y3 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1)); } /* 圆1在圆2的上边 */ else if (differenceX == 0 && differenceY < 0) { x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2)); y2 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2)); x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2)); y4 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2)); x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1)); y1 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1)); x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1)); y3 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1)); } /* 圆1在圆2的右边 */ else if (differenceX > 0 && differenceY == 0) { x2 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2)); y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2)); x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2)); y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2)); x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1)); y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1)); x3 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1)); y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1)); } /* 圆1在圆2的左边 */ else if (differenceX < 0 && differenceY == 0 ) { x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2)); y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2)); x4 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2)); y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2)); x1 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1)); y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1)); x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1)); y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1)); } /* 圆1在圆2的右下角 */ else if (differenceX > 0 && differenceY > 0) { x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees)); y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees)); x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2)); y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2)); x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1)); y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1)); x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees)); y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees)); } /* 圆1在圆2的左上角 */ else if (differenceX < 0 && differenceY < 0) { x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2)); y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2)); x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees)); y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees)); x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees)); y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees)); x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1)); y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(degrees - offset1)); } /* 圆1在圆2的左下角 */ else if (differenceX < 0 && differenceY > 0) { x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2)); y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2)); x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees)); y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees)); x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees)); y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees)); x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1)); y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1)); } /* 圆1在圆2的右上角 */ else { x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees)); y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees)); x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2)); y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2)); x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1)); y1 = cy1 + r1* (float) Math.sin(Math.toRadians(degrees - offset1)); x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees)); y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees)); } /* 贝塞尔曲线的控制点 */ float anchorX1,anchorY1,anchorX2,anchorY2; /* 圆1大于圆2 */ if (r1 > r2) { anchorX1 = (x2 + x3) / 2; anchorY1 = (y2 + y3) / 2; anchorX2 = (x1 + x4) / 2; anchorY2 = (y1 + y4) / 2; } /* 圆1小于或等于圆2 */ else { anchorX1 = (x1 + x4) / 2; anchorY1 = (y1 + y4) / 2; anchorX2 = (x2 + x3) / 2; anchorY2 = (y2 + y3) / 2; } /* 画粘连体 */ Path path = new Path(); path.reset(); path.moveTo(x1, y1); path.quadTo(anchorX1, anchorY1, x2, y2); path.lineTo(x4, y4); path.quadTo(anchorX2, anchorY2, x3, y3); path.lineTo(x1, y1); return path; }
再来看仿QQ聊天列表的粘连效果,我们已经实现了粘连体的绘制,接下来,我们需要实现以上的基本效果,我们给控件设置一个粘连的最大距离,即如果两个圆之间的距离超过这个值,则不再绘制粘连体。
好了,我们看效果图:
粘连体除了在类似QQ上这种效果,其实还可以做很多事,比如,如果我们用它来实现一个加载页面的效果呢。
(3)三阶贝塞尔曲线——弹性球
三阶贝塞尔曲线,就是有两个控制点,公式太复杂,我也不是很理解,不过通过之前给出的那个网址来理解,还是比较好明白的,它相比二阶曲线的优点是,由于控制点的增加,它能够更加轻松地绘制出更平滑更自然的曲线。
先来看一个web前端的效果:
真的是很酷炫......
如何绘制类似这种,看起来具有弹性球的滑动球,我们需要使用三阶贝塞尔曲线,那么首先如何用三阶贝塞尔曲线绘制出一个圆,这里有一篇文章,是关于如何用贝塞尔曲线绘制圆:http://spencermortensen.com/articles/bezier-circle/ ,大概意思是讲,我们需要一个值就是c = 0.552284749,如下图,要绘制右上角的圆弧,我们需要两个控制点,其中B就是一个控制点,我们需要保证AB = c *r,即可以画出1/4的圆弧,以此类推,连续画四段这样的圆弧,就可以画出一个标准的圆。
接下来我们观察弹性球的运动,大概可以分为以下几个阶段:
1)开始启动,此时右边点位移,其他点不动
2)开始加速
3)减速
4)到达终点
5)回弹效果
弹性球代码:
package com.zero.bezier.widget.elastic; import android.animation.Animator; import android.animation.ValueAnimator; import android.graphics.Path; import android.graphics.PointF; import android.view.animation.AccelerateDecelerateInterpolator; /** * 弹性球 * @author linzewu * @date 2016/6/1 */ public class ElasticBall 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 int mDirection; /** * 动画完成百分比(0~1) */ private float mAnimPercent; /** * 弹性距离 */ private float mElasticDistance; /** * 弹性比例 */ private float mElasticPercent = 0.8f; /** * 位移距离 */ private float mMoveDistance; /** * 动画消费时间 */ private long mDuration = 1500; /** * 偏移值 */ private float offsetTop, offsetBottom, offsetLeft, offsetRight; /** * 圆形偏移比例 */ private float c = 0.551915024494f; private float c2 = 0.65f; /** * 动画开始点 */ private Ball mStartPoint; /** * 动画结束点 */ private Ball mEndPoint; /** * 构造方法 * * @param x 圆心横坐标 * @param y 圆心纵坐标 * @param radius 圆半径 */ public ElasticBall(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 ElasticBall(endPoint.x, endPoint.y, radius); this.mStartPoint = new ElasticBall(x, y, radius); this.mStatusPoint1 = new ElasticBall(x, y, radius); this.mStatusPoint2 = new ElasticBall(x, y, radius); this.mStatusPoint3 = new ElasticBall(x, y, radius); this.mStatusPoint4 = new ElasticBall(x, y, radius); this.mStatusPoint5 = new ElasticBall(x, y, radius); this.mElasticBallInterface = elasticBallInterface; judgeDirection(); 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() { @Override public 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(drawElasticCircle(topX, topY, offsetTop, offsetTop, bottomX, bottomY, offsetBottom, offsetBottom, leftX, leftY, offsetLeft, offsetLeft, rightX, rightY, offsetRight, offsetRight)); } } }); valueAnimator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (mElasticBallInterface != null) { mElasticBallInterface.onFinish(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); } private void judgeDirection() { 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 drawElasticCircle( 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)); } }
上面完成了一个弹性球的封装,可以实现四个方向的运动,然后我们实现一个弹性球的loader:
贝塞尔曲线还有很多应用的地方,或者说在各个领域都有。
去年开始在黄同学的影响下,慢慢地去实现一些利用贝塞尔曲线实现的效果,源码有相当一部分代码也是来自于黄同学,也得益于网络上大多数技术博客无私的分享,希望自己能够通过学习这样一个开发的绘图曲线,有所提高。
参考:
http://www.jianshu.com/p/791d3a791ec2
http://spencermortensen.com/articles/bezier-circle/
源码: