前几天微博上被一个很优秀的 Android 开源组件刷屏了 - ExplosionField,效果非常酷炫,有点类似 MIUI 卸载 APP 时的动画,先来感受一下。

ExplosionField 不但效果很拉风,代码写得也相当好,让人忍不住要拿来好好读一下。


创建 ExplosionField

ExplosionField 继承自 View,在 onDraw 方法中绘制动画特效,并且它提供了一个 attach3Window 方法,可以把 ExplosionField 最为一个子 View 添加到 Activity 上的 root view 中。

publicstaticExplosionFieldattach3Window(Activityactivity){ViewGrouprootView=(ViewGroup)activity.findViewById(Window.ID_ANDROID_CONTENT);ExplosionFieldexplosionField=newExplosionField(activity);rootView.addView(explosionField,newViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT));returnexplosionField;}1234567

explosionFieldLayoutParams 属性都被设置为 MATCH_PARENT
这样一来,一个 view 炸裂出来的粒子可以绘制在整个 Activity 所在的区域。

知识点:可以用 Window.ID_ANDROID_CONTENT 来替代 android.R.id.content

炸裂之前的震动效果

在 View 的点击事件中,调用 mExplosionField.explode(v)之后,View 首先会震动,然后再炸裂。

震动效果比较简单,设定一个 [0, 1] 区间 ValueAnimator,然后在 AnimatorUpdateListeneronAnimationUpdate 中随机平移 x 和 y坐标,最后把 scale 和 alpha 值动态减为 0。

