粒子动画的使用和原理
什么是粒子系统
粒子系统通过发射许多微小粒子来表示不规则模糊物体。粒子系统常用于游戏引擎,用来实现火、云、烟花、雨、雪花等效果的实现。通俗来讲,在Android中,一个粒子就是一个小的Drawable,比如雨点图片。而粒子系统的作用就是不停生成雨点并按照一定的轨迹发射,以实现下雨的效果。
Android如何实现粒子系统动画
Android目前并没有自带粒子系统,有一种说法是通过OpenGL实现,但是显然复杂程度比较高。幸运的是找到了github上一个粒子系统的开源库,Leonids。
这里简单描述一下使用方法,详见github主页上的使用文档。
- 添加开源库的依赖
dependencies {
compile 'com.plattysoft.leonids:LeonidsLib:1.3.2'
}
- 设置粒子系统的参数并发射粒子
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库是如何实现粒子系统的呢。从调用的方法着手进行分析。
- 调用构造函数生成一个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测量完成之后才能正确执行。
- 调用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对象。
- 调用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中,实现粒子系统的动画效果。