【Android效果集】學習ExplosionField之粒子破碎效果

jopen 8年前發布 | 18K 次閱讀 Android開發 移動開發

前段時間在某效果網站看到開源項目【ExplosionField】非常喜歡,于是自己跟著源碼學習著去做了做。跟源碼效果有一點區別,我都是盡力讀懂源碼然后用自己的理解寫出來,源碼有些看不懂的地方,我也就沒有用到,因為自己的代碼要保證自己都能看懂。

最后效果如下: 

20151126231103953.gif

(本文適合有一年Android開發經驗者學習)

本文可以學到: 
1.開源項目ExplosionField的實現思路 
2.圖示效果的實現過程 
3.屬性動畫的用法


實現思路:

1.新建一個 Bean Particle,表示一個粒子對象;新建一個 View ExplosionField作為畫布用來顯示破碎的粒子;新建一個屬性動畫(ValueAnimator) ExplosionAnimator用來改變不同時刻的粒子狀態;

2.通過View生成圖片Bitmap,把生成的圖片分解成若干個粒子,讓每個粒子記錄特定的位置,所有的粒子組合能看出是原圖。

3.加上動畫效果,使得點擊View后,粒子能有所變化。

4.構思算法,形成不一樣的效果。

5.匹配不同分辨率的設備。

6.重構。


詳細過程:

可以先看看項目結構,非常簡單: 

blob.png                    v                                    v           

1.新建對象

1.1 新建Particle對象,用來描述粒子,包括屬性有顏色、透明度、圓心坐標、半徑。

public class Particle {
    float cx; //center x of circle
    float cy; //center y of circle
    float radius;

    int color;
    float alpha;
}

1.2 新建ExplosionField對象,繼承自View,用于做粒子集的畫布,需要重寫onDraw()方法

public class ExplosionField extends View{

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

    public ExplosionField(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
       //初始化
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //繪制粒子
    }
}

1.3 新建ExplosionAnimator,繼承自ValueAnimator,用來執行自定義動畫。ValueAnimator簡單來說就是在一段時間內通過不斷改變值(一般是改變某個屬性的值)來達到動畫效果。更多可以參考《Android屬性動畫完全解析(上),初識屬性動畫的基本用法》來學習。

而我們現在是準備在一段時間內(大概1.5秒)讓ValueAnimator里的值從0.0f變化到1.0f,然后根據系統生成的遞增隨機值(范圍在0.0f~1.0f)改變Particle里的屬性值。

public class ExplosionAnimator extends ValueAnimator{
    public static final int DEFAULT_DURATION = 1500;

    public ExplosionAnimator() {
        setFloatValues(0.0f, 1.0f);
        setDuration(DEFAULT_DURATION);
    }
}

這樣,在1.5秒內,通過ExplosionAnimator的方法getAnimatedValue()就能夠不斷得到遞增的范圍在0.0f~1.0f之間的值。

2.復制出View的快照圖片

首先通過view的寬高創建出一個同樣大小的空白圖,用Bitmap的靜態方法createBitmap()創建,最后一個參數表示圖片質量。

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

然后通過畫布Canvas,先把空白圖設置到畫布里,再讓view把自己畫在畫布上,空白圖也變成了view的翻版了。

   mCanvas.setBitmap(bitmap);
    view.draw(mCanvas);
    //此處bitmap已是同view顯示一樣的圖

完整代碼:

//ExplosionField.java

public class ExplosionField extends View{
        private static final Canvas mCanvas = new Canvas();

        private Bitmap createBitmapFromView(View view) {
        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;
    }
}

PS:在原項目ExplosionField中還有一個判斷,如果view是ImageView的對象,那么直接獲得ImageView依附的BitmapDrawable圖。

     if (view instanceof ImageView) {
            Drawable drawable = ((ImageView)view).getDrawable();
            if (drawable != null && drawable instanceof BitmapDrawable) {
                return ((BitmapDrawable) drawable).getBitmap();
            }
    }

我為什么去掉了呢?是因為如果ImageView設置了背景(background)的話,這樣直接獲取的BitmapDrawable是src的引用,并不包括背景色。所以統一用畫布繪制的方法生成快照。

