Android粒子破碎效果(2)——實現多種破碎效果之ParticleSmasher

上一篇我們分析了開源項目ExplosionFiled,瞭解了其繪製動畫效果的流程以及粒子運動的軌跡的計算。學習要與實踐相結合,因此,在該項目的基礎上,我又做一些自己的改進和功能的增加。

1、介紹

特色:

  • 六種效果,包含爆炸效果、墜落效果、四個方向的逐漸飄落效果;
  • 鏈式調用,自定義動畫時間、樣式、動畫幅度等;

地址:

Github地址

效果圖:

六種效果演示

用法:

導入

dependencies {
 compile 'com.ifadai:particlesmasher:1.0.1'
}

簡單使用:

 ParticleSmasher smasher = new ParticleSmasher(this);
 // 默認爲爆炸動畫
 smasher.with(view).start();

複雜一點:

smasher.with(view)
        .setStyle(SmashAnimator.STYLE_DROP)    // 設置動畫樣式
        .setDuration(1500)                     // 設置動畫時間
        .setStartDelay(300)                    // 設置動畫前延時
        .setHorizontalMultiple(2)              // 設置橫向運動幅度,默認爲3
        .setVerticalMultiple(2)                // 設置豎向運動幅度,默認爲4
       .addAnimatorListener(new SmashAnimator.OnAnimatorListener() {
                            @Override
                            public void onAnimatorStart() {
                                super.onAnimatorStart();
                                // 回調,動畫開始
                            }

                            @Override
                            public void onAnimatorEnd() {
                                super.onAnimatorEnd();
                                // 回調,動畫結束
                            }
                        })
        .start();    

讓View重新顯示:

smasher.reShowView(view);

2、代碼解析

項目結構:

項目結構

ParticleSmasher:

與ExplosionFiled相同,這裏也是繼承自View,用於繪製動畫效果。先看構造方法:

public ParticleSmasher(Activity activity) {
        super((Context) activity);
        this.mActivity = activity;
        addView2Window(activity);
        init();
    }

這裏通過addView2Window()將繪製動畫效果的View添加到了RootView中,因此同一界面上多個View要實現動畫效果時,只實例化一個ParticleSmasher即可。

/**
     * 添加View到當前界面
     */
    private void addView2Window(Activity activity) {
        ViewGroup rootView = (ViewGroup) activity.findViewById(Window.ID_ANDROID_CONTENT);
        // 需要足夠的空間展現動畫,因此這裏使用的是充滿父佈局
        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        rootView.addView(this, layoutParams);
    }

開始動畫時,是調用particleSmasher.with(view)方法,該方法會實例化一個SmashAnimator對象,後續可以通過一系列鏈式調用,來修改該對象的屬性。同時,將該對象添加到了Animator集合中,方便管理。

public SmashAnimator with(View view) {
        // 每次都新建一個單獨的SmashAnimator對象
        SmashAnimator animator = new SmashAnimator(this, view);
        mAnimators.add(animator);
        return animator;
    }

SmashAnimator :

不同於ExplosionFiled,這裏的SmashAnimator沒有直接繼承ValueAnimator,而是在內部實例化了一個ValueAnimator。

初始化:

public SmashAnimator(ParticleSmasher view, View animatorView) {
        this.mContainer = view;
        init(animatorView);
    }

    private void init(View animatorView) {
        this.mAnimatorView = animatorView;
        mBitmap = mContainer.createBitmapFromView(animatorView);
        mRect = mContainer.getViewRect(animatorView);
        initValueAnimator();
        initPaint();
    }

    private void initValueAnimator() {
        mValueAnimator = new ValueAnimator();
        mValueAnimator.setFloatValues(0F, mEndValue);
        mValueAnimator.setInterpolator(DEFAULT_INTERPOLATOR);
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
    }

然後,添加了一系列的set方法,用於設置動畫時間、啓動延時、動畫類型、水平變化幅度、垂直變化幅度、粒子基礎半徑、動畫回調事件等。同時,還有最重要的start()方法,用於開始動畫:

/**
     *   開始動畫
     */
    public void start() {
        setValueAnimator();
        calculateParticles(mBitmap);
        hideView(mAnimatorView, mStartDelay);
        mValueAnimator.start();
        mContainer.invalidate();
    }

