一个实现粒子爆炸效果的控件

 

这次带来的控件,可以实现让任意指定控件“爆炸”。像下面这样:

 

一个实现粒子爆炸效果的控件_第1张图片

 

效果很直观,下面上代码。核心控件就一个ExplodeView:

public class ExplodeView extends View {

    private static Context context;

    // 被爆炸的视图
    private View view;


    /**
     * 可设置参数
     */
    // 粒子半径
    private int particleRadius;

    // 动画持续时间
    private int duration;

    // 爆炸轨迹算子
    private TrackIterator trackIterator;


    /**
     * 运行参数
     */
    // 爆炸动画
    private ExplosionAnimator explodeAnimator;

    // 爆炸动画监听
    private OnExplodeListener listener;


    public ExplodeView(Context context) {
        this(context, null);
    }

    public ExplodeView(Context context, AttributeSet attrs) {
        this(context, attrs, -1);
    }

    public ExplodeView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    /**
     * 初始化方法
     *
     * @param context
     */
    private void init(Context context) {

        this.context = context;

        particleRadius = dp2px(5);
        duration = 1000;

        attach2Activity((Activity) context);
    }

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

        // 每次页面刷新时,命令爆炸动画类重新绘制粒子位置
        if (explodeAnimator != null) {
            explodeAnimator.draw(canvas);
        }
    }

    /**
     * 触发爆炸方法
     *
     * @param view 被爆炸的视图
     */
    public void explode(View view) {

        if (view == null) {
            return;
        }

        if (explodeAnimator != null && explodeAnimator.isStarted()) {
            return;
        }

        this.view = view;

        Rect rect = new Rect();
        view.getGlobalVisibleRect(rect);
        int[] location = new int[2];
        getLocationOnScreen(location);
        rect.offset(-location[0], -location[1]);

        Bitmap bitmap = createBitmapFromView(view);
        explodeAnimator = new ExplosionAnimator(bitmap, rect);

        bitmap.recycle();
        bitmap = null;
        System.gc();

        view.setEnabled(false);

        ValueAnimator shakeAnimator = ValueAnimator.ofFloat(0f, 1f);
        shakeAnimator.setDuration(150);
        shakeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            Random random = new Random();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                ExplodeView.this.view.setTranslationX((random.nextFloat() - 0.5f) * ExplodeView.this.view.getWidth() * 0.05f);
                ExplodeView.this.view.setTranslationY((random.nextFloat() - 0.5f) * ExplodeView.this.view.getHeight() * 0.05f);
            }
        });

        shakeAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                explodeAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animation) {
                        ExplodeView.this.view.setVisibility(View.INVISIBLE);

                        if (listener != null) {
                            listener.onStart(ExplodeView.this.view);
                        }
                    }

                    @Override
                    public void onAnimationEnd(Animator animation) {
                        ExplodeView.this.view.setEnabled(true);

                        explodeAnimator.particles = null;
                        explodeAnimator = null;

                        if (listener != null) {
                            listener.onFinish(ExplodeView.this.view);
                        }
                    }
                });

                explodeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        invalidate();
                    }
                });

                explodeAnimator.start();
            }
        });

        shakeAnimator.start();
    }

    /**
     * 将自身添加到当前页面的窗口中
     *
     * @param 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);
    }

    /**
     * 获取被爆炸视图的缓存方法
     *
     * @param view 被爆炸视图
     * @return 缓存
     */
    private Bitmap createBitmapFromView(View view) {
        view.setDrawingCacheEnabled(true);
        Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
        view.destroyDrawingCache();

        return bitmap;
    }

    public static int dp2px(float dipValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }

    /**
     * 轨迹迭代方法
     *
     * @param position 粒子编号
     * @param particle 粒子对象
     * @param factor   动画进度
     */
    private void advance(int position, Particle particle, float factor) {

        float x = particle.centerX;
        float y = particle.centerY;
        float alpha = particle.alpha;
        float radius = particle.radius;
        int color = particle.color;

        particle.centerX = trackIterator.getX(position, x, y, radius, color, alpha, particle.rectWidth, particle.rectHeight, factor);
        particle.centerY = trackIterator.getY(position, x, y, radius, color, alpha, particle.rectWidth, particle.rectHeight, factor);
        particle.alpha = trackIterator.getAlpha(position, x, y, radius, color, alpha, particle.rectWidth, particle.rectHeight, factor);
        particle.radius = trackIterator.getRadius(position, x, y, radius, color, alpha, particle.rectWidth, particle.rectHeight, factor);
        particle.color = trackIterator.getColor(position, x, y, radius, color, alpha, particle.rectWidth, particle.rectHeight, factor);

    }

    /**
     * 设置粒子半径
     *
     * @param particleRadius 粒子半径 单位dp
     */
    public void setParticleRadius(int particleRadius) {
        this.particleRadius = dp2px(particleRadius);
    }

    /**
     * 设置爆炸动画时长
     *
     * @param duration 时长 单位毫秒
     */
    public void setDuration(int duration) {
        this.duration = duration;
    }

    /**
     * 爆炸轨迹算子 决定了粒子的运动轨迹
     *
     * @param trackIterator 轨迹算子,目前支持的算子:
     *                      ·NormalTrackIterator:普通算子,粒子呈类自由落体
     *                      ·BezierTrackIterator:Bezier算子,粒子呈斜向上Bezier曲线
     *                      ·QuadraticTrackIterator:二次函数算子,粒子呈斜向上抛物线
     */
    public void setTrackIterator(TrackIterator trackIterator) {
        this.trackIterator = trackIterator;
    }

    /**
     * 动画监听接口
     */
    public interface OnExplodeListener {

        /**
         * 动画开始时回调
         *
         * @param view
         */
        void onStart(View view);

        /**
         * 动画结束时回调
         *
         * @param view
         */
        void onFinish(View view);
    }

    /**
     * 设置动画监听
     *
     * @param listener
     */
    public void setOnExplodeListener(OnExplodeListener listener) {
        this.listener = listener;
    }

    /**
     * 爆炸动画类,实现了初始化粒子和更新轨迹的操作
     */
    class ExplosionAnimator extends ValueAnimator {

        // 所有粒子数组
        private Particle[][] particles;

        private Paint paint;

        /**
         * 初始化粒子
         *
         * @param bitmap 爆炸view的视图缓存
         * @param bound  爆炸view的显示区域
         */
        public ExplosionAnimator(Bitmap bitmap, Rect bound) {
            paint = new Paint();
            setFloatValues(0.0f, 1.0f);
            setDuration(duration);
            setInterpolator(new LinearInterpolator());

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

            int columnCount = w / particleRadius; //横向粒子个数
            int rowCount = h / particleRadius;    //竖向粒子个数

            particles = new Particle[rowCount][columnCount];

            for (int row = 0; row < rowCount; row++) { //行
                for (int column = 0; column < columnCount; column++) { //列
                    int color = bitmap.getPixel(column * particleRadius, row * particleRadius);
                    particles[row][column] = new Particle(row * columnCount + column, color,
                            particleRadius, bound.left, bound.top, bound.width(), bound.height(),
                            column, row, bound.centerX(), bound.centerY());
                }
            }
        }

        /**
         * 刷新粒子轨迹
         *
         * @param canvas
         */
        public void draw(Canvas canvas) {
            if (!isStarted()) {
                return;
            }
            for (int row = 0; row < particles.length; row++) { //行
                for (int column = 0; column < particles[row].length; column++) { //列
                    advance(row * particles[row].length + column, particles[row][column], (Float) getAnimatedValue());

                    paint.setColor(particles[row][column].color);
                    paint.setAlpha((int) (Color.alpha(particles[row][column].color) * particles[row][column].alpha));

                    canvas.drawCircle(particles[row][column].centerX, particles[row][column].centerY, particles[row][column].radius, paint);
                }
            }
        }
    }

    /**
     * 粒子对象
     */
    public class Particle {

        // 粒子中心x坐标
        private float centerX;

        // 粒子中心y坐标
        private float centerY;

        // 粒子半径
        private float radius;

        // 粒子颜色
        private int color;

        // 粒子透明度
        private float alpha;

        // 爆炸view的显示区域宽度
        private int rectWidth;

        // 爆炸view的显示区域高度
        private int rectHeight;

        /**
         * 构造方法
         *
         * @param position   粒子编号
         * @param color      粒子颜色
         * @param width      粒子半径
         * @param left       爆炸view的左上角x坐标
         * @param top        爆炸view的左上角y坐标
         * @param rectWidth  爆炸view的宽度
         * @param rectHeight 爆炸view的高度
         * @param column     粒子在粒子矩阵中的列数
         * @param row        粒子在粒子矩阵中的行数
         * @param centerX    爆炸view的中心x坐标
         * @param centerY    爆炸view的中心y坐标
         */
        public Particle(int position, int color, int width, int left, int top, int rectWidth, int rectHeight, int column, int row, float centerX, float centerY) {

            if (trackIterator == null) {
                trackIterator = new NormalTrackIterator();
            }

            trackIterator.initParticle(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, centerX, centerY, this);

            this.color = trackIterator.getColor(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, 0);
            this.alpha = trackIterator.getAlpha(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, 0);
            this.radius = trackIterator.getRadius(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, 0);
            this.centerX = trackIterator.getX(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, 0);
            this.centerY = trackIterator.getY(position, left + width * column, top + width * row, width, color, 1, rectWidth, rectHeight, 0);
            this.rectWidth = rectWidth;
            this.rectHeight = rectHeight;

        }

        public float getCenterX() {
            return centerX;
        }

        public void setCenterX(float centerX) {
            this.centerX = centerX;
        }

        public float getCenterY() {
            return centerY;
        }

        public void setCenterY(float centerY) {
            this.centerY = centerY;
        }

        public float getRadius() {
            return radius;
        }

        public void setRadius(float radius) {
            this.radius = radius;
        }

        public int getColor() {
            return color;
        }

        public void setColor(int color) {
            this.color = color;
        }

        public float getAlpha() {
            return alpha;
        }

        public void setAlpha(float alpha) {
            this.alpha = alpha;
        }
    }
}

