粒子動(dòng)畫(huà)的使用和原理

什么是粒子系統(tǒng)

粒子系統(tǒng)通過(guò)發(fā)射許多微小粒子來(lái)表示不規(guī)則模糊物體蒲肋。粒子系統(tǒng)常用于游戲引擎蘑拯,用來(lái)實(shí)現(xiàn)火、云兜粘、煙花申窘、雨、雪花等效果的實(shí)現(xiàn)孔轴。通俗來(lái)講偶洋,在Android中,一個(gè)粒子就是一個(gè)小的Drawable距糖,比如雨點(diǎn)圖片。而粒子系統(tǒng)的作用就是不停生成雨點(diǎn)并按照一定的軌跡發(fā)射牵寺,以實(shí)現(xiàn)下雨的效果悍引。

Android如何實(shí)現(xiàn)粒子系統(tǒng)動(dòng)畫(huà)

Android目前并沒(méi)有自帶粒子系統(tǒng),有一種說(shuō)法是通過(guò)OpenGL實(shí)現(xiàn)帽氓,但是顯然復(fù)雜程度比較高趣斤。幸運(yùn)的是找到了github上一個(gè)粒子系統(tǒng)的開(kāi)源庫(kù),Leonids黎休。

這里簡(jiǎn)單描述一下使用方法浓领,詳見(jiàn)github主頁(yè)上的使用文檔。

  1. 添加開(kāi)源庫(kù)的依賴
dependencies {
    compile 'com.plattysoft.leonids:LeonidsLib:1.3.2'
}
  1. 設(shè)置粒子系統(tǒng)的參數(shù)并發(fā)射粒子
ParticleSystem particleSystem = new ParticleSystem(rootLayout,10000, drawable, 10000);
particleSystem.setAccelerationModuleAndAndAngleRange(0.00001f, 0.00002f, 0, 360)
                        .setRotationSpeed(60f);
particleSystem.emitWithGravity(rootLayout, Gravity.TOP, 5);

基本流程就是初始化一個(gè)粒子系統(tǒng)對(duì)象势腮,然后根據(jù)需要設(shè)置粒子數(shù)联贩、運(yùn)動(dòng)軌跡、旋轉(zhuǎn)等屬性捎拯,然后就開(kāi)始發(fā)射泪幌。可以設(shè)置粒子發(fā)射的角度、運(yùn)行的加速度祸泪、縮放吗浩、淡出等參數(shù)來(lái)設(shè)置粒子的運(yùn)動(dòng)軌跡。

ParticleSystem(ViewGroup parentView, int maxParticles, Drawable drawable, long timeToLive)

簡(jiǎn)單介紹一下其中一個(gè)構(gòu)造函數(shù)的參數(shù)没隘,maxParticles是最大粒子數(shù)懂扼,指的是場(chǎng)上最多能存活的粒子總數(shù),當(dāng)場(chǎng)上存在的粒子數(shù)達(dá)到maxParticles后右蒲,粒子系統(tǒng)就會(huì)停止發(fā)射新粒子阀湿,直到場(chǎng)上的部分粒子消亡。在emitWithGravity中有個(gè)參數(shù)是particlesPerSecond品嚣,指的是每秒發(fā)射的粒子數(shù)炕倘。timeToLive是單個(gè)粒子能存活的時(shí)間。粒子產(chǎn)生之后按照對(duì)應(yīng)的運(yùn)動(dòng)軌跡運(yùn)行翰撑,直到timeToLive時(shí)長(zhǎng)之后罩旋,就會(huì)消失。

需要注意的是眶诈,ParticleSystem在發(fā)射的時(shí)候需要獲取anchorView參數(shù)的位置涨醋,因此需要在measure之后才能正確運(yùn)行,而不能在onCreate中調(diào)用逝撬。

Leonids源碼解析

