美文网首页
粒子动画的使用和原理

粒子动画的使用和原理

作者: 可乐游侠 | 来源:发表于2019-12-21 22:06 被阅读0次

    什么是粒子系统

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

    相关文章

      网友评论

          本文标题:粒子动画的使用和原理

          本文链接:https://www.haomeiwen.com/subject/mxrnnctx.html