贝塞尔抛洒礼物动画FireworkAnim

概述

由于公司项目开发需要一个点赞收藏的动效,想给用户一种新鲜感,不那么大众化的效果,于是就自己写了一个类似礼物抛洒的动效,本人无审美,所以不知道效果怎样,只是觉得跟一般的效果还是有区别的。如果有小伙伴觉得有用的话,欢迎使用!主要原理是利用贝塞尔曲线生成随机路径,然后加上一些辅助动画,看起来有一种抛洒的效果。
一切的代码都是为了展示效果给用户,所以,废话不多说,先上图,看效果~

image

如何使用它

Step 1.先在 build.gradle(Project:XXXX) 的 repositories 添加::

allprojects {
	repositories {
		...
		maven { url "https://jitpack.io" }
	}
}

Step 2. 然后在 build.gradle(Module:app) 的 dependencies 添加:

dependencies {
       implementation 'com.github.HeYongRui:FireworkAnim:v1.0.0'
}

使用方法:
     BezierFireworkAnim bezierFireworkAnim = new BezierFireworkAnim(activity);//此处传入Activity参数做上下文
     bezierFireworkAnim.startAnim(view);//传入动画的目标视图控件
     bezierFireworkAnim.cancelAnim();//取消动画

分析

由图可以分析出,首先是根据点击的目标控件在其中心位置动态添加随机个数的小图标,让其覆盖在当前页面的最顶层,然后对这些小图标进行旋转、渐变、位移的动画进行组合就没了,分析完毕,开始撸起袖子干~


首先是获取到点击的位置坐标,并在此处动态生成小图标

        int[] startXY = new int[2];
        targetView.getLocationInWindow(startXY);
        int height = targetView.getHeight();
        int width = targetView.getWidth();
        this.mTargetX = startXY[0] + width / 2;
        this.mTargetY = startXY[1] + height / 2;

然后初始化所有需要的图标资源,我这里用了十四个图标,然后随机设置

//初始化烟花图标资源
        mDrawables = new Drawable[14];
        mDrawables[0] = context.getResources().getDrawable(R.drawable.emoji_1);
        mDrawables[1] = context.getResources().getDrawable(R.drawable.emoji_2);
        mDrawables[2] = context.getResources().getDrawable(R.drawable.emoji_3);
        mDrawables[3] = context.getResources().getDrawable(R.drawable.emoji_4);
        mDrawables[4] = context.getResources().getDrawable(R.drawable.emoji_5);
        mDrawables[5] = context.getResources().getDrawable(R.drawable.emoji_6);
        mDrawables[6] = context.getResources().getDrawable(R.drawable.emoji_7);
        mDrawables[7] = context.getResources().getDrawable(R.drawable.emoji_8);
        mDrawables[8] = context.getResources().getDrawable(R.drawable.emoji_9);
        mDrawables[9] = context.getResources().getDrawable(R.drawable.emoji_10);
        mDrawables[10] = context.getResources().getDrawable(R.drawable.emoji_11);
        mDrawables[11] = context.getResources().getDrawable(R.drawable.emoji_12);
        mDrawables[12] = context.getResources().getDrawable(R.drawable.emoji_13);
        mDrawables[13] = context.getResources().getDrawable(R.drawable.emoji_14);

下一步就是随机动态生成小图标并添加到页面最顶层,随机数保持在一定范围内,不然可能只生成一两个,效果就不太明显

        int size = mRandom.nextInt(4) + 10;
        if (mAnimatorList == null) {
            mAnimatorList = new ArrayList<>();
        } else {
            mAnimatorList.clear();
        }
        for (int i = 0; i < size; i++) {
            //动态创建烟花效果小图标
            ImageView fireworkItemView = new ImageView(mActivity);
            fireworkItemView.setImageDrawable(mDrawables[mRandom.nextInt(14)]);
            RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(50, 50);
            fireworkItemView.setLayoutParams(lp);
            mRootView.addView(fireworkItemView);
        }

上面的rootview就是当前页面的顶层视图,需要传入Activity作为参数才可获取到

 mRootView = (ViewGroup) activity.getWindow().getDecorView();

接下来就是动画的部分了,首先是旋转动画,根据抛洒路线和方向大体分为左右两个部分,所以旋转角度要随机,而且方向也要随机,才符合常规物理逻辑,看起来不那么生硬

        //旋转属性动画
        int rotationAngle = mRandom.nextInt(520) + 200;
        ObjectAnimator rotationAnimator = ObjectAnimator.ofFloat(target, "rotation", 0,
                rotationAngle % 2 == 0 ? rotationAngle : -rotationAngle);
        rotationAnimator.setInterpolator(new AnticipateInterpolator());
        rotationAnimator.setTarget(target);

如上面代码所示,生成一定范围的随机旋转角度和随机左右方向,并且用属性动画设置,因为抛洒动作是先向上减速然后自由落体逐渐加速,所以此处还用到了插值器,插值器的学习请自习查阅相关资料,最后把这个属性动画设置到目标视图(刚才逐个添加的小图标)上