那么Leonids庫(kù)是如何實(shí)現(xiàn)粒子系統(tǒng)的呢浴骂。從調(diào)用的方法著手進(jìn)行分析。

  1. 調(diào)用構(gòu)造函數(shù)生成一個(gè)ParticleSystem對(duì)象
    public ParticleSystem(ViewGroup parentView, int maxParticles, Drawable drawable, long timeToLive) {
        this(parentView, maxParticles, timeToLive);

        if (drawable instanceof AnimationDrawable) {
            AnimationDrawable animation = (AnimationDrawable) drawable;
            for (int i=0; i<mMaxParticles; i++) {
                mParticles.add (new AnimatedParticle (animation));
            }
        }
        else {
            Bitmap bitmap = null;
            if (drawable instanceof BitmapDrawable) {
                bitmap = ((BitmapDrawable) drawable).getBitmap();
            }
            else {
                bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                        drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
                Canvas canvas = new Canvas(bitmap);
                drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
                drawable.draw(canvas);
            }
            for (int i=0; i<mMaxParticles; i++) {
                mParticles.add (new Particle (bitmap));
            }
        }
    }
    
private ParticleSystem(ViewGroup parentView, int maxParticles, long timeToLive) {
        ...
        setParentViewGroup(parentView);
        ···
    }

    public ParticleSystem setParentViewGroup(ViewGroup viewGroup) {
        mParentView = viewGroup;
        if (mParentView != null) {
            mParentView.getLocationInWindow(mParentLocation);
        }
        return this;
    }

構(gòu)造方法看起來(lái)比較簡(jiǎn)單宪潮,把drawable對(duì)象生成maxParticless個(gè)Particle對(duì)象溯警,也就是粒子,然后添加到列表mParticles保存狡相。

在構(gòu)造函數(shù)中梯轻,調(diào)用了setParentViewGroup方法,其中調(diào)用了getLocationInWindow方法獲取了parentView的位置尽棕,因此需要在View測(cè)量完成之后才能正確執(zhí)行喳挑。

  1. 調(diào)用setAccelerationModuleAndAndAngleRange設(shè)置ParticleInitializer對(duì)象
public ParticleSystem setAccelerationModuleAndAndAngleRange(float minAcceleration, float maxAcceleration, int minAngle, int maxAngle) {
       mInitializers.add(new AccelerationInitializer(dpToPx(minAcceleration), dpToPx(maxAcceleration),
         minAngle, maxAngle));
   return this;
}

從注釋可以看出來(lái),ParticleInitializer的作用就是設(shè)定粒子初始化的時(shí)候的加速度滔悉、旋轉(zhuǎn)速度伊诵、角度等參數(shù)的范圍,可以同時(shí)設(shè)置多個(gè)Initializer回官。

@Override
public void initParticle(Particle p, Random r) {
   float angle = mMinAngle;
   if (mMaxAngle != mMinAngle) {
      angle = r.nextInt(mMaxAngle - mMinAngle) + mMinAngle;
   }
   float angleInRads = (float) (angle*Math.PI/180f);
   float value = r.nextFloat()*(mMaxValue-mMinValue)+mMinValue;
   p.mAccelerationX = (float) (value * Math.cos(angleInRads));
   p.mAccelerationY = (float) (value * Math.sin(angleInRads));
}

選擇其中一個(gè)實(shí)現(xiàn)類看曹宴,主要是實(shí)現(xiàn)了ParticleInitializer接口的initParticle方法,方法中生成了設(shè)定的范圍內(nèi)的隨機(jī)數(shù)孙乖,并賦值給Particle對(duì)象浙炼。

  1. 調(diào)用emitWithGravity方法開(kāi)始粒子動(dòng)畫(huà)
public void emitWithGravity (View emiter, int gravity, int particlesPerSecond) {
   // Setup emiter
   configureEmiter(emiter, gravity);
   startEmiting(particlesPerSecond);
}

在方法中調(diào)用了configureEmiter和startEmiting兩個(gè)方法份氧,從方法名就可以看出來(lái),configureEmiter是對(duì)發(fā)射器進(jìn)行配置弯屈。

