什么是粒子系統(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è)上的使用文檔。
- 添加開(kāi)源庫(kù)的依賴
dependencies {
compile 'com.plattysoft.leonids:LeonidsLib:1.3.2'
}
- 設(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)行分析。
- 調(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í)行喳挑。
- 調(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ì)象浙炼。
- 調(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à)效果甩恼。