使用过MIUI的同学应该遇到过MIUI的app卸载动画,作为多年的米粉,当我尝试去实现这个动画的时候,第一时间就是在网上看有没有类似的效果,果然我找到了这个:
【Android效果集】学习ExplosionField之粒子破碎效果
可这个动画使用起来并不理想,其粒子在爆炸后,其运动方向左右摇摆,当我仔细阅读代码之后,发现其中 advance方法(即动画进行过程中,用于改变粒子参数的方法)如图:
可以看到,随着动画的进行,粒子的圆心x坐标,每次都会加一个随机正负的随机数;圆心的y坐标会加一个正随机数;因此粒子的左右移动是不确定的,这并不符合自然规律。
那么什么才是自然规律呢?
- 粒子在x轴上:爆炸的那一刻,就决定了是往左还是往右,之后只能朝着这个方向继续移动。
- 粒子的y轴上:可以看到MIUI的效果,是粒子先向上运动,然后下落。
于是,我又找了开源项目:
ExplosionField
该项目效果如图:
可以看到效果几乎与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):
我们重点来讲讲粒子的生成方法和变化方法:
首先是粒子的各项参数(加注释版):
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;
}
恩...配合下面的思维导图食用更佳:
红色参数:粒子在生成时,就固定下来的参数,随着动画进程而不改变的值。
请注意绿色部分的正负取值
总之,上面的一系列计算,都是以为了让每一个粒子都有不一样的参数,以及后续在动画进程中不一样的运动轨迹。值得注意的是,上面的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),那么,我们在函数曲线中看一下:
可以看到,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
我们将其应用到函数曲线中:
因为View坐标系y轴是向下的,与数学坐标系相反,我们可以修改一下方程,达到类似View坐标系的效果:
总结
代码分析的差不多了,我们基本上可以看出开发者的思路:粒子的生成的时候,通过大量的随机运算,给粒子赋予尽量区别于其他粒子的参数。
其中:
- 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