5分鐘快速實現Android爆炸破碎酷炫動效

5分鐘快速實現Android爆炸破碎酷炫動效

這個破碎動畫,是一種類似小米系統刪除應用時的爆炸破碎效果的動畫。

效果圖展示

先來看下是怎樣的動效,要是感覺不是理想的學習目標,就跳過,避免浪費大家的時間。??

ezgif-2-a640aae0e5.gif

源碼在這里??https://github.com/ReadyShowShow/explosion

解析在這里??http://www.lxweimin.com/p/11bed7dabe2c

一行代碼即可調用該動畫

new ExplosionField(this).explode(view, null))

下面開始我們酷炫的Android動畫特效正式講解??


先來個整體結構的把握

整體結構非常簡單明了,新老從業者都可快速看懂,容易把握學習。

./
|-- explosion
|   |-- MainActivity.java (測試爆炸破碎動效的主界面)
|   |-- animation(爆炸破碎動效有關的類均在這里)
|   |   |-- ExplosionAnimator.java(爆炸動畫)
|   |   |-- ExplosionField.java(爆炸破碎動畫所依賴的View)
|   |   `-- ParticleModel.java(每個破碎后的粒子的model,顏色、位置、大小等)
|   `-- utils
|       `-- UIUtils.java(計算狀態欄高度的工具類)
`-- architecture.md

庖丁解牛

下面開始每個類的詳細分析
本著從簡到繁、由表及里的原則,詳細講解每個類

MainActivity.java

MainActivity.java是測試動效的界面,該Activity內部有7個測試按鈕。該類做的事情非常單純,就是給每個View分別綁定click點擊事件,讓View在點擊時能觸發爆炸破碎動畫。

/**
 * 說明:測試的界面
 * 作者:Jian
 * 時間:2017/12/26.
 */
public class MainActivity extends AppCompatActivity {

    /**
     * 加載布局文件,添加點擊事件
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViewsClick();
    }

    /**
     * 添加點擊事件的實現
     */
    private void initViewsClick() {
        // 為單個View添加點擊事件
        final View title = findViewById(R.id.title_tv);
        title.setOnClickListener(v ->
                new ExplosionField(MainActivity.this).explode(title, null));

        // 為中間3個View添加點擊事件
        setSelfAndChildDisappearOnClick(findViewById(R.id.title_disappear_ll));
        // 為下面3個View添加點擊事件
        setSelfAndChildDisappearAndAppearOnClick(findViewById(R.id.title_disappear_and_appear_ll));

        // 跳轉到github網頁的點擊事件
        findViewById(R.id.github_tv).setOnClickListener((view) -> {
            Intent intent = new Intent();
            intent.setAction(Intent.ACTION_VIEW);
            Uri content_url = Uri.parse(getString(R.string.github));
            intent.setData(content_url);
            startActivity(intent);
        });
    }

    /**
     * 為自己以及子View添加破碎動畫,動畫結束后,把View消失掉
     * @param view 可能是ViewGroup的view
     */
    private void setSelfAndChildDisappearOnClick(final View view) {
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                setSelfAndChildDisappearOnClick(viewGroup.getChildAt(i));
            }
        } else {
            view.setOnClickListener(v ->
                    new ExplosionField(MainActivity.this).explode(view,
                            new AnimatorListenerAdapter() {
                                @Override
                                public void onAnimationEnd(Animator animation) {
                                    super.onAnimationEnd(animation);
                                    view.setVisibility(View.GONE);
                                }
                            }));
        }
    }

    /**
     * 為自己以及子View添加破碎動畫,動畫結束后,View自動出現
     * @param view 可能是ViewGroup的view
     */
    private void setSelfAndChildDisappearAndAppearOnClick(final View view) {
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            for (int i = 0; i < viewGroup.getChildCount(); i++) {
                setSelfAndChildDisappearAndAppearOnClick(viewGroup.getChildAt(i));
            }
        } else {
            view.setOnClickListener(v ->
                    new ExplosionField(MainActivity.this).explode(view, null));
        }
    }
}

ParticleModel.java

ParticleModel.java是包含一個粒子的所有信息的model。advance方法根據值動畫返回的進度計算出粒子的位置和顏色等信息

 * 說明:爆破粒子,每個移動與漸變的小塊
 * 作者:Jian
 * 時間:2017/12/26.
 */