好了,先拿一個TextView做示范,看看復制的效果:

20151127115104987.gif

3.把快照分解成若干粒子

前面我們已經生成了快照圖片,現在我們需要把快照分解成若干個粒子,這些粒子的組合能看出來是原圖的影子,然后再讓粒子動起來形成后面的動畫。

那怎么做呢?ExplosionField項目是分解成15 * 15個粒子,我這里有點不一樣我就直接按照我的思路講解了。

首先定義一個二維數組Particle[][](一維的也行啦,原項目就是定義一維的),用來存放所有粒子,因為圖片大小不同,粒子個數也不會相同,所以我們把粒子的寬高固定,在Particle類中新加一個靜態常量屬性

   public static final int PART_WH = 8; //默認小球寬高

然后根據view的寬高,算出橫豎粒子的個數

//ExplosionAnimator.java - generateParticles(Bitmap bitmap, Rect bound)

        int w = bound.width();
        int h = bound.height();

        int partW_Count = w / Particle.PART_WH; //橫向個數
        int partH_Count = h / Particle.PART_WH; //豎向個數

        Particle[][] particles = new Particle[partH_Count][partW_Count];

其中bound是Rect類型,通過view.getGlobalVisibleRect()方法能得到view相對于整個屏幕的坐標

    Rect bound = new Rect();
    view.getGlobalVisibleRect(rect);

然后把二維粒子數組對應圖片的位置,設置為相應的顏色屬性和坐標。

通過bitmap.getPixel(x, y)可以獲得(x, y)坐標的bitmap的顏色值

//ExplosionAnimator.java - generateParticles(Bitmap bitmap, Rect bound)

        Point point = null;
        for (int row = 0; row < partH_Count; row ++) { //行
            for (int column = 0; column < partW_Count; column ++) { //列
                //取得當前粒子所在位置的顏色
                int color = bitmap.getPixel(column * partW_Count, row * partH_Count);

                point = new Point(column, row); //x是列,y是行

                particles[row][column] = Particle.generateParticle(color, bound, point);
            }
        }

在Particle類中定義靜態方法generateParticle()用來生成新的Particle對象

//Particle.java

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

        Particle particle = new Particle();
        particle.mBound = bound;
        particle.color = color;
        particle.alpha = 1f;

        particle.radius = PART_WH;
        particle.cx = bound.left + PART_WH * column;
        particle.cy = bound.top + PART_WH * row;

        return particle;
    }

這里把半徑設置為寬長,而不是寬的一半,是因為疊加顯示效果會更好看一點。

為了能夠顯示出來,我們新建一個draw()方法,用從ExplosionField傳來的canvas來繪制所有粒子

//ExplosionAnimator.java

    public void draw(Canvas canvas) {
        for (Particle[] particle : mParticles) {
            for (Particle p : particle) {
                canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
            }
        }
    }


//ExplosionField.java

    private ArrayList<ExplosionAnimator> explosionAnimators;

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

因為畫布可能同時繪制幾個動畫,所以用一個List保存動畫集。

現在大概的效果是這樣:

blob.png  blob.png

4.加上動畫,使得粒子動起來

前面說過,在ExplosionAnimator中通過方法getAnimatedValue()就能夠不斷得到遞增的范圍在0.0f~1.0f之間的值(記做factor)。

我們先在Particle寫好得到變化因素后,屬性要發生的改變。cx左右移動都可以,cy向下移動且距離和view高度有關(不同高度圖片,每次下降距離不同),radius變小,alpha變得越來越透明。只要符合這幾點,算法隨便寫就可以了。

//Particle.java

    public 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());
    }

記住傳進來的factor是從0.0f到1.0f不斷遞增的。

然后改造draw()方法,每次繪制都讓粒子“前進一步”調用一次advance()方法,然后根據新屬性重新繪制

//ExplosionAnimator.java

    public void draw(Canvas canvas) {
        if(!isStarted()) { //動畫結束時停止
            return;
        }
        for (Particle[] particle : mParticles) {
            for (Particle p : particle) {

                p.advance((Float) getAnimatedValue());

                mPaint.setColor(p.color);
                mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha)); //這樣透明顏色就不是黑色了

                canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
            }
        }

        mContainer.invalidate();
    }

