Android粒子破碎效果(1)——开源项目ExplosionField代码分析

使用过MIUI的同学应该遇到过MIUI的app卸载动画,作为多年的米粉,当我尝试去实现这个动画的时候,第一时间就是在网上看有没有类似的效果,果然我找到了这个:

【Android效果集】学习ExplosionField之粒子破碎效果

可这个动画使用起来并不理想,其粒子在爆炸后,其运动方向左右摇摆,当我仔细阅读代码之后,发现其中 advance方法(即动画进行过程中,用于改变粒子参数的方法)如图:

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第1张图片
image

可以看到,随着动画的进行,粒子的圆心x坐标,每次都会加一个随机正负的随机数;圆心的y坐标会加一个正随机数;因此粒子的左右移动是不确定的,这并不符合自然规律。

那么什么才是自然规律呢?

  • 粒子在x轴上:爆炸的那一刻,就决定了是往左还是往右,之后只能朝着这个方向继续移动。
  • 粒子的y轴上:可以看到MIUI的效果,是粒子先向上运动,然后下落。

于是,我又找了开源项目:

ExplosionField

该项目效果如图:

explosionfield.gif

可以看到效果几乎与MIUI的效果相同,但是该项目没有一句注释,且其对粒子的参数进行的大量数学计算,因此我费了好大劲,终于像解方程一样,理清了开发者的思路。下面先分析该项目代码:

代码分析

使用方法:

实例化:

mExplosionField = ExplosionField.attach2Window(this);

给View添加爆炸效果:

mExplosionField.explode(view);

分析

该项目总共有四个类:

  • ExplosionAnimator,继承自ValueAnimator,负责产生具有动画规律的数字,还有负责生成粒子、绘制粒子的方法。
  • ExplosionField,继承自View,用于将动画生成的粒子绘制在界面上,包含执行动画、将自身添加到ContentView中的方法。
  • Particle,粒子的实体类,同时也是ExplosionAnimator的内部类,包含粒子绘制的参数,以及最重要的粒子随着动画进程,改变自身参数的advance方法。
  • Utils,工具类,包含dp转px、根据View创建Bitmap方法。

其思路流程不在赘述,了解过自定义View和属性动画的同学应该都能看的懂,这里贴两个思维导图(原谅我做的图太丑了 o(╥﹏╥)o):

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第2张图片
ExplosionField
Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第3张图片
ExplosionAnimator

我们重点来讲讲粒子的生成方法和变化方法:

首先是粒子的各项参数(加注释版):

 private class Particle {
 
        float alpha;        // 透明度
        int color;          // 颜色
        float cx;          // 粒子圆心 x
        float cy;          // 粒子圆心 y
        float radius;      // 粒子半径
        float baseCx;      // 粒子圆心 x的基础值,后续cx的取值就由baseCx为基准
        float baseCy;      // 粒子圆心 y的基础值,后续cy的取值就由baseCy为基准
        float baseRadius;  // 粒子的基础半径,后续radius的取值就由baseRadius为基准
        float top;         // 负责cy变化的因素
        float bottom;      // 负责cx变化的因素
        float mag;         // 负责cy变化的因素(因为是基于上面两个值计算而来,通过修改计算公式可以修改粒子变化幅度
        float neg;         // 同上
        float life;        // 决定了粒子在动画开始多久之后,开始显示
        float overflow;    // 决定了粒子动画结束前多少时间开始隐藏
        
        }

当我刚开始看到一大堆bottom、top、mag等参数时,一脸懵逼,后来通过分析其粒子生成方法和粒子变化方法,才推测出这些参数的用处。

然后,我们来看看粒子生成方法 generateParticle(int color, Random random):

private Particle generateParticle(int color, Random random) {
        Particle particle = new Particle();
        particle.color = color;
        particle.radius = V;
        if (random.nextFloat() < 0.2f) {
            particle.baseRadius = V + ((X - V) * random.nextFloat());
        } else {
            particle.baseRadius = W + ((V - W) * random.nextFloat());
        }
        float nextFloat = random.nextFloat();
        particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);
        particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());
        particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;
        float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;
        particle.bottom = f;
        particle.mag = 4.0f * particle.top / particle.bottom;
        particle.neg = (-particle.mag) / particle.bottom;
        f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f));
        particle.baseCx = f;
        particle.cx = f;
        f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));
        particle.baseCy = f;
        particle.cy = f;
        particle.life = END_VALUE / 10 * random.nextFloat();
        particle.overflow = 0.4f * random.nextFloat();
        particle.alpha = 1f;
        return particle;
    }

恩...配合下面的思维导图食用更佳:

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第4张图片
生成粒子

红色参数:粒子在生成时,就固定下来的参数,随着动画进程而不改变的值。

请注意绿色部分的正负取值

总之,上面的一系列计算,都是以为了让每一个粒子都有不一样的参数,以及后续在动画进程中不一样的运动轨迹。值得注意的是,上面的top和bottom在计算中,使用了同一个变量--nextFloat,因此bottom与top的规律在于:top越大,bottom的相对值就越小,反之亦然。表现在运动轨迹上,就是粒子横向运动的越远,竖直方向运动的就越近(相对来说).这里就不得不佩服开发者的细心了,这种规律都能考虑到 Orz。

