贝塞尔抛洒礼物动画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,有需要的童鞋可以看一下。
以上就是全部的分析,这是本人第一次写文章,如有不足之处,还请多多包涵~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容