开源项目ExplosionField(爆炸特效)源码分析

这是前几日在朋友圈传疯了的开源项目 如果没记错的话 小米手机卸载应用的时候就是使用的这个效果 于是我去github fork 了这个项目 地址如下:

点击打开链接



效果图:


我使用的IDE 是 android studio

我把源码 和范例程序简单的移植到了android studio 然后随便拿了几个图(其实是QQ空间apk里的)

工程目录如下


其实很简单啦 就是个activity  点击其中每个 view 就会产生爆炸特效  首先view会颤抖下 然后爆炸

所有的源码都有注释 如果有错 欢迎指出

下载: 点击打开链接


MainActivity.java

package com.chan.explosionfield;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;

public class MainActivity extends AppCompatActivity {

    //爆炸区域
    private ExplosionField mExplosionField;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mExplosionField = ExplosionField.attach2Window(this);
        addListener(findViewById(R.id.root));
    }

    //给需要爆炸的视图添加到爆炸区域中
    private void addListener(View root) {

        //如果是view group 类型 就把它的子视图添加到区域中
        if (root instanceof ViewGroup) {
            ViewGroup parent = (ViewGroup) root;
            for (int i = 0; i < parent.getChildCount(); i++) {
                addListener(parent.getChildAt(i));
            }
        }

        //这里是View 类型的视图
        else {

            //设置它为可点击的
            root.setClickable(true);

            //添加监听器
            root.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                    //爆炸该视图
                    mExplosionField.explode(v);
                    //取消注册其点击事件
                    v.setOnClickListener(null);
                }
            });
        }
    }
}

到这里不得不看explision field的源码 爆炸特效从explode那个函数开始

ExplisionField.java

/*
 * Copyright (C) 2015 tyrantgit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.chan.explosionfield;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;


/**
 * 爆炸区域
 */
public class ExplosionField extends View {
    
    //爆炸的动画
    private List<ExplosionAnimator> mExplosions = new ArrayList<>();
    private int[] mExpandInset = new int[2];
    

    //ctor
    public ExplosionField(Context context) {
        super(context);
        init();
    }

    public ExplosionField(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ExplosionField(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        Arrays.fill(mExpandInset, Utils.dp2Px(32));
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //这里配合Explosion Animator的draw互相调用 知道用完动画的播放时间
        for (ExplosionAnimator explosion : mExplosions) {
            explosion.draw(canvas);
        }
    }

    public void expandExplosionBound(int dx, int dy) {
        mExpandInset[0] = dx;
        mExpandInset[1] = dy;
    }


    public void explode(Bitmap bitmap, Rect bound, long startDelay, long duration) {

        //产生爆炸的动画 并且启动它
        final ExplosionAnimator explosion = new ExplosionAnimator(this, bitmap, bound);
        explosion.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mExplosions.remove(animation);
            }
        });
        explosion.setStartDelay(startDelay);
        explosion.setDuration(duration);
        mExplosions.add(explosion);
        explosion.start();
    }

    /**
     * 引爆view
     * @param view 即将被引爆的view
     */

    private int i = 0;

    public void explode(final View view) {

        //获得它被可见的区域
        Rect r = new Rect();
        view.getGlobalVisibleRect(r);

        //获得当前视图在屏幕中的位置
        int[] location = new int[2];
        getLocationOnScreen(location);

        //偏移rect 但是我没能理解这个意思
        r.offset(-location[0], -location[1]);
        r.inset(-mExpandInset[0], -mExpandInset[1]);

        int startDelay = 100;

        //这个动画使得view “振动”
        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.start();

        //让其逐渐变小 然后消失
        view.animate().setDuration(150)
                .setStartDelay(startDelay)
                .scaleX(0f).scaleY(0f)
                .alpha(0f).start();

        //爆炸相关的视图
        explode(Utils.createBitmapFromView(view),
                r,
                startDelay,
                ExplosionAnimator.DEFAULT_DURATION
        );
    }

    public void clear() {
        mExplosions.clear();
        invalidate();
    }

    //获得爆炸区域
    public static ExplosionField attach2Window(Activity activity) {

        //获得MainActivity layout的 根布局的父布局
        //在activity中 setContentView 会在当前布局文件外再套一个父布局
        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
        ExplosionField explosionField = new ExplosionField(activity);

        //将爆炸区域添加到其中
        //ExplosionField extents View
        rootView.addView(explosionField, new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

        //返回爆炸区域
        return explosionField;
    }
}

