粒子动画的使用和原理

什么是粒子系统

粒子系统通过发射许多微小粒子来表示不规则模糊物体。粒子系统常用于游戏引擎,用来实现火、云、烟花、雨、雪花等效果的实现。通俗来讲,在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中,实现粒子系统的动画效果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352