intstartDelay=100;ValueAnimatoranimator=ValueAnimator.ofFloat(0f,1f).setDuration(150);animator.addUpdateListener(newValueAnimator.AnimatorUpdateListener(){Randomrandom=newRandom();@OverridepublicvoidonAnimationUpdate(ValueAnimatoranimation){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();123456789101112131415根据 View 创建一个 bitmap

View 震动完了就开始进行最难的炸裂,并且炸裂是跟隐藏同时进行的,先来看一下炸裂的 API - void explode(Bitmap bitmap, Rect bound, long startDelay, long duration)

前两个参数 bitmap 和 bound 是关键,通过 View 来创建 bitmap 的代码比较有意思。

如果 View 是一个 ImageView,并且它的 Drawable 是一个 BitmapDrawable 就可以直接获取这个 Bitmap。

if(viewinstanceofImageView){Drawabledrawable=((ImageView)view).getDrawable();if(drawable!=null&&drawableinstanceofBitmapDrawable){return((BitmapDrawable)drawable).getBitmap();}}123456

如果不是一个 ImageView,可以按照如下步骤创建一个 bitmap:

新建一个 Canvas

根据 View 的大小创建一个空的 bitmap

把空的 bitmap 设置为 Canvas 的底布

把 view 绘制在 canvas上

把 canvas 的 bitmap 设置成 null

当然,绘制之前要清掉 View 的焦点,因为焦点可能会改变一个 View 的 UI 状态。
一下代码中用到的 sCanvas 是一个静态变量,这样可以节省每次创建时产生的开销。

view.clearFocus();Bitmapbitmap=createBitmapSafely(view.getWidth(),view.getHeight(),Bitmap.Config.ARGB_8888,1);if(bitmap!=null){synchronized(sCanvas){Canvascanvas=sCanvas;canvas.setBitmap(bitmap);view.draw(canvas);canvas.setBitmap(null);}}1234567891011

作者创建位图的办法非常巧妙,如果新建 Bitmap 时产生了 OOM,可以主动进行一次 GC - System.gc(),然后再次尝试创建。

这个函数的实现方式让人佩服作者的功力。

publicstaticBitmapcreateBitmapSafely(intwidth,intheight,Bitmap.Configconfig,intretryCount){try{returnBitmap.createBitmap(width,height,config);}catch(OutOfMemoryErrore){e.printStackTrace();if(retryCount>0){System.gc();returncreateBitmapSafely(width,height,config,retryCount-1);}returnnull;}}123456789101112

出了 bitmap,还有一个一个很重要的参数 bound,它的创建相对比较简单:

Rectr=newRect();view.getGlobalVisibleRect(r);int[]location=newint[2];getLocationOnScreen(location);r.offset(-location[0],-location[1]);r.inset(-mExpandInset[0],-mExpandInset[1]);123456

首先获取 需要炸裂的View 的全局可视区域 - Rect r,然后通过 getLocationOnScreen(location) 获取 ExplosionField 在屏幕中的坐标,并根据这个坐标把 炸裂View 的可视区域进行平移,这样炸裂效果才会显示在 ExplosionField 中,最后根据 mExpandInset 值(默认为 0)扩展一下。

那创建的 bitmap 和 bound 有什么用呢?我们继续往下分析。

创建粒子

先来看一下炸裂成粒子这个方法的全貌:

publicvoidexplode(Bitmapbitmap,Rectbound,longstartDelay,longduration){finalExplosionAnimatorexplosion=newExplosionAnimator(this,bitmap,bound);explosion.addListener(newAnimatorListenerAdapter(){@OverridepublicvoidonAnimationEnd(Animatoranimation){mExplosions.remove(animation);}});explosion.setStartDelay(startDelay);explosion.setDuration(duration);mExplosions.add(explosion);explosion.start();}12345678910111213

这里要解释一下为什么用一个容器类变量 - mExplosions 来保存一个 ExplosionAnimator。因为 activity 中多个 View 的炸裂效果可能要同时进行,所以要把每个 View 对应的炸裂动画保存起来,等动画结束的时候再删掉。

作者自定义了一个继承自 ValueAnimator 的类 - ExplosionAnimator,它主要做了两件事情,一个是创建粒子 - generateParticle,另一个是绘制粒子 - draw(Canvas canvas)

先来看一下构造函数:

publicExplosionAnimator(Viewcontainer,Bitmapbitmap,Rectbound){mPaint=newPaint();mBound=newRect(bound);intpartLen=15;mParticles=newParticle[partLen*partLen];Randomrandom=newRandom(System.currentTimeMillis());intw=bitmap.getWidth()/(partLen+2);inth=bitmap.getHeight()/(partLen+2);for(inti=0;i<partLen;i++){for(intj=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);}123456789101112131415161718

根据构造函数可以知道作者把 bitmap 分成了一个 17 x 17 的矩阵,每个元素的宽度和高度分别是 wh

intw=bitmap.getWidth()/(partLen+2);inth=bitmap.getHeight()/(partLen+2);12

所有的粒子是一个 15 x 15 的矩阵,元素色值是位图对应的像素值。

bitmap.getPixel((j+1)*w,(i+1)*h)1

结构如下图所示,其中空心部分是粒子。

●●●●●●●●●●●●●●●●●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●○○○○○○○○○○○○○○○●●●●●●●●●●●●●●●●●●

generateParticle 会根据一定的算法随机地生成一个粒子。这部分比较繁琐,分析略去。

其中比较巧妙的还是它的 draw 方法:

publicbooleandraw(Canvascanvas){if(!isStarted()){returnfalse;}for(Particleparticle: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();returntrue;}123456789101112131415

刚开始我还一直比较困惑,既然绘制粒子是在 ExplosionFieldonDraw 方法中进行,那肯定需要不停地刷新,结果作者并不是这么做的,实现方法又着实惊艳了一把。

首先,作者在 ExplosionAnimator 类中重载了 start() 方法,通过调用 mContainer.invalidate(mBound) 来刷新 将要炸裂的 View 所对应的区块。

@Overridepublicvoidstart(){super.start();mContainer.invalidate(mBound);}12345

而 mContainer 即是占满了 activity 的 view - ExplosionField,它的 onDraw 方法中又会调用 ExplosionAnimatordraw 方法。

@OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas);for(ExplosionAnimatorexplosion:mExplosions){explosion.draw(canvas);}}1234567

这样便形成了一个递归,两者相互调用,不停地刷新,直到所有粒子的 alpha 值变为 0,刷新就停下来了。

publicbooleandraw(Canvascanvas){if(!isStarted()){returnfalse;}for(Particleparticle: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();returntrue;}123456789101112131415总结

这个开源库的代码质量相当高,十分佩服作者。



更多特效。。《IT蓝豹》