class ParticleModel {
    // 默認小球寬高
    static final int PART_WH = 8;
    // 隨機數,隨機出位置和大小
    static Random random = new Random();
    //center x of circle
    float cx;
    //center y of circle
    float cy;
    // 半徑
    float radius;
    // 顏色
    int color;
    // 透明度
    float alpha;
    // 整體邊界
    Rect mBound;

    ParticleModel(int color, Rect bound, Point point) {
        int row = point.y; //行是高
        int column = point.x; //列是寬

        this.mBound = bound;
        this.color = color;
        this.alpha = 1f;
        this.radius = PART_WH;
        this.cx = bound.left + PART_WH * column;
        this.cy = bound.top + PART_WH * row;
    }

    // 每一步動畫都得重新計算出自己的狀態值
    void advance(float factor) {
        cx = cx + factor * random.nextInt(mBound.width()) * (random.nextFloat() - 0.5f);
        cy = cy + factor * random.nextInt(mBound.height() / 2);

        radius = radius - factor * random.nextInt(2);

        alpha = (1f - factor) * (1 + random.nextFloat());
    }
}

ExplosionAnimation.java

ExlosionAnimation.java是動畫類,是一個值動畫,在值動畫每次產生一個值的時候,就計算出整個爆炸破碎動效內的全部粒子的狀態。這些狀態交由使用的View在渲染時進行顯示。

/**
 * 說明:爆炸動畫類,讓離子移動和控制離子透明度
 * 作者:Jian
 * 時間:2017/12/26.
 */
class ExplosionAnimator extends ValueAnimator {
    private static final int DEFAULT_DURATION = 1500;
    private ParticleModel[][] mParticles;
    private Paint mPaint;
    private View mContainer;

    public ExplosionAnimator(View view, Bitmap bitmap, Rect bound) {
        setFloatValues(0.0f, 1.0f);
        setDuration(DEFAULT_DURATION);

        mPaint = new Paint();
        mContainer = view;
        mParticles = generateParticles(bitmap, bound);
    }

    // 生成粒子,按行按列生成全部粒子
    private ParticleModel[][] generateParticles(Bitmap bitmap, Rect bound) {
        int w = bound.width();
        int h = bound.height();

        // 橫向粒子的個數
        int horizontalCount = w / ParticleModel.PART_WH;
        // 豎向粒子的個數
        int verticalCount = h / ParticleModel.PART_WH;

        // 粒子寬度
        int bitmapPartWidth = bitmap.getWidth() / horizontalCount;
        // 粒子高度
        int bitmapPartHeight = bitmap.getHeight() / verticalCount;

        ParticleModel[][] particles = new ParticleModel[verticalCount][horizontalCount];
        for (int row = 0; row < verticalCount; row++) {
            for (int column = 0; column < horizontalCount; column++) {
                //取得當前粒子所在位置的顏色
                int color = bitmap.getPixel(column * bitmapPartWidth, row * bitmapPartHeight);

                Point point = new Point(column, row);
                particles[row][column] = new ParticleModel(color, bound, point);
            }
        }
        return particles;
    }

    // 由view調用,在View上繪制全部的粒子
    void draw(Canvas canvas) {
        // 動畫結束時停止
        if (!isStarted()) {
            return;
        }
        // 遍歷粒子,并繪制在View上
        for (ParticleModel[] particle : mParticles) {
            for (ParticleModel p : particle) {
                p.advance((Float) getAnimatedValue());
                mPaint.setColor(p.color);
                // 錯誤的設置方式只是這樣設置,透明色會顯示為黑色
                // mPaint.setAlpha((int) (255 * p.alpha)); 
                // 正確的設置方式,這樣透明顏色就不是黑色了
                mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
                canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
            }
        }
        mContainer.invalidate();
    }

    @Override
    public void start() {
        super.start();
        mContainer.invalidate();
    }
}

ExplosionField.java

ExplosionField.java是真實執行上面ExplosionAnimator。ExplosionField會創建一個View并依附在Activity的根View上。

/**
 * 說明:每次爆炸時,創建一個覆蓋全屏的View,這樣的話,不管要爆炸的View在任何位置都能顯示爆炸效果
 * 作者:Jian
 * 時間:2017/12/26.
 */
