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

上一篇我们分析了开源项目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年最后一篇博客了吧,希望新的一年,可以有更多的进步,共勉!

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,510评论 25 707
  • 1 背景 不能只分析源码呀,分析的同时也要整理归纳基础知识,刚好有人微博私信让全面说说Android的动画,所以今...
    未聞椛洺阅读 2,691评论 0 10
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,024评论 4 62
  • 2017年6月11号对我来说是一次难忘的经历,跟随泰安行动派的小伙伴一起去济南参加刘洋老师关于如何打造财富自...
    兔小慧hui阅读 668评论 4 1
  • 有人说一天当中有一个小时可以从繁杂的琐事中抽离出来专注自己喜欢的事,这一天便是幸福的。于我来说,每天读书,写文字...
    若凡666阅读 225评论 0 1