Android粒子爆炸特效[可用于任意控件]

小米手机用户可以看到,小米手机在应用卸载时会有一个粒子爆炸的特效效果,对此类动画效果垂涎已久,奈何一直没有机会用。正好最近项目里需要用到粒子爆炸的特效,于是便抽时间自己也试着仿写了一个效果出来。

粒子爆炸

先看下效果:

粒子特效

How to use:



Step 1. Add the JitPack repository to your build file
Add it in your root build.gradle at the end of repositories:

    allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }

Step 2.Add the dependency

    dependencies {
            compile 'com.github.zhaolei9527:Particle-master:v1.0.1'
    }

代码中这样使用:

首先提供了响应式触发的方式,首先进行埋雷,当控件被点击时,触发爆炸。是不是很刺激?

   //目前提供了六种的粒子爆炸特效
        explosionSite1 = new ExplosionSite(this, new BooleanFactory());
        explosionSite2 = new ExplosionSite(this, new ExplodeParticleFactory());
        explosionSite3 = new ExplosionSite(this, new FallingParticleFactory());
        explosionSite4 = new ExplosionSite(this, new FlyawayFactory());
        explosionSite5 = new ExplosionSite(this, new InnerFallingParticleFactory());
        explosionSite6 = new ExplosionSite(this, new VerticalAscentFactory());

        //爆炸激活方式一:将View或ViewGroup添加至雷管监听,View被点击时,触发爆炸
        explosionSite1.addListener(img_1);
        explosionSite2.addListener(img_2);
        explosionSite3.addListener(img_6);
        explosionSite4.addListener(img_4);
        explosionSite5.addListener(img_5);
        explosionSite6.addListener(img_3);

其次针对一些情况,提供了另外一种直接的触发方式。是不是更刺激?

                //爆炸激活方式二:将View或ViewGroup直接点燃爆炸
                explosionSite1.explode(img_1);
                explosionSite2.explode(img_2);
                explosionSite3.explode(img_3);
                explosionSite4.explode(img_4);
                explosionSite5.explode(img_5);
                explosionSite6.explode(img_6);

看下原理:GitHub项目地址
网上实现类似相同效果的很多,基本规则也都差不多。
主要对象如下:
ExplosionSite:爆炸效果发生的场地,是一个View。当一个控件需要爆炸时,需要为控件生成一个ExplosionSite,这个ExplosionSite覆盖整个屏幕,于是我们才能看到完整的爆炸效果,在ExplosionField的构造函数中,传入不同的ParticleFactory,就可以生成不同的爆炸效果。

ExplosionAnimator:爆炸动画,其实是一个计时器,继承自ValueAnimator。0x400s内,完成爆炸动画,每次计时,就更新所有粒子的运动状态。draw()方法是它最重要的方法,也就是使所有粒子重绘自身,从而实现动画效果。

ParticleFactory:是一个抽象类。用于产生粒子数组,不同的ParticleFactory可以产生不同类型的粒子数组。

Particle:抽象的粒子类。代表粒子本身,必须拥有的属性包括,当前自己的cx,cy坐标和颜色color。必须实现两个方法,draw()方法选择怎么绘制自身(圆形还是方形等),caculate()计算当前时间,自己所处的位置。


实现原理:
1、获取当前控件背景bitmap
例如,例子中使用的是imageview,对于控件,在Utils中提供有一个工具类,可以获得其背景的Bitmap对象

   public static Bitmap createBitmapFromView(View view) {
        view.clearFocus();
        Bitmap bitmap = createBitmapSafely(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888, 1);
        if (bitmap != null) {
            synchronized (sCanvas) {
                Canvas canvas = sCanvas;
                canvas.setBitmap(bitmap);
                view.draw(canvas);
                canvas.setBitmap(null);
            }
        }
        return bitmap;
    }

    public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
        try {
            return Bitmap.createBitmap(width, height, config);
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            if (retryCount > 0) {
                System.gc();
                return createBitmapSafely(width, height, config, retryCount - 1);
            }
            return null;
        }
    }

2、将View镜像转换成粒子对象
获取Bitmap以后,我们交给工厂对象进行加工,根据Bitmap生产Particle数组。
我们知道Bitmap可以看成是一个像素矩阵,矩阵上面的点,就是一个个带有颜色的像素,于是我们可以获取到每个点的颜色和位置,组装成一个对象Particle,这么一来,Particle就代表带有颜色的点了。

public abstract class ParticleFactory {
    public abstract Particle[][] generateParticles(Bitmap bitmap, Rect bound);
}

简单来说都一样,就是拿要爆炸的View制作一个镜像出来然后返回。

然后来看一个粒子效果的具体实现。

  public Particle[][] generateParticles(Bitmap bitmap, Rect bound) {
        int w = bound.width();
        int h = bound.height();

        int partW_Count = w / PART_WH; //横向个数
        int partH_Count = h / PART_WH; //竖向个数

        int bitmap_part_w = bitmap.getWidth() / partW_Count;
        int bitmap_part_h = bitmap.getHeight() / partH_Count;

        Particle[][] particles = new Particle[partH_Count][partW_Count];
        for (int row = 0; row < partH_Count; row ++) { //行
            for (int column = 0; column < partW_Count; column ++) { //列
                //取得当前粒子所在位置的颜色
                int color = bitmap.getPixel(column * bitmap_part_w, row * bitmap_part_h);

                float x = bound.left + BooleanFactory.PART_WH * column;
                float y = bound.top + BooleanFactory.PART_WH * row;
                particles[row][column] = new BooleanParticle(color,x,y,bound);
            }
        }

        return particles;
    }

