第三方开源控件ExplosionField源码解析

笑谈风云,一语定乾坤。今天和大伙分享一个有意思的开源控件。

简介

先上一张图,大家感受下。

是不是酷炫?是不是似曾相识?在小米系统MIUI中的删除APK功能也是有上面这种粒子炸裂的效果的。下面给出github地址
如果无法访问的话,就直接在下面下载源码:

集成和使用

首先导入explosionfield做为library,并在gradle中添加好依赖。(也可以将library打包成aar文件,直接放入到libs目录中)。
做完以上步骤后,在Activity的onCreate方法中实例化ExplosionField。
    private ExplosionField mExplosionField;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mExplosionField = ExplosionField.attach2Window(this);
    }
然后在调用mExplosionField.explode(View)就可以实现炸裂效果了。


源码分析

集成和使用是比较简单,那么再看看具体是如何实现的:
首先,将ExplosionField依附到activity上。
ExplosionField.java
    public static ExplosionField attach2Window(Activity activity) {
        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
        ExplosionField explosionField = new ExplosionField(activity);
        rootView.addView(explosionField, new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        return explosionField;
    }
通过attach2Window()来实现了ExplosionField类的实例化。通过观察Window源码
Window.java
    public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
可知道findViewById(Window.ID_ANDROID_CONTENT)可以获取到activity的根布局。
然后通过设置ViewGroup.LayoutParams.MATCH_PARENT可以将炸裂区域设置为整个activity。
ExplosionField实例化:
在ExplosionField的构造器中有个init()方法,
ExplosionField.java
    private int[] mExpandInset = new int[2];

    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));
    }
Utils.java
    private static final float DENSITY = Resources.getSystem().getDisplayMetrics().density;
    private static final Canvas sCanvas = new Canvas();

    public static int dp2Px(int dp) {
        return Math.round(dp * DENSITY);
    }
默认将数组mExpandInset的值初始化为32dp对应的px.
初始化的时候,也没有做太多的操作。那么,再接着看看具体的炸裂方法。
ExplosionField.java
    public void explode(final View view) {
        Rect r = new Rect();
        view.getGlobalVisibleRect(r);
        int[] location = new int[2];
        getLocationOnScreen(location);
        r.offset(-location[0], -location[1]);
        r.inset(-mExpandInset[0], -mExpandInset[1]);
        int startDelay = 100;
        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);
    }
传入的参数是View。接着实例化一个矩形作为参数,调用view.getGlobalVisibleRect()这个方法。这个方法是用来获取控件视图在屏幕中的可视区域。然后通过调用getLocationOnScreen()获取控件视图在整个屏幕内的绝对坐标。
Rect.java
    public void offset(int dx, int dy) {
        left += dx;
        top += dy;
        right += dx;
        bottom += dy;
    }
    public void inset(int dx, int dy) {
        left += dx;
        top += dy;
        right -= dx;
        bottom -= dy;
    }
通过调用offset()、inset()来调整rect的位置。再通过ValueAnimator来实现view的抖动动画。
ExplosionField.java
    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();
    }
调用完抖动效果后,再开始实现粒子炸裂动画。粒子的炸裂方法explode()需要四个参数,第一个参数是Bitmap.
Utils.java
    public static Bitmap createBitmapFromView(View view) {
        if (view instanceof ImageView) {//从图片中创建Bitmap
            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);
                view.draw(canvas);
                canvas.setBitmap(null);
            }
        }
        return bitmap;
    }

    /**
     * 创建Bitmap 如果在创建Bitmap过程中内存不足,则手动触发一次GC,然后再尝试创建
     * @param width
     * @param height
     * @param config
     * @param retryCount
     * @return
     */
    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;
        }
    }
通过工具类来生成Bitmap。如果view是ImageView,则将ImageView转换成Bitmap,否则就生成一个默认的Bitmap.值得一提的是,在生成Bitmap的方法中,作者还考虑到了内存溢出问题,在捕获到内存溢出错误后,手动执行一次垃圾回收,然后再去尝试生成Bitmap。感觉有点6啊。
第二个参数是视图可见区域矩形。第三个参数是延时执行动画的毫秒数。最后一个参数是动画执行的时间长度。在explode方法中,首先实例化出ExplosionAnimator的一个实例,再将该实例加入到动画集合中,然后运行。再给该实例一个监听,当动画执行结束后,从动画集合中remove掉。看来,实现粒子炸裂效果应该就是ExplosionAnimator类中了。
ExplosionAnimator的构造方法:
ExplosionAnimator.java
    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;//区域矩阵
    private View mContainer;//视图View

    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++) {
                mParticles[(i * partLen) + j] = generateParticle(bitmap.getPixel((j + 1) * w, (i + 1) * h), random);
            }
        }
        mContainer = container;
        setFloatValues(0f, END_VALUE);//设置动画起点终点
        setInterpolator(DEFAULT_INTERPOLATOR);//设置变速器
        setDuration(DEFAULT_DURATION);//设置动画运行时间
    }
构造器ExplosionAnimator将Bitmap等分成了17*17个矩阵块,所有的粒子是15*15个,每个粒子的颜色是通过bitmap.getPixel(x,y)来获取到的。
ExplosionAnimator.java
    private Particle generateParticle(int color, Random random) {
        Particle particle = new Particle();
        particle.color = color;
        particle.radius = V;
        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();//0-0.14
        particle.overflow = 0.4f * random.nextFloat();//0-0.4
        particle.alpha = 1f;
        return particle;
    }
方法generateParticle()生成粒子,一个参数是生成粒子的颜色,还有一个是随机函数的实例。在generateParticle方法中,对粒子的透明度、颜色、圆心x/y坐标、圆半径、起点x/y坐标、初始圆半径、顶部距离、底部距离、生命周期等参数进行的初始化。
粒子类
Particle.java
    private class Particle {
        //粒子透明度
        float alpha;
        //粒子颜色
        int color;
        //圆心x坐标
        float cx;
        //圆心y坐标
        float cy;
        //圆半径
        float radius;
        //起点x坐标
        float baseCx;
        //起点Y坐标
        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;
        }
    }
这个类中声明了粒子运动的方法 advance(float),传入的参数是一个浮点型数值。也就是ValueAnimator的进度值。
ExplosionAnimator.java
    @Override
    public void start() {
        super.start();
        mContainer.invalidate(mBound);
    }
当ExplosionAnimator开始运行动画时,会调用其自身的start()方法,在其start()方法中又会通知mContainer这个ExplosionField的实例进行重绘。ExplosionField重绘会调用onDraw()方法
ExplosionField.java
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (ExplosionAnimator explosion : mExplosions) {
            explosion.draw(canvas);
        }
    }
在ExplosionField的onDraw()方法中又会去遍历ExplosionAnimator的集合,并逐一调用ExplosionAnimator的draw()方法
ExplosionAnimator.java
    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);
            }
        }
        mContainer.invalidate();
        return true;
    }
在ExplosionAnimator的draw()方法中去执行粒子的advance()方法来更新粒子状态,同时将粒子绘制出来,并通知mContainer进行重绘直到粒子的透明度为0停止。如此循环往复,则粒子炸裂的效果就跃然于眼前了。

整个View涉及到的代码量不大,但是读起来让人觉得酣畅淋漓。

















猜你喜欢

转载自blog.csdn.net/shirakawakanaki/article/details/53502787