我们继续来看粒子的变化方法 advance(float factor):

public void advance(float factor) {
            float f = 0f;
            float normalization = factor / END_VALUE;
            if (normalization < life || normalization > 1f - overflow) {
                alpha = 0f;
                return;
            }
            normalization = (normalization - life) / (1f - life - overflow);
            float f2 = normalization * END_VALUE;
            if (normalization >= 0.7f) {
                f = (normalization - 0.7f) / 0.3f;
            }
            alpha = 1f - f;
            f = bottom * f2;
            cx = baseCx + f;
            cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
            radius = V + (baseRadius - V) * f2;
        }

添加注释后:

 public void advance(float factor) {

            float f = 0f;

            // normal= 粒子在可显示的范围内,动画进行到了几分之几
            float normalization = factor / END_VALUE;

            // 动画开始前和结束前的一段时间内是透明(不进行绘制)的。
            if (normalization < life || normalization > 1f - overflow) {
                alpha = 0f;
                return;
            }
            // normal= 粒子在可显示的范围内,动画实际进行到了几分之几
            normalization = (normalization - life) / (1f - life - overflow);

            // f2= 实际进行到的数值
            float f2 = normalization * END_VALUE;

            // 动画实际进程超过7/10,则开始逐渐透明。
            if (normalization >= 0.7f) {
                f = (normalization - 0.7f) / 0.3f;
            }
            alpha = 1f - f;

            // cx 在baseCx的基础上增长f2个bottom(bottom可能是负数,这里就表现了粒子是往左移动还是往右移动
            f = bottom * f2;
            cx = baseCx + f;

            // 可以把这个计算视为一个方程,然后,我们一步步简化:
            // 已知:mag=4*top/bottom; neg=-mag / bottom; f=bottom*f2;
            // 则:cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
            // 则:cy= (float)(baseCy-(-(4*top/bottom)/bottom)*bottom*bottom*f2*f2)-bottom*f2*4*top/bottom;
            // 则:cy= baseCy+(4*top*(f2*(f2-1)));
            // 那么,我们就可以的出cy的变化曲线函数: y=baseCy+4*top*(x*(x-1),再简化: y=j+k*(x*(x-1),j、k都是常数,x为 0~1.4;
            // 那么,粒子的变化因素只有一个x*(x-1)
            cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;

            // 可以简化为:y=k*x,k是常数,x为 0~1.4;因此radius是不断增长的。
            radius = V + (baseRadius - V) * f2;
        }

注释里基本都写的很清楚了,关键是Cy的取值,我们可以看到,cy的变化因素为y=x*(x-1),那么,我们在函数曲线中看一下:

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第5张图片
Cy的变化曲线

可以看到,y是先下降再上升,且当x小于1时,y是负值。动画的结束值是1.4,那么当动画进程在0.5之前时,baseCy是加一个不断变小的负值,表现到View坐标系中,则是粒子向上运动。之后,便是baseCy加一个不断增加的值,表现为粒子向下运动。

我们可以测试一下,先打印第一个粒子的baseCy和top值:

if(ttt==0){
    tt=bottom;
    Log.d("ExplosionAnimator","baseCy="+baseCy+";top="+top);
    } else{
        if(ttt==bottom){
            Log.d("ExplosionAnimator","baseCy="+baseCy+";top="+top);
            }
    }

日志:

D/ExplosionAnimator: baseCy=299.99106;top=147.68047

我们将其应用到函数曲线中:

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第6张图片
Cy变化曲线2

因为View坐标系y轴是向下的,与数学坐标系相反,我们可以修改一下方程,达到类似View坐标系的效果:

Android粒子破碎效果(1)——开源项目ExplosionField代码分析_第7张图片
Cy变化曲线3

总结

代码分析的差不多了,我们基本上可以看出开发者的思路:粒子的生成的时候,通过大量的随机运算,给粒子赋予尽量区别于其他粒子的参数。

其中:

  • cx,初始位置为view中心点左右随机偏移一定值,根据bottom值,又可以分为向左运动(bottom为负数)的粒子、向右运动(bottom为正数)的粒子;
  • cy,初始位置为view中心点上下随机偏移一定值,粒子在y轴上沿y=x*(x-1)曲线运动;
  • radius,初始为大半径(1/5概率)、小半径(4/5概率),之后开始逐渐变大;
  • alpha,初始为1,动画实际进程超过7/10时,开始逐渐变透明;
  • 每一个粒子都有一个经过随机运算得出的life和overflow,取值差不多为0.0x~0.1x之间,用于控制粒子在开始的前多少时间、动画结束前的多少时间,是不显示的,这样就有了一个错落出现、消失的层次感。

在这里,再次为开发者献上自己的膝盖~~~

一般当我们读懂了别人的代码后,自己去实现的时候,总是会遇到这样那样的问题,因此,我们这里可以尝试自己去顺着大牛的思路来实现这个效果,同时,加入自己的想法,进行部分功能的改进。这些东西就留给下一篇博客了!

Android粒子破碎效果(2)——实现多种破碎效果之ParticleSmasher

你可能感兴趣的:(Android粒子破碎效果(1)——开源项目ExplosionField代码分析)