public class ExplosionField extends View {
    private static final String TAG = "ExplosionField";
    private static final Canvas mCanvas = new Canvas();
    private ExplosionAnimator animator;

    public ExplosionField(Context context) {
        super(context);
    }

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

    /**
     * 執行爆破破碎動畫
     */
    public void explode(final View view, final AnimatorListenerAdapter listener) {
        Rect rect = new Rect();
        view.getGlobalVisibleRect(rect); //得到view相對于整個屏幕的坐標
        rect.offset(0, -UIUtils.statusBarHeignth()); //去掉狀態欄高度

        animator = new ExplosionAnimator(this, createBitmapFromView(view), rect);

        // 接口回調
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if (listener != null) listener.onAnimationStart(animation);
                // 延時添加到界面上
                attach2Activity((Activity) getContext());
                // 讓被爆炸的View消失(爆炸的View是新創建的View,原View本身不會發生任何變化)
                view.animate().alpha(0f).setDuration(150).start();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (listener != null) listener.onAnimationEnd(animation);
                // 從界面中移除
                removeFromActivity((Activity) getContext());
                // 讓被爆炸的View顯示(爆炸的View是新創建的View,原View本身不會發生任何變化)
                view.animate().alpha(1f).setDuration(150).start();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                if (listener != null) listener.onAnimationCancel(animation);
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                if (listener != null) listener.onAnimationRepeat(animation);
            }
        });
        animator.start();
    }

    private Bitmap createBitmapFromView(View view) {
//         為什么屏蔽以下代碼段?
//         如果ImageView直接得到位圖,那么當它設置背景(backgroud)時,不會讀取到背景顏色
//        if (view instanceof ImageView) {
//            Drawable drawable = ((ImageView)view).getDrawable();
//            if (drawable != null && drawable instanceof BitmapDrawable) {
//                return ((BitmapDrawable) drawable).getBitmap();
//            }
//        }
        //view.clearFocus(); //不同焦點狀態顯示的可能不同——(azz:不同就不同有什么關系?)

        Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

        if (bitmap != null) {
            synchronized (mCanvas) {
                mCanvas.setBitmap(bitmap);
                view.draw(mCanvas);
                // 清除引用
                mCanvas.setBitmap(null);
            }
        }
        return bitmap;
    }

    /**
     * 將創建的ExplosionField添加到Activity上
     */
    private void attach2Activity(Activity activity) {
        ViewGroup rootView = activity.findViewById(Window.ID_ANDROID_CONTENT);

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

    /**
     * 將ExplosionField從Activity上移除
     */
    private void removeFromActivity(Activity activity) {
        ViewGroup rootView = activity.findViewById(Window.ID_ANDROID_CONTENT);
        rootView.removeView(this);
    }
}

動畫執行時為什么要創建一個新View(ExplosionField

其實上面的動畫類ExplosionAnimator已經實現了核心功能,直接在原View上使用該動畫應該是沒問題的。為什么還要引入一個ExplosionField類呢?動畫的執行為什么不能直接在原本的View上執行呢?偏偏要在一個看似多余的ExplosionField對象上執行呢。

這里就得從Android下View繪制原理來解釋了:Android下的View都有一個Bound,在View進行measure和layout的時候,已經確定了View的大小和位置,如果要在這個View上進行動畫的話,就會出現動畫只能在view大小范圍內進行展現。當然了,也不是說在原來View上一定不能實現這一動效,就是相當復雜,要在動畫執行過程中,不斷改變原View的大小和View的屬性等信息,相當復雜。

在性能還行的前提下,要優先代碼的整潔度,盡量避免為了優化的性能,而舍棄整潔清爽的代碼。一般來說,過度的優化,并沒有給用戶帶來太多體驗上的提升,反而給項目帶來了巨大的維護難度。

UIUtils.java

UIUtils是關于UI的工具類,沒啥可說的

public class UIUtils {

    public static int dp2px(double dpi) {
        return (int) (Resources.getSystem().getDisplayMetrics().density * dpi + 0.5f);
    }

    public static int statusBarHeignth() {
        return dp2px(25);
    }
}

結束

源碼在這里??https://github.com/ReadyShowShow/explosion

如果有優化的建議與意見,歡迎大家提Issues或者郵箱ReadyShowShow@gmail.com

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372

推薦閱讀更多精彩內容