private void configureEmiter(View emiter, int gravity) {
   // It works with an emision range
   int[] location = new int[2];
   emiter.getLocationInWindow(location);
   
   // Check horizontal gravity and set range
   if (hasGravity(gravity, Gravity.LEFT)) {
      mEmiterXMin = location[0] - mParentLocation[0];
      mEmiterXMax = mEmiterXMin;
   }
   else if (hasGravity(gravity, Gravity.RIGHT)) {
      mEmiterXMin = location[0] + emiter.getWidth() - mParentLocation[0];
      mEmiterXMax = mEmiterXMin;
   }
   else if (hasGravity(gravity, Gravity.CENTER_HORIZONTAL)){
      mEmiterXMin = location[0] + emiter.getWidth()/2 - mParentLocation[0];
      mEmiterXMax = mEmiterXMin;
   }
   else {
      // All the range
      mEmiterXMin = location[0] - mParentLocation[0];
      mEmiterXMax = location[0] + emiter.getWidth() - mParentLocation[0];
   }
   
   // Now, vertical gravity and range
   if (hasGravity(gravity, Gravity.TOP)) {
      mEmiterYMin = location[1] - mParentLocation[1];
      mEmiterYMax = mEmiterYMin;
   }
   else if (hasGravity(gravity, Gravity.BOTTOM)) {
      mEmiterYMin = location[1] + emiter.getHeight() - mParentLocation[1];
      mEmiterYMax = mEmiterYMin;
   }
   else if (hasGravity(gravity, Gravity.CENTER_VERTICAL)){
      mEmiterYMin = location[1] + emiter.getHeight()/2 - mParentLocation[1];
      mEmiterYMax = mEmiterYMin;
   }
   else {
      // All the range
      mEmiterYMin = location[1] - mParentLocation[1];
      mEmiterYMax = location[1] + emiter.getHeight() - mParentLocation[1];
   }
}

方法里有很多個(gè)if語(yǔ)句蜗帜,其實(shí)就是通過(guò)傳進(jìn)來(lái)的parentView計(jì)算出位置,結(jié)合Gravity計(jì)算出發(fā)射器的范圍资厉,也就是粒子運(yùn)動(dòng)起點(diǎn)的范圍厅缺。

private void startEmiting(int particlesPerSecond) {
   mActivatedParticles = 0;
   mParticlesPerMilisecond = particlesPerSecond/1000f;
   // Add a full size view to the parent view    
   mDrawingView = new ParticleField(mParentView.getContext());
   mParentView.addView(mDrawingView);
   mEmitingTime = -1; // Meaning infinite
   mDrawingView.setParticles (mActiveParticles);
   updateParticlesBeforeStartTime(particlesPerSecond);
   mTimer = new Timer();
   mTimer.schedule(mTimerTask, 0, TIMMERTASK_INTERVAL);
}

而在startEmiting中可以看到,作者在mParentView中添加了一個(gè)自定義View宴偿,ParticleField中定義了一個(gè)Particle的列表湘捎,在onDraw的時(shí)候?qū)⑺械腜article繪制到View上。到這里我們就大概知道了這個(gè)ParticleSystem是怎么實(shí)現(xiàn)的窄刘。

但是那些ParticleInitializer又是在哪里派上用場(chǎng)呢窥妇。方法的最后啟動(dòng)了一個(gè)Timer,大概做了這么個(gè)操作娩践。

@Override
public void run() {
    if(mPs.get() != null) {
        ParticleSystem ps = mPs.get();
        ps.onUpdate(ps.mCurrentTime);
        ps.mCurrentTime += TIMMERTASK_INTERVAL;
    }
}

Timer中做了兩個(gè)事情活翩,一個(gè)是計(jì)時(shí),一個(gè)是調(diào)用了onUpdate方法翻伺。

private void onUpdate(long miliseconds) {
   while (((mEmitingTime > 0 && miliseconds < mEmitingTime)|| mEmitingTime == -1) && // This point should emit
         !mParticles.isEmpty() && // We have particles in the pool 
         mActivatedParticles < mParticlesPerMilisecond*miliseconds) { // and we are under the number of particles that should be launched
      // Activate a new particle
      activateParticle(miliseconds);       
   }
   synchronized(mActiveParticles) {
      for (int i = 0; i < mActiveParticles.size(); i++) {
         boolean active = mActiveParticles.get(i).update(miliseconds);
         if (!active) {
            Particle p = mActiveParticles.remove(i);
            i--; // Needed to keep the index at the right position
            mParticles.add(p);
         }
      }
   }
   mDrawingView.postInvalidate();
}

在onUpdate中計(jì)算了當(dāng)前應(yīng)該存活的粒子有多少個(gè)材泄,如果大于現(xiàn)有粒子數(shù),就調(diào)用activateParticle進(jìn)行添加吨岭。

