Android粒子破碎效果(1)——開源項目ExplosionField代碼分析

使用過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;
    }

恩…配合下面的思維導圖食用更佳:

生成粒子

紅色參數:粒子在生成時,就固定下來的參數,隨着動畫進程而不改變的值。

請注意綠色部分的正負取值

總之,上面的一系列計算,都是以爲了讓每一個粒子都有不一樣的參數,以及後續在動畫進程中不一樣的運動軌跡。值得注意的是,上面的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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章