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

概述

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


前戏:

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
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值