最后一句的mContainer其實就是ExplosionField,調用它的invalidate()方法,就是調用ExplosionField的onDraw()方法。而ExplosionField的onDraw()里又調用了ExplosionAnimator的draw()方法。這樣循環就出現了動畫效果。

結束的條件就是第一句if(!isStarted())如果動畫停止了,就斷了繪制循環。

PS:這里值得一提的有setAlpha()方法,之前我用的是

    mPaint.setColor(p.color);
    mPaint.setAlpha((int) (255 * p.alpha));

這樣有個問題就是當顏色為透明時,顯示的是黑色。

而改為了方法:

   mPaint.setColor(p.color);
   mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));

透明顏色就為透明色了。

現在動畫過程已經寫完,就差開始的導火線了,我們在動畫開始的時候啟動這根導火線,重寫start()方法:

//ExplosionAnimator.java

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

那在哪使動畫開始呢,即在哪調用explosionAnimator.start()呢?

在ExplosionField中建立一個“爆炸”方法,只要調用這個方法,傳入view,最后執行animator.start(),view就會執行爆炸效果

    public void explode(final View view) {
        Rect rect = new Rect();
        view.getGlobalVisibleRect(rect); //得到view相對于整個屏幕的坐標
        rect.offset(0, -Utils.dp2px(25)); //去掉狀態欄高度

        final ExplosionAnimator animator = new ExplosionAnimator(this, createBitmapFromView(view), rect);
        explosionAnimators.add(animator);

        animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                view.animate().alpha(0f).setDuration(150).start();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                view.animate().alpha(1f).setDuration(150).start();

                //動畫結束時從動畫集中移除
                explosionAnimators.remove(animation);
                animation = null;
            }
        });
        animator.start();
    }

現在的效果:

20151203171656325.gif

5.附著動畫到任意Activity,添加監聽器給需要有動畫效果的view

現在動畫效果什么的都做好了,要如何使用呢?

現在的思路是在Activity的最上層蓋一層透明的ExplosionField視圖,用來顯示粒子動畫。

//ExplosionField.java

   /**
     * 給Activity加上全屏覆蓋的ExplosionField
     */
    private void attach2Activity(Activity activity) {
        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);

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

其實Activity的根視圖并不是我們設置的xml,它上面還有一層,通過findViewById(Window.ID_ANDROID_CONTENT)能夠得到,然后我們再把ExplosionField全屏加載在Activity的最上層,這樣顯示動畫效果就不會被遮蓋。

然后我們可以在初始化的時候加上這個方法:

public class ExplosionField extends View{

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

    public ExplosionField(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init() {
        ...
        attach2Activity((Activity) getContext());
    }

    ...
}

在看Activity的onCreate()方法就非常簡單了:

//MainActivity.java

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main_az);

        ExplosionField explosionField = new ExplosionField(this);

        explosionField.addListener(findViewById(R.id.root));
    }

最后一句調用了addListener()方法,就是把需要實現點擊破碎效果的view加上監聽器,看代碼:

    public void addListener(View view) {
        if (view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            int count = viewGroup.getChildCount();
            for (int i = 0 ; i < count; i++) {
                addListener(viewGroup.getChildAt(i));
            }
        } else {
            view.setClickable(true);
            view.setOnClickListener(getOnClickListener());
        }
    }


    private OnClickListener getOnClickListener() {
        if (null == onClickListener) {

            onClickListener = new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    ExplosionField.this.explode(v);
                }
            };
        }

        return onClickListener;
    }

只要傳入ViewGroup,會自動遞歸查找Child View,并給Child View加上點擊監聽器,一旦點擊就調用爆破方法執行動畫。

最終效果大圖:

20151203174342012.gif

更多詳細代碼可 fork 源碼查看!

源碼地址:https://github.com/Xieyupeng520/AZExplosion

如果你喜歡這個效果,請給我Github上一個Star鼓勵一下哈O(∩_∩)O謝謝!

原文出處:http://blog.csdn.net/XieYupeng520/article/details/49951835 

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!