使用過MIUI的同學應該遇到過MIUI的app卸載動畫,作爲多年的米粉,當我嘗試去實現這個動畫的時候,第一時間就是在網上看有沒有類似的效果,果然我找到了這個:
【Android效果集】學習ExplosionField之粒子破碎效果
可這個動畫使用起來並不理想,其粒子在爆炸後,其運動方向左右搖擺,當我仔細閱讀代碼之後,發現其中 advance方法(即動畫進行過程中,用於改變粒子參數的方法)如圖:
可以看到,隨着動畫的進行,粒子的圓心x座標,每次都會加一個隨機正負的隨機數;圓心的y座標會加一個正隨機數;因此粒子的左右移動是不確定的,這並不符合自然規律。
那麼什麼纔是自然規律呢?
- 粒子在x軸上:爆炸的那一刻,就決定了是往左還是往右,之後只能朝着這個方向繼續移動。
- 粒子的y軸上:可以看到MIUI的效果,是粒子先向上運動,然後下落。
於是,我又找了開源項目:
該項目效果如圖:
可以看到效果幾乎與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之間,用於控制粒子在開始的前多少時間、動畫結束前的多少時間,是不顯示的,這樣就有了一個錯落出現、消失的層次感。
在這裏,再次爲開發者獻上自己的膝蓋~~~
一般當我們讀懂了別人的代碼後,自己去實現的時候,總是會遇到這樣那樣的問題,因此,我們這裏可以嘗試自己去順着大牛的思路來實現這個效果,同時,加入自己的想法,進行部分功能的改進。這些東西就留給下一篇博客了!