setValueAnimator()方法會將鏈式調用設置的一系列值,賦給ValueAnimator對象:

/**
     *   設置動畫參數
     */
    private void setValueAnimator() {
        mValueAnimator.setDuration(mDuration);
        mValueAnimator.setStartDelay(mStartDelay);
        mValueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (mOnExplosionListener != null) {
                    mOnExplosionListener.onAnimatorEnd();
                }
                mContainer.removeAnimator(SmashAnimator.this);
            }

            @Override
            public void onAnimationStart(Animator animation) {
                if (mOnExplosionListener != null) {
                    mOnExplosionListener.onAnimatorStart();
                }

            }
        });
    }

與ExplosionFiled不同的是,這裏的生成粒子方法calculateParticles(bitmap)中,並不是固定生成15*15個粒子,而是根據粒子的基礎半徑,計算需要的粒子數量,然後再通過判斷動畫類型,從而生成不同參數的粒子。(這裏有一個問題,即進行動畫的View過小的時候,生成的粒子數量不夠多,這時候可以修改粒子基礎半徑大小,使得可以生成足夠多的粒子):

/**
     * 根據圖片計算粒子
     * @param bitmap      需要計算的圖片
     */
    private void calculateParticles(Bitmap bitmap) {

        int col = bitmap.getWidth() /(mRadius*2);
        int row = bitmap.getHeight() / (mRadius*2);

        Random random = new Random(System.currentTimeMillis());
        mParticles = new Particle[row][col];

        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                int x=j * mRadius*2 + mRadius;
                int y=i * mRadius*2 + mRadius;
                int color = bitmap.getPixel(x, y);
                Point point=new Point(mRect.left+x,mRect.top+y);

                switch (mStyle){
                    case STYLE_EXPLOSION:
                        mParticles[i][j] = new ExplosionParticle(color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                    case STYLE_DROP:
                        mParticles[i][j] = new DropParticle(point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                    case STYLE_FLOAT_LEFT:
                        mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_LEFT,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                    case STYLE_FLOAT_RIGHT:
                        mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_RIGHT,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                    case STYLE_FLOAT_TOP:
                        mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_TOP,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                    case STYLE_FLOAT_BOTTOM:
                        mParticles[i][j] = new FloatParticle(FloatParticle.ORIENTATION_BOTTOM,point,color, mRadius, mRect, mEndValue, random, mHorizontalMultiple, mVerticalMultiple);
                        break;
                }

            }
        }
        mBitmap.recycle();
        mBitmap = null;
    }

最後是draw(canvas)方法,由ParticleSmasher中的onDraw()方法調用,用於循環繪製粒子,並根據動畫進程調用粒子的advance方法,來改變粒子的參數:

/**
     *   開始逐個繪製粒子
     *   @param canvas  繪製的畫板
     *   @return 是否成功
     */
    public boolean draw(Canvas canvas) {
        if (!mValueAnimator.isStarted()) {
            return false;
        }
        for (Particle[] particle : mParticles) {
            for (Particle p : particle) {
                // 根據動畫進程,修改粒子的參數
                p.advance((float) (mValueAnimator.getAnimatedValue()), mEndValue);
                if (p.alpha > 0) {
                    mPaint.setColor(p.color);
                    mPaint.setAlpha((int) (Color.alpha(p.color) * p.alpha));
                    canvas.drawCircle(p.cx, p.cy, p.radius, mPaint);
                }
            }
        }
        mContainer.invalidate();
        return true;
    }

粒子實體類:

  • Particle:包含粒子各項參數,和改變粒子參數的advance()方法。

在ExplosionFiled的基礎上,我刪除了top、bottom、mag、neg參數,新增了horizontalElement、verticalElement參數,一個是粒子水平變化參數,一個是垂直變化參數,這樣更直觀一些。同時,將life修改爲font,overflow修改爲later。

public abstract class Particle {

    public int color;                // 顏色
    public float radius;             // 半徑
    public float alpha;              // 透明度(0~1)
    public float cx;                 // 圓心 x
    public float cy;                 // 圓心 y


    public float horizontalElement;  // 水平變化參數
    public float verticalElement;    // 垂直變化參數

