美文网首页
Android粒子破碎效果(2)——实现多种破碎效果之Parti

Android粒子破碎效果(2)——实现多种破碎效果之Parti

作者: ifadai | 来源:发表于2017-12-22 11:42 被阅读0次

    上一篇我们分析了开源项目ExplosionFiled,了解了其绘制动画效果的流程以及粒子运动的轨迹的计算。学习要与实践相结合,因此,在该项目的基础上,我又做一些自己的改进和功能的增加。

    1、介绍

    特色:

    • 六种效果,包含爆炸效果、坠落效果、四个方向的逐渐飘落效果;
    • 链式调用,自定义动画时间、样式、动画幅度等;

    地址:

    Github地址

    效果图:

    六种效果演示

    用法:

    导入

    dependencies {
     compile 'com.ifadai:particlesmasher:1.0.1'
    }
    

    简单使用:

     ParticleSmasher smasher = new ParticleSmasher(this);
     // 默认为爆炸动画
     smasher.with(view).start();
    

    复杂一点:

    smasher.with(view)
            .setStyle(SmashAnimator.STYLE_DROP)    // 设置动画样式
            .setDuration(1500)                     // 设置动画时间
            .setStartDelay(300)                    // 设置动画前延时
            .setHorizontalMultiple(2)              // 设置横向运动幅度,默认为3
            .setVerticalMultiple(2)                // 设置竖向运动幅度,默认为4
           .addAnimatorListener(new SmashAnimator.OnAnimatorListener() {
                                @Override
                                public void onAnimatorStart() {
                                    super.onAnimatorStart();
                                    // 回调,动画开始
                                }
    
                                @Override
                                public void onAnimatorEnd() {
                                    super.onAnimatorEnd();
                                    // 回调,动画结束
                                }
                            })
            .start();    
    

    让View重新显示:

    smasher.reShowView(view);
    

    2、代码解析

    项目结构:

    项目结构

    ParticleSmasher:

    与ExplosionFiled相同,这里也是继承自View,用于绘制动画效果。先看构造方法:

    public ParticleSmasher(Activity activity) {
            super((Context) activity);
            this.mActivity = activity;
            addView2Window(activity);
            init();
        }
    

    这里通过addView2Window()将绘制动画效果的View添加到了RootView中,因此同一界面上多个View要实现动画效果时,只实例化一个ParticleSmasher即可。

    /**
         * 添加View到当前界面
         */
        private void addView2Window(Activity activity) {
            ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
            // 需要足够的空间展现动画,因此这里使用的是充满父布局
            ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            rootView.addView(this, layoutParams);
        }
    

    开始动画时,是调用particleSmasher.with(view)方法,该方法会实例化一个SmashAnimator对象,后续可以通过一系列链式调用,来修改该对象的属性。同时,将该对象添加到了Animator集合中,方便管理。

    public SmashAnimator with(View view) {
            // 每次都新建一个单独的SmashAnimator对象
            SmashAnimator animator = new SmashAnimator(this, view);
            mAnimators.add(animator);
            return animator;
        }
    

    SmashAnimator :

    不同于ExplosionFiled,这里的SmashAnimator没有直接继承ValueAnimator,而是在内部实例化了一个ValueAnimator。

    初始化:

    public SmashAnimator(ParticleSmasher view, View animatorView) {
            this.mContainer = view;
            init(animatorView);
        }
    
        private void init(View animatorView) {
            this.mAnimatorView = animatorView;
            mBitmap = mContainer.createBitmapFromView(animatorView);
            mRect = mContainer.getViewRect(animatorView);
            initValueAnimator();
            initPaint();
        }
    
        private void initValueAnimator() {
            mValueAnimator = new ValueAnimator();
            mValueAnimator.setFloatValues(0F, mEndValue);
            mValueAnimator.setInterpolator(DEFAULT_INTERPOLATOR);
        }
    
        private void initPaint() {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
        }
    

    然后,添加了一系列的set方法,用于设置动画时间、启动延时、动画类型、水平变化幅度、垂直变化幅度、粒子基础半径、动画回调事件等。同时,还有最重要的start()方法,用于开始动画:

    /**
         *   开始动画
         */
        public void start() {
            setValueAnimator();
            calculateParticles(mBitmap);
            hideView(mAnimatorView, mStartDelay);
            mValueAnimator.start();
            mContainer.invalidate();
        }
    

    setValueAnimator()方法会将链式调用设置的一系列值,赋给ValueAnimator对象:

    /**
         *   设置动画参数
         */
        private void setValueAnimator() {
            mValueAnimator.setDuration(mDuration);
            mValueAnimator.setStartDelay(mStartDelay);
            mValueAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    if (mOnExplosionListener != null) {
                        mOnExplosionListener.onAnimatorEnd();
                    }
                    mContainer.removeAnimator(SmashAnimator.this);
                }
    
                @Override
                public void onAnimationStart(Animator animation) {
                    if (mOnExplosionListener != null) {
                        mOnExplosionListener.onAnimatorStart();
                    }
    
                }
            });
        }
    

    与ExplosionFiled不同的是,这里的生成粒子方法calculateParticles(bitmap)中,并不是固定生成15*15个粒子,而是根据粒子的基础半径,计算需要的粒子数量,然后再通过判断动画类型,从而生成不同参数的粒子。(这里有一个问题,即进行动画的View过小的时候,生成的粒子数量不够多,这时候可以修改粒子基础半径大小,使得可以生成足够多的粒子):

    /**
         * 根据图片计算粒子
         * @param bitmap      需要计算的图片
         */
        private void calculateParticles(Bitmap bitmap) {
    
            int col = bitmap.getWidth() /(mRadius*2);
            int row = bitmap.getHeight() / (mRadius*2);
    
            Random random = new Random(System.currentTimeMillis());
            mParticles = new Particle[row][col];
    
            for (int i = 0; i < row; i++) {
                for (int j = 0; j < col; j++) {
                    int x=j * mRadius*2 + mRadius;
                    int y=i * mRadius*2 + mRadius;
                    int color = bitmap.getPixel(x, y);
                    Point point=new Point(mRect.left+x,mRect.top+y);
    
                    switch (mStyle){
                        case STYLE_EXPLOSION:
                            mParticles[i][j] = new ExplosionParticle(color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                        case STYLE_DROP:
                            mParticles[i][j] = new DropParticle(point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                        case STYLE_FLOAT_LEFT:
                            mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_LEFT,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                        case STYLE_FLOAT_RIGHT:
                            mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_RIGHT,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                        case STYLE_FLOAT_TOP:
                            mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_TOP,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                        case STYLE_FLOAT_BOTTOM:
                            mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_BOTTOM,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                            break;
                    }
    
                }
            }
            mBitmap.recycle();
            mBitmap = null;
        }
    

    最后是draw(canvas)方法,由ParticleSmasher中的onDraw()方法调用,用于循环绘制粒子,并根据动画进程调用粒子的advance方法,来改变粒子的参数:

    /**
         *   开始逐个绘制粒子
         *   @param canvas  绘制的画板
         *   @return 是否成功
         */
        public boolean draw(Canvas canvas) {
            if (!mValueAnimator.isStarted()) {
                return false;
            }
            for (Particle[] particle : mParticles) {
                for (Particle p : particle) {
                    // 根据动画进程,修改粒子的参数
                    p.advance((float) (mValueAnimator.getAnimatedValue()), mEndValue);
                    if (p.alpha > 0) {
                        mPaint.setColor(p.color);
                        mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
                        canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
                    }
                }
            }
            mContainer.invalidate();
            return true;
        }
    

    粒子实体类:

    • Particle:包含粒子各项参数,和改变粒子参数的advance()方法。

    在ExplosionFiled的基础上,我删除了top、bottom、mag、neg参数,新增了horizontalElement、verticalElement参数,一个是粒子水平变化参数,一个是垂直变化参数,这样更直观一些。同时,将life修改为font,overflow修改为later。

    public abstract class Particle {
    
        public int color;                // 颜色
        public float radius;             // 半径
        public float alpha;              // 透明度(0~1)
        public float cx;                 // 圆心 x
        public float cy;                 // 圆心 y
    
    
        public float horizontalElement;  // 水平变化参数
        public float verticalElement;    // 垂直变化参数
    
        public float baseRadius;         // 初始半径,同时负责半径大小变化
        public float baseCx;             // 初始圆心 x
        public float baseCy;             // 初始圆心 y
    
        public float font;               // 决定了粒子在动画开始多久之后,开始显示
        public float later;              // 决定了粒子动画结束前多少时间开始隐藏
    
        public void advance(float factor, float endValue) {
        }
    }
    
    
    • ExplosionParticle、DropParticle、FloatParticle:爆炸粒子、坠落粒子、飘落粒子。都继承了Particle,通过构造方法,生成粒子,通过advance方法,在动画进程中改变粒子参数。

    这里以ExplosionParticle(爆炸效果的粒子)为例,我们用构造方法来初始化粒子的参数:


    爆炸粒子初始化参数

    这里最重要且最值得注意的是horizontalElement和verticalElement的生成,用到了horizontalMultiple和verticalMultiple,即变化幅度,也可以理解为变化倍数,即粒子可以到达多远的距离,这个值越大,粒子运动得越远,反之亦然。

     private static float getHorizontalElement(Rect rect, Random random, float nextFloat,float horizontalMultiple) {
    
            // 第一次随机运算:h=width*±(0.01~0.49)
            float horizontal = rect.width() * (random.nextFloat() - 0.5f);
    
            // 第二次随机运行: h= 1/5概率:h;3/5概率:h*0.6; 1/5概率:h*0.3; nextFloat越大,h越小。
            horizontal = nextFloat < 0.2f ? horizontal :
                    nextFloat < 0.8f ? horizontal * 0.6f : horizontal * 0.3f;
    
            // 上面的计算是为了让横向变化参数有随机性,下面的计算是修改横向变化的幅度。
            return horizontal * horizontalMultiple;
        }
    
        private static float getVerticalElement(Rect rect, Random random, float nextFloat,float verticalMultiple) {
    
            // 第一次随机运算: v=height*(0.5~1)
            float vertical = rect.height() * (random.nextFloat() * 0.5f + 0.5f);
    
            // 第二次随机运行: v= 1/5概率:v;3/5概率:v*1.2; 1/5概率:v*1.4; nextFloat越大,h越大。
            vertical = nextFloat < 0.2f ? vertical :
                    nextFloat < 0.8f ? vertical * 1.2f : vertical * 1.4f;
    
            // 上面的计算是为了让变化参数有随机性,下面的计算是变化的幅度。
            return vertical * verticalMultiple;
        }
        
    

    比如在比较扁平的控件中,因为verticalElement是基于控件的height,进行一系列随机运算而生成的,因此如果不增大verticalMultiple的值的话,粒子的垂直运动范围是很有限的距离,因此可以适当增加verticalMultiple,这样会更美观。

    粒子的变化方法advance(),在去掉了一些功能重复的参数以及对公式进行简化之后,advance方法逻辑清晰了许多:

     public void advance(float factor, float endValue) {
    
            // 动画进行到了几分之几
            float normalization = factor / endValue;
    
            if (normalization < font || normalization > 1f - later) {
                alpha = 0;
                return;
            }
            alpha = 1;
    
            // 粒子可显示的状态中,动画实际进行到了几分之几
            normalization = (normalization - font) / (1f - font - later);
            // 动画超过7/10,则开始逐渐变透明
            if (normalization >= 0.7f) {
                alpha = 1f - (normalization - 0.7f) / 0.3f;
            }
    
            float realValue = normalization * endValue;
    
            // y=j+k*x,j、k都是常数,x为 0~1.4
            cx = baseCx + horizontalElement * realValue;
    
            // y=j+k*(x*(x-1),j、k都是常数,x为 0~1.4
            cy = baseCy + verticalElement * (realValue * (realValue - 1));
    
            radius = baseRadius + baseRadius / 4 * realValue;
    
        }
    

    其余的几种动画效果与爆炸粒子有些细微的差别,比如初始化粒子的baseCx、baseCy位置不同;粒子变化过程中飘落的粒子需要判断是否已经到了该变化的时候等。这些就不一一写出来了,感兴趣的可以去看一下代码,注释基本都讲的很清楚了。

    这大概是2017年最后一篇博客了吧,希望新的一年,可以有更多的进步,共勉!

    相关文章

      网友评论

          本文标题:Android粒子破碎效果(2)——实现多种破碎效果之Parti

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