简单讲一下实现原理吧。粒子的爆炸效果通过属性动画不断迭代实现。具体由内部类ExplosionAnimator实现。ExplosionAnimator创建时将目标对象的bitmap缓存分割成若干小块,每个小块视为一个粒子,粒子颜色取小块右下角像素颜色,也就是这几句话:

particles = new Particle[rowCount][columnCount];

for (int row = 0; row < rowCount; row++) { //行
    for (int column = 0; column < columnCount; column++) { //列
        int color = bitmap.getPixel(column * particleRadius, row * particleRadius);
        particles[row][column] = new Particle(row * columnCount + column, color,
                particleRadius, bound.left, bound.top, bound.width(), bound.height(),
                column, row, bound.centerX(), bound.centerY());
    }
}

属性动画迭代时,通过ExplosionAnimator的draw方法不断更新粒子在屏幕上的显示位置。也就是这几句:

for (int row = 0; row < particles.length; row++) { //行
    for (int column = 0; column < particles[row].length; column++) { //列
        advance(row * particles[row].length + column, particles[row][column], (Float) getAnimatedValue());

        paint.setColor(particles[row][column].color);
        paint.setAlpha((int) (Color.alpha(particles[row][column].color) * particles[row][column].alpha));

        canvas.drawCircle(particles[row][column].centerX, particles[row][column].centerY, particles[row][column].radius, paint);
    }
}

