什么是粒子系统

粒子系统通过发射许多微小粒子来表示不规则模糊物体。粒子系统常用于游戏引擎,用来实现火、云、烟花、雨、雪花等效果的实现。通俗来讲,在Android中,一个粒子就是一个小的Drawable,比如雨点图片。而粒子系统的作用就是不停生成雨点并按照一定的轨迹发射,以实现下雨的效果。

Android如何实现粒子系统动画

Android目前并没有自带粒子系统,有一种说法是通过OpenGL实现,但是显然复杂程度比较高。幸运的是找到了github上一个粒子系统的开源库,Leonids

这里简单描述一下使用方法,详见github主页上的使用文档。

  1. 添加开源库的依赖
dependencies {
    compile 'com.plattysoft.leonids:LeonidsLib:1.3.2'
}
  1. 设置粒子系统的参数并发射粒子
ParticleSystem particleSystem = new ParticleSystem(rootLayout,10000, drawable, 10000);
particleSystem.setAccelerationModuleAndAndAngleRange(0.00001f, 0.00002f, 0, 360)
                        .setRotationSpeed(60f);
particleSystem.emitWithGravity(rootLayout, Gravity.TOP, 5);

基本流程就是初始化一个粒子系统对象,然后根据需要设置粒子数、运动轨迹、旋转等属性,然后就开始发射。可以设置粒子发射的角度、运行的加速度、缩放、淡出等参数来设置粒子的运动轨迹。

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

简单介绍一下其中一个构造函数的参数,maxParticles是最大粒子数,指的是场上最多能存活的粒子总数,当场上存在的粒子数达到maxParticles后,粒子系统就会停止发射新粒子,直到场上的部分粒子消亡。在emitWithGravity中有个参数是particlesPerSecond,指的是每秒发射的粒子数。timeToLive是单个粒子能存活的时间。粒子产生之后按照对应的运动轨迹运行,直到timeToLive时长之后,就会消失。

需要注意的是,ParticleSystem在发射的时候需要获取anchorView参数的位置,因此需要在measure之后才能正确运行,而不能在onCreate中调用。

Leonids源码解析

那么Leonids库是如何实现粒子系统的呢。从调用的方法着手进行分析。

  1. 调用构造函数生成一个ParticleSystem对象
	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;
	}

构造方法看起来比较简单,把drawable对象生成maxParticless个Particle对象,也就是粒子,然后添加到列表mParticles保存。

在构造函数中,调用了setParentViewGroup方法,其中调用了getLocationInWindow方法获取了parentView的位置,因此需要在View测量完成之后才能正确执行。

  1. 调用setAccelerationModuleAndAndAngleRange设置ParticleInitializer对象
public ParticleSystem setAccelerationModuleAndAndAngleRange(float minAcceleration, float maxAcceleration, int minAngle, int maxAngle) {
       mInitializers.add(new AccelerationInitializer(dpToPx(minAcceleration), dpToPx(maxAcceleration),
         minAngle, maxAngle));
   return this;
}

从注释可以看出来,ParticleInitializer的作用就是设定粒子初始化的时候的加速度、旋转速度、角度等参数的范围,可以同时设置多个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));
}

选择其中一个实现类看,主要是实现了ParticleInitializer接口的initParticle方法,方法中生成了设定的范围内的随机数,并赋值给Particle对象。

  1. 调用emitWithGravity方法开始粒子动画
public void emitWithGravity (View emiter, int gravity, int particlesPerSecond) {
   // Setup emiter
   configureEmiter(emiter, gravity);
   startEmiting(particlesPerSecond);
}

在方法中调用了configureEmiter和startEmiting两个方法,从方法名就可以看出来,configureEmiter是对发射器进行配置。

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];
   }
}

方法里有很多个if语句,其实就是通过传进来的parentView计算出位置,结合Gravity计算出发射器的范围,也就是粒子运动起点的范围。

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中添加了一个自定义View,ParticleField中定义了一个Particle的列表,在onDraw的时候将所有的Particle绘制到View上。到这里我们就大概知道了这个ParticleSystem是怎么实现的。

但是那些ParticleInitializer又是在哪里派上用场呢。方法的最后启动了一个Timer,大概做了这么个操作。

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

Timer中做了两个事情,一个是计时,一个是调用了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中计算了当前应该存活的粒子有多少个,如果大于现有粒子数,就调用activateParticle进行添加。

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++;
}

在方法中从粒子池里拿出一个粒子,并根据设置的Initializer进行状态的初始化,然后添加到mActiveParticles中。

而后面就是调用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方法中对粒子是否存活以及粒子的位置和旋转角度进行计算。

然后把mActiveParticles中的粒子过了存活时间的粒子移除,放回粒子池中,然后调用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中,调用了Particle的draw方法,把Particle绘制出来。

总结

简单来说,ParticleSystem主要是添加一个View到页面中,然后维护一个Particle的列表,通过Initializer和Modifier定时计算每个Particle当前的状态,然后绘制到View中,实现粒子系统的动画效果。