很简单,其中Rect类型的bound,是代表原来View控件的宽高信息。
根据我们设定的每个粒子的大小,和控件的宽高,我们就可以计算出,有多少个粒子组成这个控件的背景。
我们取得每个粒子所在位置的颜色,位置,用于生产粒子,这就是BooleanParticle。

3、爆炸对象及主要流程

public class ExplosionSite extends View {
    private static final String TAG = "ExplosionField";
    private ArrayList<ExplosionAnimator> explosionAnimators;
    private HashMap<View, ExplosionAnimator> explosionAnimatorsMap;
    private OnClickListener onClickListener;
    private ParticleFactory mParticleFactory;

    public ExplosionSite(Context context, ParticleFactory particleFactory) {
        super(context);
        init(particleFactory);
    }

    public ExplosionSite(Context context, AttributeSet attrs, ParticleFactory particleFactory) {
        super(context, attrs);
        init(particleFactory);
    }

    private void init(ParticleFactory particleFactory) {
        explosionAnimators = new ArrayList<ExplosionAnimator>();
        explosionAnimatorsMap = new HashMap<View, ExplosionAnimator>();
        mParticleFactory = particleFactory;
        attach2Activity((Activity) getContext());
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (ExplosionAnimator animator : explosionAnimators) {
            animator.draw(canvas);
        }
    }

    /**
     * 爆破
     *
     * @param view 使得该view爆破
     */
    public void explode(final View view) {
        //防止重复点击
        if (explosionAnimatorsMap.get(view) != null && explosionAnimatorsMap.get(view).isStarted()) {
            return;
        }
        if (view.getVisibility() != View.VISIBLE || view.getAlpha() == 0) {
            return;
        }

        final Rect rect = new Rect();
        view.getGlobalVisibleRect(rect); //得到view相对于整个屏幕的坐标
        int contentTop = ((ViewGroup) getParent()).getTop();
        Rect frame = new Rect();
        ((Activity) getContext()).getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
        int statusBarHeight = frame.top;
        rect.offset(0, -contentTop - statusBarHeight);//去掉状态栏高度和标题栏高度
        if (rect.width() == 0 || rect.height() == 0) {
            return;
        }

        //震动动画
        ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            Random random = new Random();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                view.setTranslationX((random.nextFloat() - 0.5f) * view.getWidth() * 0.05f);
                view.setTranslationY((random.nextFloat() - 0.5f) * view.getHeight() * 0.05f);
            }
        });
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                explode(view, rect);
            }
        });
        animator.start();
    }

    private void explode(final View view, Rect rect) {
        final ExplosionAnimator animator = new ExplosionAnimator(this, Utils.createBitmapFromView(view), rect, mParticleFactory);
        explosionAnimators.add(animator);
        explosionAnimatorsMap.put(view, animator);
        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                //缩小,透明动画
                view.animate().setDuration(150).scaleX(0f).scaleY(0f).alpha(0f).start();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                view.animate().alpha(1f).scaleX(1f).scaleY(1f).setDuration(150).start();

                //动画结束时从动画集中移除
                explosionAnimators.remove(animation);
                explosionAnimatorsMap.remove(view);
                animation = null;
            }
        });
        animator.start();
    }

    /**
     * 给Activity加上全屏覆盖的ExplosionField
     */
    private void attach2Activity(Activity activity) {
        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);

        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        rootView.addView(this, lp);
    }


    /**
     * 希望谁有破碎效果,就给谁加Listener
     *
     * @param view 可以是ViewGroup
     */
    public void addListener(View view) {
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            int count = viewGroup.getChildCount();
            for (int i = 0; i < count; i++) {
                addListener(viewGroup.getChildAt(i));
            }
        } else {
            view.setClickable(true);
            view.setOnClickListener(getOnClickListener());
        }
    }

    private OnClickListener getOnClickListener() {
        if (null == onClickListener) {
            onClickListener = new OnClickListener() {
                @Override
                public void onClick(View v) {
                    ExplosionSite.this.explode(v);
                }
            };
        }
        return onClickListener;
    }
}

总结:总体来说实现很简单,就是根据工厂类,生成粒子数组。
而其实质是一个ValueAnimator,在一定时间内,从0数到1。
然后提供了一个draw()方法,方法里面调用了每个粒子的advance()方法,并且传入了当前数到的数字。
advance()方法里,其实调用了draw()方法和caculate()方法。

上面的实现,其实是一个固定的流程,添加了爆炸场地以后,我们就开始从0数到1,在这个过程中,粒子会根据当前时间,绘制自己的位置,所以粒子的位置,其实是它自己决定的,和流程无关。
也就是说,我们只要用不同的算法,绘制粒子的位置即可,实现了流程和粒子运动的分离。
所以除了提供的六种爆炸特效之外,只要遵循这个原则,就能生成更多的粒子爆炸特效。

有了需求才有了功能,有了想法才有了创作,你的反馈会是使我进步的最大动力。
觉得还不够方便?还想要什么功能?告诉我!欢迎反馈,欢迎Star。源码入口:GitHub项目地址

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

推荐阅读更多精彩内容