刚刚在MainActivity看到ExplosionField是由attach2Window这个方法产生  这里的注释是很完整的


爆炸时会产生当前view 的快照 然后根据快照 取其中的像素 作为爆炸烟火的颜色 这个均由Utils.java生成


Utils.java

/*
 * Copyright (C) 2015 tyrantgit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.chan.explosionfield;


import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.widget.ImageView;

public class Utils {

    private Utils() {
    }

    /**
     * 像素密度
     */
    private static final float DENSITY = Resources.getSystem().getDisplayMetrics().density;
    /**
     * 用来绘图
     */
    private static final Canvas sCanvas = new Canvas();

    /**
     * 将dp 转为 像素
     * @param dp
     * @return
     */
    public static int dp2Px(int dp) {
        return Math.round(dp * DENSITY);
    }

    /**
     * 从视图获得它的图像
     * @param view 要爆炸的view
     * @return 它的图像
     */
    public static Bitmap createBitmapFromView(View view) {

        //如果当前的是ImageView 类型
        //那么最方便了 它的Drawable 是 BitmapDrawable的
        //可以直接获得其中的图
        if (view instanceof ImageView) {
            Drawable drawable = ((ImageView) view).getDrawable();
            if (drawable != null && drawable instanceof BitmapDrawable) {
                return ((BitmapDrawable) drawable).getBitmap();
            }
        }

        //如果不是
        //那么首先 就要使他失去焦点
        //因为获得了焦点的视图可能会随时就改变
        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);

                //将视图绘制在canvas中
                view.draw(canvas);

                //然后一处空白的快照
                //以此来获得真正的视图快照
                canvas.setBitmap(null);
            }
        }

        //现在空白的快照已经有了view的样子
        //是真正的快照了
        return bitmap;
    }

    /**
     * 创建一个和指定尺寸大小一样的bitmap
     * @param width 宽
     * @param height 高
     * @param config 快照配置 详见 {@link android.graphics.Bitmap.Config}
     * @param retryCount 当生成空白bitmap发生oom时  我们会尝试再试试生成bitmap 这个为尝试的次数
     * @return 一个和指定尺寸大小一样的bitmap
     */
    public static Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int retryCount) {
        try {
            //创建空白的bitmap
            return Bitmap.createBitmap(width, height, config);

            //如果发生了oom
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            if (retryCount > 0) {
                //主动gc 然后再次试试
                System.gc();
                return createBitmapSafely(width, height, config, retryCount - 1);
            }

            //直到次数用光
            return null;
        }
    }
}


最后的特效都是在动画里面产生的


