贝塞尔曲线实为实现动画的一大利器,下面举个例子演示贝塞尔曲线的使用实例。人生第一篇博客,不足之处望大家果断指出。最终效果如下。
前戏:
new一个空的activity和一个java class,使java class继承View,然后在activity布局中引用View,代码如下。
public class Demo6View extends View { public Demo7View(Context context, AttributeSet attrs) { super(context, attrs); } }
<com.sleepless.animhomework.View.Demo6View android:layout_width="match_parent" android:layout_height="match_parent" />
正餐:
前戏做完,就要进入正题了,View的编写。
1.首先我们先定义一些全局变量,这些都用得到,具体用处下文解释。
private static final float C = 0.551915024494f; private Paint mPaint;//画笔 private int mRadiusBig = 100, mRadiusSmall = (int) (mRadiusBig*0.77f), mWidth, mHeight, mMimWidth = (int) (mRadiusSmall * 2 * 3)/*fill view mim width*/; private ValueAnimator mValueAnimator; private Path mPathBezier,mPathCircle,mPathBezier2; private float mFraction,mFractionDegree; private float[] mPointData = new float[8];// 4个数据点 顺时针排序,从左边开始 private float[] mPointCtrl = new float[16];// 8个控制点
2.在构造方法中初始化这些变量,我们使用一个valueAnimator来控制整个动画得运行。
public Demo6View(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setStyle(Paint.Style.FILL); mPaint.setColor(Color.BLACK); mPathBezier=new Path(); mPathBezier2=new Path(); mPathCircle=new Path(); mValueAnimator = ValueAnimator.ofFloat(0, 1, 0); mValueAnimator.setDuration(3000); mValueAnimator.setRepeatCount(Integer.MAX_VALUE); mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mFraction = (float) animation.getAnimatedValue(); mFractionDegree = animation.getAnimatedFraction(); invalidate(); } }); }
3.重写onMeasure()方法,从而更好的控制绘图的大小和位置。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 为了能够更好的控制绘制的大小和位置,当然,初学者写死也是可以的 super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = MeasureSpec.getSize(widthMeasureSpec); mHeight = MeasureSpec.getSize(heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (widthMode != MeasureSpec.AT_MOST && heightMode != MeasureSpec.AT_MOST) { if (mWidth < mMimWidth) mWidth = mMimWidth; if (mHeight < mMimWidth) mHeight = mMimWidth; } else if (widthMeasureSpec != MeasureSpec.AT_MOST) { if (mWidth < mMimWidth) mWidth = mMimWidth; } else if (heightMeasureSpec != MeasureSpec.AT_MOST) { if (mHeight < mMimWidth) mHeight = mMimWidth; } setMeasuredDimension(mWidth, mHeight); }
4.重写onDraw()方法,这也是关键所在,下面我重点解释整个过程。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(mWidth / 2, mHeight / 2); canvas.scale(1, -1); canvas.rotate(-360 * mFractionDegree); if (mFraction < (1 / 4f)) { setCirclePath(); canvas.drawPath(mPathCircle, mPaint); } else if(mFraction<(5/6f)){ setBezierPath(); canvas.drawPath(mPathBezier, mPaint); canvas.drawPath(mPathBezier2, mPaint); canvas.drawCircle(-mRadiusSmall-(mFraction-1 / 3f)*mRadiusBig*2-0.17f*mRadiusSmall, 0, mRadiusSmall,mPaint); canvas.drawCircle(mRadiusSmall+(mFraction-1 / 3f)*mRadiusBig*2+0.17f*mRadiusSmall, 0, mRadiusSmall,mPaint); }else{ canvas.drawCircle(-mRadiusSmall-(mFraction-1 / 3f)*mRadiusBig*2-0.17f*mRadiusSmall, 0, mRadiusSmall,mPaint); canvas.drawCircle(mRadiusSmall+(mFraction-1 / 3f)*mRadiusBig*2+0.17f*mRadiusSmall, 0, mRadiusSmall,mPaint); } }
(1)当进度小于四分之一时我们调用setCirclePath()方法,然后画出轨迹,setCirclePath()方法如下。
代码中mPointData[ ]是贝塞尔曲线的初始点和终点,mPointData[ ]是控制点。贝塞尔曲线是有起点、控制点、终点决定的一条曲线。我们首先使用的四条三阶的贝塞尔曲线围成一个圆形,大概的位置就是,第一条贝塞尔曲线在坐标在第四象限,第二条在第一象限,第三条在第二象限。。。
第一条贝塞尔曲线的起点在x轴上左侧,终点在y轴上,两个控制点中,第一个控制点x坐标和起点x坐标相同,y坐标是终点坐标的C倍(C是贝塞尔拟合参数),第二个控制点x坐标是起点x坐标的C倍,y坐标和终点y坐标相同。其他各条以此类推。
然后随着mFraction进度的变化,各条贝塞尔曲线的值都在变化,第一条的起点在向左侧移动,终点在向下移动,控制点一的纵坐标写死,横坐标随起点的移动而移动,控制点二的横坐标与起点横坐标的差值相同随起点移动而移动,纵坐标写死。其他各条贝塞尔曲线以此类推。
这样我们就看到一个圆形左右两边往外扩展,上下向里凹陷。
private void setCirclePath() { mPointData[0] = -mRadiusBig-mFraction*mRadiusBig*2; mPointData[1] = 0; mPointData[2] = 0; mPointData[3] = -mRadiusBig+mFraction*mRadiusBig*1.5f; mPointData[4] = -mPointData[0]; mPointData[5] = 0; mPointData[6] = 0; mPointData[7] = -mPointData[3]; mPointCtrl[0] = mPointData[0];// x轴一样 mPointCtrl[1] = -mRadiusBig * C;// y轴向下的 mPointCtrl[2] = mPointData[0]+mRadiusBig * (1-C); mPointCtrl[3] = -mRadiusBig;// y轴一样 mPointCtrl[4] = -mPointCtrl[2]; mPointCtrl[5] = mPointCtrl[3]; mPointCtrl[6] = -mPointCtrl[0]; mPointCtrl[7] = mPointCtrl[1]; mPointCtrl[8] = mPointCtrl[6]; mPointCtrl[9] = -mPointCtrl[7]; mPointCtrl[10] = mPointCtrl[4]; mPointCtrl[11] = -mPointCtrl[5]; mPointCtrl[12] = mPointCtrl[2]; mPointCtrl[13] = -mPointCtrl[3]; mPointCtrl[14] = mPointData[0]; mPointCtrl[15] = -mPointCtrl[1]; mPathCircle.reset(); mPathCircle.moveTo(mPointData[0], mPointData[1]); mPathCircle.cubicTo(mPointCtrl[0], mPointCtrl[1], mPointCtrl[2], mPointCtrl[3], mPointData[2], mPointData[3]); mPathCircle.cubicTo(mPointCtrl[4], mPointCtrl[5], mPointCtrl[6], mPointCtrl[7], mPointData[4], mPointData[5]); mPathCircle.cubicTo(mPointCtrl[8], mPointCtrl[9], mPointCtrl[10], mPointCtrl[11], mPointData[6], mPointData[7]); mPathCircle.cubicTo(mPointCtrl[12], mPointCtrl[13], mPointCtrl[14], mPointCtrl[15], mPointData[0], mPointData[1]); }
(2)当进度大于四分之一后我们调用setBezierPath()方法,画出四个二阶的贝塞尔曲线,然后画出两个圆形,代码如下。
我们可以看到源码中,两个圆形的坐标同样是向外移动的,我们需要保证两个圆形的左右和原来大圆的左右重合,并且以相同速度在移动,我在两个圆形之间加了一段小距离会看着过度更加自然。
然后我们重点说藏在圆形后面的四个二阶贝塞尔曲线,是他们使两个圆形看起来藕断丝连。
这四个贝塞尔曲线写成了左右两组,我们看左边的,贝塞尔曲线起点的位置在左边圆形的顶部位置,随着圆形的移动而移动,当进度大于三分之二后使其向圆心移动,这样可以使两球的连接变细。贝塞尔曲线终点的位置是前一个阶段第一个三阶贝塞尔曲线的终点,并且保持原来的速度继续向里凹陷,凹陷到一定程度后纵坐标保持不变,当进度大于四分之三时,两个球的连接绷断,即终点的横坐标开始变化,向圆心的方向移动。贝塞尔曲线控制点的位置在圆形的右边,横坐标和圆形的原点相差一个半径,控制点的纵坐标和终点的纵坐标相同,并随着终点纵坐标的变化而变化。
一个二阶贝塞尔曲线完了之后直线连接下个贝塞尔曲线的起点,上下两个贝塞尔曲线合成一组。左右两组是镜像的映射关系。
private void setBezierPath() { mPointData[0] = -mRadiusSmall-(mFraction-1 / 3f)*mRadiusBig*2; mPointData[1] = mFraction<2/3f?-mRadiusSmall:-mRadiusSmall+(mFraction-2/3f)*3*mRadiusSmall; mPointData[2] = mFraction<3/4f?0:(mFraction-3/4f)*16*mPointData[0]; mPointData[3] = -mRadiusBig + mFraction * mRadiusBig * 1.5f<-0.01f*mRadiusBig? -mRadiusBig + mFraction * mRadiusBig * 1.5f:0.01f*-mRadiusBig; mPointData[4] = mPointData[2]; mPointData[5] = -mPointData[3]; mPointData[6] = mPointData[0]; mPointData[7] = -mPointData[1]; mPointCtrl[0] = mPointData[0]+mRadiusSmall; mPointCtrl[1] = mPointData[3]; mPointCtrl[2] = mPointData[0]+mRadiusSmall; mPointCtrl[3] = -mPointCtrl[1]; mPathBezier.reset(); mPathBezier.moveTo(mPointData[0], mPointData[1]); mPathBezier.quadTo(mPointCtrl[0], mPointCtrl[1], mPointData[2], mPointData[3]); mPathBezier.lineTo(mPointData[4], mPointData[5]); mPathBezier.quadTo(mPointCtrl[2], mPointCtrl[3], mPointData[6], mPointData[7]); mPathBezier2.reset(); mPathBezier2.moveTo(-mPointData[0], mPointData[1]); mPathBezier2.quadTo(-mPointCtrl[0], mPointCtrl[1], -mPointData[2], mPointData[3]); mPathBezier2.lineTo(-mPointData[4], mPointData[5]); mPathBezier2.quadTo(-mPointCtrl[2], mPointCtrl[3], -mPointData[6], mPointData[7]); }
(3)当进度大于六分之五时,二阶贝塞尔曲线消失,只剩下两个圆形以原来的速度向外移动,然后周而复始反向重复刚才的过程。到这里就基本结束了,我做的这个动画,自己还不是非常满意,应该是可以优化的,比如绷断之后是不是要加速运动等等,总之我看着还有些别扭,谁有更好的办法希望分享出来,大家一起进步。
5.最后重写这两个方法开启和终止动画。
@Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!mValueAnimator.isRunning()) mValueAnimator.start(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mValueAnimator.isRunning()) mValueAnimator.cancel(); }
结尾:
代码写的比较乱,希望见谅。可以看到一些很奇怪的数字,那些基本上是为了使动画的衔接更加自然而调的,写到这里人生第一篇博客就完工了。
最后为慕课网做个广告,真的很棒!同时感谢宜生老师。