美文网首页
Android粒子破碎效果(1)——开源项目ExplosionF

Android粒子破碎效果(1)——开源项目ExplosionF

作者: ifadai | 来源:发表于2017-12-19 11:59 被阅读0次

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

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

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

    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):

    ExplosionField 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;
        }
    

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

    生成粒子

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

    <font color=green>请注意绿色部分的正负取值</font>

    总之,上面的一系列计算,都是以为了让每一个粒子都有不一样的参数,以及后续在动画进程中不一样的运动轨迹。值得注意的是,上面的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),那么,我们在函数曲线中看一下:

    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
    

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

    Cy变化曲线2

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

    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)——开源项目ExplosionF

          本文链接:https://www.haomeiwen.com/subject/qpybwxtx.html