private void activateParticle(long delay) {
   Particle p = mParticles.remove(0); 
   p.init();
   // Initialization goes before configuration, scale is required before can be configured properly
   for (int i=0; i<mInitializers.size(); i++) {
      mInitializers.get(i).initParticle(p, mRandom);
   }
   int particleX = getFromRange (mEmiterXMin, mEmiterXMax);
   int particleY = getFromRange (mEmiterYMin, mEmiterYMax);
   p.configure(mTimeToLive, particleX, particleY);
   p.activate(delay, mModifiers);
   mActiveParticles.add(p);
   mActivatedParticles++;
}

在方法中從粒子池里拿出一個(gè)粒子拉宗,并根據(jù)設(shè)置的Initializer進(jìn)行狀態(tài)的初始化,然后添加到mActiveParticles中辣辫。

而后面就是調(diào)用Particle的update方法旦事。

public boolean update (long miliseconds) {
   long realMiliseconds = miliseconds - mStartingMilisecond;
   if (realMiliseconds > mTimeToLive) {
      return false;
   }
   mCurrentX = mInitialX+mSpeedX*realMiliseconds+mAccelerationX*realMiliseconds*realMiliseconds;
   mCurrentY = mInitialY+mSpeedY*realMiliseconds+mAccelerationY*realMiliseconds*realMiliseconds;
   mRotation = mInitialRotation + mRotationSpeed*realMiliseconds/1000;
   for (int i=0; i<mModifiers.size(); i++) {
      mModifiers.get(i).apply(this, realMiliseconds);
   }
   return true;
}

在update方法中對(duì)粒子是否存活以及粒子的位置和旋轉(zhuǎn)角度進(jìn)行計(jì)算。

然后把mActiveParticles中的粒子過(guò)了存活時(shí)間的粒子移除急灭,放回粒子池中族檬,然后調(diào)用postInvalidate更新ParticleField。

@Override
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   // Draw all the particles
   synchronized (mParticles) {
      for (int i = 0; i < mParticles.size(); i++) {
         mParticles.get(i).draw(canvas);
      }
   }
}

在ParticleField的onDraw中化戳,調(diào)用了Particle的draw方法,把Particle繪制出來(lái)埋凯。

總結(jié)

簡(jiǎn)單來(lái)說(shuō)点楼,ParticleSystem主要是添加一個(gè)View到頁(yè)面中,然后維護(hù)一個(gè)Particle的列表白对,通過(guò)Initializer和Modifier定時(shí)計(jì)算每個(gè)Particle當(dāng)前的狀態(tài)掠廓,然后繪制到View中,實(shí)現(xiàn)粒子系統(tǒng)的動(dòng)畫(huà)效果甩恼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蟀瞧,一起剝皮案震驚了整個(gè)濱河市沉颂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌悦污,老刑警劉巖铸屉,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異切端,居然都是意外死亡彻坛,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門踏枣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)昌屉,“玉大人,你說(shuō)我怎么就攤上這事茵瀑〖渫裕” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵马昨,是天一觀的道長(zhǎng)竞帽。 經(jīng)常有香客問(wèn)我,道長(zhǎng)偏陪,這世上最難降的妖魔是什么抢呆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮笛谦,結(jié)果婚禮上抱虐,老公的妹妹穿的比我還像新娘。我一直安慰自己饥脑,他們只是感情好恳邀,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著灶轰,像睡著了一般谣沸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上笋颤,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天乳附,我揣著相機(jī)與錄音,去河邊找鬼伴澄。 笑死赋除,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的非凌。 我是一名探鬼主播举农,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼敞嗡!你這毒婦竟也來(lái)了颁糟?” 一聲冷哼從身側(cè)響起航背,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎棱貌,沒(méi)想到半個(gè)月后玖媚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡键畴,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年最盅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片起惕。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涡贱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惹想,到底是詐尸還是另有隱情问词,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布嘀粱,位于F島的核電站激挪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏锋叨。R本人自食惡果不足惜垄分,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望娃磺。 院中可真熱鬧薄湿,春花似錦、人聲如沸偷卧。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)听诸。三九已至坐求,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晌梨,已是汗流浹背桥嗤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仔蝌,地道東北人砸逊。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像掌逛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子司倚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345