    public float baseRadius;         // 初始半徑,同時負責半徑大小變化
    public float baseCx;             // 初始圓心 x
    public float baseCy;             // 初始圓心 y

    public float font;               // 決定了粒子在動畫開始多久之後,開始顯示
    public float later;              // 決定了粒子動畫結束前多少時間開始隱藏

    public void advance(float factor, float endValue) {
    }
}
  • ExplosionParticle、DropParticle、FloatParticle:爆炸粒子、墜落粒子、飄落粒子。都繼承了Particle,通過構造方法,生成粒子,通過advance方法,在動畫進程中改變粒子參數。

這裏以ExplosionParticle(爆炸效果的粒子)爲例,我們用構造方法來初始化粒子的參數:
爆炸粒子初始化參數

這裏最重要且最值得注意的是horizontalElement和verticalElement的生成,用到了horizontalMultiple和verticalMultiple,即變化幅度,也可以理解爲變化倍數,即粒子可以到達多遠的距離,這個值越大,粒子運動得越遠,反之亦然。

 private static float getHorizontalElement(Rect rect, Random random, float nextFloat,float horizontalMultiple) {

        // 第一次隨機運算:h=width*±(0.01~0.49)
        float horizontal = rect.width() * (random.nextFloat() - 0.5f);

        // 第二次隨機運行: h= 1/5概率:h;3/5概率:h*0.6; 1/5概率:h*0.3; nextFloat越大,h越小。
        horizontal = nextFloat < 0.2f ? horizontal :
                nextFloat < 0.8f ? horizontal * 0.6f : horizontal * 0.3f;

        // 上面的計算是爲了讓橫向變化參數有隨機性,下面的計算是修改橫向變化的幅度。
        return horizontal * horizontalMultiple;
    }

    private static float getVerticalElement(Rect rect, Random random, float nextFloat,float verticalMultiple) {

        // 第一次隨機運算: v=height*(0.5~1)
        float vertical = rect.height() * (random.nextFloat() * 0.5f + 0.5f);

        // 第二次隨機運行: v= 1/5概率:v;3/5概率:v*1.2; 1/5概率:v*1.4; nextFloat越大,h越大。
        vertical = nextFloat < 0.2f ? vertical :
                nextFloat < 0.8f ? vertical * 1.2f : vertical * 1.4f;

        // 上面的計算是爲了讓變化參數有隨機性,下面的計算是變化的幅度。
        return vertical * verticalMultiple;
    }

比如在比較扁平的控件中,因爲verticalElement是基於控件的height,進行一系列隨機運算而生成的,因此如果不增大verticalMultiple的值的話,粒子的垂直運動範圍是很有限的距離,因此可以適當增加verticalMultiple,這樣會更美觀。

粒子的變化方法advance(),在去掉了一些功能重複的參數以及對公式進行簡化之後,advance方法邏輯清晰了許多:

 public void advance(float factor, float endValue) {

        // 動畫進行到了幾分之幾
        float normalization = factor / endValue;

        if (normalization < font || normalization > 1f - later) {
            alpha = 0;
            return;
        }
        alpha = 1;

        // 粒子可顯示的狀態中,動畫實際進行到了幾分之幾
        normalization = (normalization - font) / (1f - font - later);
        // 動畫超過7/10,則開始逐漸變透明
        if (normalization >= 0.7f) {
            alpha = 1f - (normalization - 0.7f) / 0.3f;
        }

        float realValue = normalization * endValue;

        // y=j+k*x,j、k都是常數,x爲 0~1.4
        cx = baseCx + horizontalElement * realValue;

        // y=j+k*(x*(x-1),j、k都是常數,x爲 0~1.4
        cy = baseCy + verticalElement * (realValue * (realValue - 1));

        radius = baseRadius + baseRadius / 4 * realValue;

    }

其餘的幾種動畫效果與爆炸粒子有些細微的差別,比如初始化粒子的baseCx、baseCy位置不同;粒子變化過程中飄落的粒子需要判斷是否已經到了該變化的時候等。這些就不一一寫出來了,感興趣的可以去看一下代碼,註釋基本都講的很清楚了。

這大概是2017年最後一篇博客了吧,希望新的一年,可以有更多的進步,共勉!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章