具体的迭代算法由抽象算子TrackIterator实现,简单提一下TrackIterator算子中需要实现的方法:

initParticle():初始化算子,在爆炸开始之前会被调用
getX():决定了下一时刻粒子的x坐标。
getY():决定了下一时刻粒子的y坐标。
getRadius():决定了下一时刻粒子的半径。
getColor():决定了下一时刻粒子的颜色。
getAlpha():决定了下一时刻粒子的透明度。

 

控件自带了四种算子的实现,分别是:

NormalTrackIterator:普通随机算子,粒子向下无规则随机掉落
BezierTrackIterator:贝塞尔算子,粒子沿随机贝塞尔曲线运动
QuadraticTrackIterator:二次函数算子,粒子沿随机二次函数曲线运动
FreeFallTrackIterator:自由落体算子,粒子沿计算空气阻力的类平抛曲线运动

 

介绍完了,终于可以试一试效果了。先在布局里随便加个图片:




    

Antivity里简单写几句代码:

private ImageView imageView;

private ExplodeView explodeView;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    imageView = findViewById(R.id.imageview);

    explodeView = new ExplodeView(this);
    explodeView.setParticleRadius(3);
    explodeView.setTrackIterator(new FreeFallTrackIterator());

    explodeView.setOnExplodeListener(new ExplodeView.OnExplodeListener() {
        @Override
        public void onStart(View view) {

        }

        @Override
        public void onFinish(View view) {
            imageView.setVisibility(View.VISIBLE);
        }
    });

    imageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            explodeView.explode(imageView);
        }
    });
}

代码很简单,通过ExplodeView的explode方法传入任意View对象,就能让这个View呈现爆炸效果了。顺带一提,爆炸动画结束后对象的View默认被隐藏了,可以通过监听接口OnExplodeListener来在爆炸动画结束后执行自己指定的操作。

好了,看看效果吧: 

一个实现粒子爆炸效果的控件_第2张图片

 

再试试别的算子。换成贝塞尔算子:

explodeView.setTrackIterator(new BezierTrackIterator());

效果:

一个实现粒子爆炸效果的控件_第3张图片

 

再试试二次函数算子:

explodeView.setTrackIterator(new QuadraticTrackIterator());

效果:

一个实现粒子爆炸效果的控件_第4张图片

很像MiUI删除应用时的爆炸效果是不是?

 

最后再试试自由落体算子:

explodeView.setTrackIterator(new FreeFallTrackIterator());

效果:

一个实现粒子爆炸效果的控件_第5张图片

 

当然你们也可以自己实现TrackIterator来定义算子,实现想要的效果,这里只是抛砖引玉。

终于终于讲完了。最后再来总结一下。ExplodeView集成了爆炸功能,提供explode方法让指定View对象爆炸,提供setTrackIterator方法设置不同的爆炸算子,提供OnExplodeListener接口监听爆炸过程。还有一些可设置参数在源码注释里都写得比较清楚了,大家自己看注释就好。


最后的最后,附上源码地址:https://download.csdn.net/download/Sure_Min/12581569

 

这次的内容就到这里,我们下次再见。

你可能感兴趣的:(自定义控件)