贝塞尔曲线实现球形一变二动画效果

来源:互联网 发布:数据库范式题目 编辑:程序博客网 时间:2024/06/02 07:28
概述

贝塞尔曲线实为实现动画的一大利器,下面举个例子演示贝塞尔曲线的使用实例。人生第一篇博客,不足之处望大家果断指出。最终效果如下。


前戏:

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();    }

结尾:

代码写的比较乱,希望见谅。可以看到一些很奇怪的数字,那些基本上是为了使动画的衔接更加自然而调的,写到这里人生第一篇博客就完工了。

最后为慕课网做个广告,真的很棒!同时感谢宜生老师。

0 0