/*
 * Copyright (C) 2015 tyrantgit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 package com.chan.explosionfield;

import android.animation.ValueAnimator;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;

import java.util.Random;

public class ExplosionAnimator extends ValueAnimator {

    /**
     * 默认的播放时间
     */
    static long DEFAULT_DURATION = 0x400;
    /**
     * 加速度补间器
     */
    private static final Interpolator DEFAULT_INTERPOLATOR = new AccelerateInterpolator(0.6f);
    private static final float END_VALUE = 1.4f;
    private static final float X = Utils.dp2Px(5);
    private static final float Y = Utils.dp2Px(20);
    private static final float V = Utils.dp2Px(2);
    private static final float W = Utils.dp2Px(1);

    //绘制的画笔
    private Paint mPaint;
    private Particle[] mParticles;
    //要绘制的区域
    private Rect mBound;
    //要爆炸的view
    private View mContainer;

    public ExplosionAnimator(View container, Bitmap bitmap, Rect bound) {

        //用来画烟花
        mPaint = new Paint();
        //爆炸区域
        mBound = new Rect(bound);

        //生成爆炸烟花点
        int partLen = 15;
        mParticles = new Particle[partLen * partLen];

        //随机的从生成的快照里获得颜色 用作烟花点的颜色
        Random random = new Random(System.currentTimeMillis());
        int w = bitmap.getWidth() / (partLen + 2);
        int h = bitmap.getHeight() / (partLen + 2);

        for (int i = 0; i < partLen; i++) {
            for (int j = 0; j < partLen; j++) {

                //要取颜色的位置
                final int x = (j + 1) * w;
                final int y = (i + 1) * h;

                //获取颜色
                final int color = bitmap.getPixel(x, y);
                //生成爆炸粒子
                mParticles[(i * partLen) + j] = generateParticle(color, random);
            }
        }

        //保存当前的视图
        mContainer = container;

        //设置值
        setFloatValues(0f, END_VALUE);
        //设置补间器
        setInterpolator(DEFAULT_INTERPOLATOR);
        //设置动画时长
        setDuration(DEFAULT_DURATION);
    }

    /**
     * 生成爆炸粒子
     * @param color 爆炸粒子的颜色
     * @param random
     * @return 爆炸粒子
     */
    private Particle generateParticle(int color, Random random) {

        //生成烟花点
        Particle particle = new Particle();
        particle.color = color;

        //设置半径
        particle.radius = V;

        //产生随机大小的base radius
        if (random.nextFloat() < 0.2f) {
            particle.baseRadius = V + ((X - V) * random.nextFloat());
        } else {
            particle.baseRadius = W + ((V - W) * random.nextFloat());
        }

        float nextFloat = random.nextFloat();
        particle.top = mBound.height() * ((0.18f * random.nextFloat()) + 0.2f);
        particle.top = nextFloat < 0.2f ? particle.top : particle.top + ((particle.top * 0.2f) * random.nextFloat());
        particle.bottom = (mBound.height() * (random.nextFloat() - 0.5f)) * 1.8f;
        float f = nextFloat < 0.2f ? particle.bottom : nextFloat < 0.8f ? particle.bottom * 0.6f : particle.bottom * 0.3f;
        particle.bottom = f;
        particle.mag = 4.0f * particle.top / particle.bottom;
        particle.neg = (-particle.mag) / particle.bottom;
        f = mBound.centerX() + (Y * (random.nextFloat() - 0.5f));
        particle.baseCx = f;
        particle.cx = f;
        f = mBound.centerY() + (Y * (random.nextFloat() - 0.5f));
        particle.baseCy = f;
        particle.cy = f;
        particle.life = END_VALUE / 10 * random.nextFloat();
        particle.overflow = 0.4f * random.nextFloat();
        particle.alpha = 1f;
        return particle;
    }

    public boolean draw(Canvas canvas) {
        //直到播放完动画
        if (!isStarted()) {
            return false;
        }

        //遍历烟花点 然后绘制
        for (Particle particle : mParticles) {

            //设置烟花点的属性
            particle.advance((float) getAnimatedValue());

            //如果不是透明的 那就绘制出来
            if (particle.alpha > 0f) {
                mPaint.setColor(particle.color);
                mPaint.setAlpha((int) (Color.alpha(particle.color) * particle.alpha));
                canvas.drawCircle(particle.cx, particle.cy, particle.radius, mPaint);
            }
        }

        //这里配合view 的 draw 互相调用
        mContainer.invalidate();
        return true;
    }

    @Override
    public void start() {
        super.start();
        //这里配合view 的 draw 互相调用
        mContainer.invalidate(mBound);
    }

    private class Particle {
        float alpha;
        int color;
        float cx;
        float cy;
        float radius;
        float baseCx;
        float baseCy;
        float baseRadius;
        float top;
        float bottom;
        float mag;
        float neg;
        float life;
        float overflow;

        public void advance(float factor) {
            float f = 0f;

            //这代表一个烟花点消逝的条件
            float normalization = factor / END_VALUE;
            if (normalization < life || normalization > 1f - overflow) {
                alpha = 0f;
                return;
            }

            //然后计算出烟花点的半径 坐标 透明度参数
            //纯数学计算
            normalization = (normalization - life) / (1f - life - overflow);
            float f2 = normalization * END_VALUE;
            if (normalization >= 0.7f) {
                f = (normalization - 0.7f) / 0.3f;
            }
            alpha = 1f - f;
            f = bottom * f2;
            cx = baseCx + f;
            cy = (float) (baseCy - this.neg * Math.pow(f, 2.0)) - f * mag;
            radius = V + (baseRadius - V) * f2;
        }
    }
}


  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值