前幾天微博上被一個(gè)很優(yōu)秀的 Android 開源組件刷屏了 - ExplosionField,效果非常酷炫,有點(diǎn)類似 MIUI 卸載 APP 時(shí)的動(dòng)畫,先來感受一下。
從事德陽電信服務(wù)器托管,服務(wù)器租用,云主機(jī),雅安服務(wù)器托管,主機(jī)域名,CDN,網(wǎng)絡(luò)代維等服務(wù)。
ExplosionField 不但效果很拉風(fēng),代碼寫得也相當(dāng)好,讓人忍不住要拿來好好讀一下。
ExplosionField
繼承自 View
,在 onDraw
方法中繪制動(dòng)畫特效,并且它提供了一個(gè) attach3Window
方法,可以把 ExplosionField
最為一個(gè)子 View 添加到 Activity 上的 root view 中。
public static ExplosionField attach3Window(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; }1234567
explosionField
的 LayoutParams
屬性都被設(shè)置為 MATCH_PARENT
,
這樣一來,一個(gè) view 炸裂出來的粒子可以繪制在整個(gè) Activity 所在的區(qū)域。
知識點(diǎn):可以用 Window.ID_ANDROID_CONTENT 來替代 android.R.id.content
在 View 的點(diǎn)擊事件中,調(diào)用 mExplosionField.explode(v)
之后,View 首先會震動(dòng),然后再炸裂。
震動(dòng)效果比較簡單,設(shè)定一個(gè) [0, 1] 區(qū)間 ValueAnimator,然后在 AnimatorUpdateListener
的 onAnimationUpdate
中隨機(jī)平移 x 和 y坐標(biāo),最后把 scale 和 alpha 值動(dòng)態(tài)減為 0。
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();123456789101112131415
View 震動(dòng)完了就開始進(jìn)行最難的炸裂,并且炸裂是跟隱藏同時(shí)進(jìn)行的,先來看一下炸裂的 API - void explode(Bitmap bitmap, Rect bound, long startDelay, long duration)
:
前兩個(gè)參數(shù) bitmap 和 bound 是關(guān)鍵,通過 View 來創(chuàng)建 bitmap 的代碼比較有意思。
如果 View 是一個(gè) ImageView,并且它的 Drawable 是一個(gè) BitmapDrawable 就可以直接獲取這個(gè) Bitmap。
if (view instanceof ImageView) { Drawable drawable = ((ImageView) view).getDrawable(); if (drawable != null && drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } }123456
如果不是一個(gè) ImageView,可以按照如下步驟創(chuàng)建一個(gè) bitmap:
新建一個(gè) Canvas
根據(jù) View 的大小創(chuàng)建一個(gè)空的 bitmap
把空的 bitmap 設(shè)置為 Canvas 的底布
把 view 繪制在 canvas上
把 canvas 的 bitmap 設(shè)置成 null
當(dāng)然,繪制之前要清掉 View 的焦點(diǎn),因?yàn)榻裹c(diǎn)可能會改變一個(gè) View 的 UI 狀態(tài)。
一下代碼中用到的 sCanvas 是一個(gè)靜態(tài)變量,這樣可以節(jié)省每次創(chuàng)建時(shí)產(chǎn)生的開銷。
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); } }1234567891011
作者創(chuàng)建位圖的辦法非常巧妙,如果新建 Bitmap 時(shí)產(chǎn)生了 OOM,可以主動(dòng)進(jìn)行一次 GC - System.gc()
,然后再次嘗試創(chuàng)建。
這個(gè)函數(shù)的實(shí)現(xiàn)方式讓人佩服作者的功力。
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; } }123456789101112
出了 bitmap,還有一個(gè)一個(gè)很重要的參數(shù) bound,它的創(chuàng)建相對比較簡單:
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]);123456
首先獲取 需要炸裂的View的全局可視區(qū)域 - Rect r
,然后通過 getLocationOnScreen(location)
獲取 ExplosionField
在屏幕中的坐標(biāo),并根據(jù)這個(gè)坐標(biāo)把 炸裂View的可視區(qū)域進(jìn)行平移,這樣炸裂效果才會顯示在 ExplosionField
中,最后根據(jù) mExpandInset 值(默認(rèn)為 0)擴(kuò)展一下。
那創(chuàng)建的 bitmap 和 bound 有什么用呢?我們繼續(xù)往下分析。
先來看一下炸裂成粒子這個(gè)方法的全貌:
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(); }12345678910111213
這里要解釋一下為什么用一個(gè)容器類變量 - mExplosions
來保存一個(gè) ExplosionAnimator
。因?yàn)?activity 中多個(gè) View 的炸裂效果可能要同時(shí)進(jìn)行,所以要把每個(gè) View 對應(yīng)的炸裂動(dòng)畫保存起來,等動(dòng)畫結(jié)束的時(shí)候再刪掉。
作者自定義了一個(gè)繼承自 ValueAnimator 的類 - ExplosionAnimator,它主要做了兩件事情,一個(gè)是創(chuàng)建粒子 - generateParticle
,另一個(gè)是繪制粒子 - draw(Canvas canvas)
。
先來看一下構(gòu)造函數(shù):
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); }123456789101112131415161718
根據(jù)構(gòu)造函數(shù)可以知道作者把 bitmap 分成了一個(gè) 17 x 17 的矩陣,每個(gè)元素的寬度和高度分別是 w
和 h
。
int w = bitmap.getWidth() / (partLen + 2);int h = bitmap.getHeight() / (partLen + 2);12
所有的粒子是一個(gè) 15 x 15 的矩陣,元素色值是位圖對應(yīng)的像素值。
bitmap.getPixel((j + 1) * w, (i + 1) * h)1
結(jié)構(gòu)如下圖所示,其中空心部分是粒子。
● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ● ●
generateParticle
會根據(jù)一定的算法隨機(jī)地生成一個(gè)粒子。這部分比較繁瑣,分析略去。
其中比較巧妙的還是它的 draw 方法:
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; }123456789101112131415
剛開始我還一直比較困惑,既然繪制粒子是在 ExplosionField
的 onDraw
方法中進(jìn)行,那肯定需要不停地刷新,結(jié)果作者并不是這么做的,實(shí)現(xiàn)方法又著實(shí)驚艷了一把。
首先,作者在 ExplosionAnimator
類中重載了 start()
方法,通過調(diào)用 mContainer.invalidate(mBound)
來刷新 將要炸裂的 View 所對應(yīng)的區(qū)塊。
@Overridepublic void start() { super.start(); mContainer.invalidate(mBound); }12345
而 mContainer 即是占滿了 activity 的 view - ExplosionField
,它的 onDraw
方法中又會調(diào)用 ExplosionAnimator
的 draw
方法。
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); for (ExplosionAnimator explosion : mExplosions) { explosion.draw(canvas); } }1234567
這樣便形成了一個(gè)遞歸,兩者相互調(diào)用,不停地刷新,直到所有粒子的 alpha 值變?yōu)?0,刷新就停下來了。
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; }123456789101112131415
這個(gè)開源庫的代碼質(zhì)量相當(dāng)高,十分佩服作者。
關(guān)注我:特效大全《it藍(lán)豹》