接下来就是重点部分了(敲黑板),童鞋们请坐直身子,竖起耳朵,叫一下旁边睡觉的童鞋了哈~最后就是小图标的位移路径和渐变度了,其实此处思考一下发现可以把位移动画和渐变动画结合,具体怎么做呢,切听下面讲解
由图可以看出,抛洒的路径是一个平滑的路径,此处需要用到贝塞尔曲线知识,其实Photoshop里的钢笔工具用的就是贝塞尔原理,如下图是一个二阶贝塞尔示意图,我们使用它就可以完成我们所需要的效果,不需要高阶的了,二阶的方程是(1 - t)^2 P0 + 2 t (1 - t) P1 + t^2 P2,此处的P0和P2分别对应起、始坐标点,P1为控制点,t为这段效果的进度值。详细了解请查阅相关资料,这里不多做叙述。

image

接下来分析起始点就是点击的视图的中心点,就是上面的mTargetX和mTargetY坐标 PointF startP = new PointF(mTargetX, mTargetY);,因为结束坐标是随机的,并且随机分布在控件下方的一定距离的一条水平线上下,所以逻辑如下:

        int random = mRandom.nextInt(50);
        float endX = 0;
        if (random % 2 == 0) {//左方
            endX = mTargetX - random * 8;
        } else {//右方
            endX = mTargetX + random * 8;
        }
        float endY = mTargetY + 400 + random;
        PointF endP = new PointF(endX, endY);//P2(结束坐标)

好了,现在开始结束点都有了,只差一个控制点了,因为抛出的路劲曲线也要随机,所以控制点的坐标也要随机在一定范围内,由图分析出大体位置并经过实践摸索,控制点坐标逻辑如下:

        int random = mRandom.nextInt(50);
        float controlX = 0;
        float controlY = mTargetY - new Random().nextInt(500) - 100;
        if (random % 2 == 0) {
            controlX = mTargetX - random * 2;
        } else {
            controlX = mTargetX + random * 2;
        }
        PointF controlP = new PointF(controlX, controlY);//P1(控制点坐标)

好了,三个坐标值都有了,接下来套入公式就可以了,怎么套进去呢,这里就要用到属性动画的自定义估值器TypeEvaluator了, TypeEvaluator是属性动画在开始和结束过程中的平滑过渡过程中的返回值,此处自定义一个贝塞尔的估值器,这样它在过程中返回的值就是我们的贝塞尔曲线的值。

public class BezierEvaluator implements TypeEvaluator<PointF> {

    private PointF controlF1;

    public BezierEvaluator(PointF controlF1) {
        this.controlF1 = controlF1;
    }

    @Override
    public PointF evaluate(float time, PointF startValue, PointF endValue) {
        float currentX, currentY;
            //二阶贝塞尔曲线方程(一个控制点)
            currentX = arithmeticProduct(1 - time, 2) * startValue.x
                    + 2 * time * (1 - time) * controlF1.x
                    + arithmeticProduct(time, 2) * endValue.x;
            currentY = arithmeticProduct(1 - time, 2) * startValue.y
                    + 2 * time * (1 - time) * controlF1.y
                    + arithmeticProduct(time, 2) * endValue.y;
        PointF currentP = new PointF(currentX, currentY);
        return currentP;
    }

    private float arithmeticProduct(float value, float square) {//返回浮点数的开方值
        double pow = Math.pow(value, square);
        return (float) pow;
    }
}

然后就可以把它设置到我们的属性动画中了

        BezierEvaluator evaluator = new BezierEvaluator(controlP);
        //贝塞尔动画
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, new PointF(startX, startY), new PointF(endX, endY));
        animator.setInterpolator(new AnticipateInterpolator());
        animator.addUpdateListener(new FireworkAnimUpdateListener(target));
        animator.setTarget(target);

这里的FireworkAnimUpdateListener是一个动画更新监听器,因为此处只是有了动画路径,我们还要更新小图标的位置和透明度,所以需要在FireworkAnimUpdateListener中去完成。

public class FireworkAnimUpdateListener implements ValueAnimator.AnimatorUpdateListener {

    private View mFireworkItemView;//烟花小图标view

    public FireworkAnimUpdateListener(View fireworkItemView) {
        this.mFireworkItemView = fireworkItemView;
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        PointF pointF = (PointF) animation.getAnimatedValue();
        //设置目标位置
        mFireworkItemView.setX(pointF.x);
        mFireworkItemView.setY(pointF.y);
        //设置透明度
        float animatedFraction = animation.getAnimatedFraction();
        float remainder = 1.0f - animatedFraction;
        mFireworkItemView.setAlpha(remainder);
    }
}

这里的代码相信大家都没有什么疑问了吧,就是把小图标的位置更新为动画过程中的对应的进度值的路径坐标,小图标的透明度设置为剩下的动画进度值。这样就可以一个动画完成位移和透明度两个操作。
最后把旋转动画和贝塞尔动画结合一起播放,就是上图的效果。

        AnimatorSet finalSet = new AnimatorSet();
        finalSet.play(bezierValueAnimator).with(rotationAnimator);
        finalSet.setInterpolator(new AnticipateInterpolator());
        finalSet.setDuration(1000);
        finalSet.setTarget(target);
        finalSet.start();


代码已经上传到了github,有需要的童鞋可以看一下。
以上就是全部的分析,这是本人第一次写文章,如有不足之处,还请多多包涵~

猜你喜欢

转载自juejin.im/post/5b30b060e51d4558cf6e8d0a
今日推荐