自定义View 粒子效果

描述

效果图:


image.png

要实现一张图片的爆炸效果,有几个关键的点:

第一点、根据图片的宽和高获取每一个像素的点,并且根据这个像素点构建在这一个像素点ball 对象;

第二点、在获取ball 的数组对象的时候,需要在子线程来做这个事情,防止UI卡顿。

第三点、启动一个动画来循环的调用绘制。

第四点、在绘制的时候需要把所有ball 都一一绘制。

创建一个粒子对象(Ball)。

    - 图片像素点颜色值color
- 粒子圆心坐标x
- 粒子圆心坐标y
- 粒子半径r
- 粒子运动水平方向速度vx
- 粒子运动垂直方向速度vy
- 粒子运动水平方式加速度ax
- 粒子运动垂直方向加速度ay
    public int color; //图片像素点颜色值
    public float x; //粒子圆心坐标x
    public float y; //粒子圆心坐标y
    public float r; //粒子半径

    public float vX;//粒子运动水平方向速度
    public float vY;//粒子运动垂直方向速度
    public float aX;//粒子运动水平方向加速度
    public float aY;//粒子运动垂直方向加速度

创建一个自定义view ,组合粒子。

 - 初始画笔Paint,bitmap对象,粒子直径(float类型)
     paint = new Paint();
     mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pic);
 - 获取所有的粒子集合
//计算粒子数
    private void calculateBalls() {
        if (mBitmap == null || mBitmap.isRecycled()) return;
        int width = mBitmap.getWidth();
        int height = mBitmap.getHeight();
        for (int i = 0; i < width; i += ballPoor) {
            for (int j = 0; j < height; j += ballPoor) {
                Ball ball = new Ball();

                int realWidth = ballPoor;
                int realHeight = ballPoor;
                if (i + realWidth > width) {
                    realWidth = width - i;
                }
                if (j + realHeight > height) {
                    realHeight = height - j;
                }
                //这里的像素值,不准确
                int[] colors = new int[realWidth * realHeight];
//            @param pixels   接收位图颜色的数组
//            @param offset   第一个要写入像素的索引[]
//            @param stride   以像素[]为单位的项数,可在其中跳过行(必须是>=位图的宽度)。可以是负的。
//            @param x        要读取的第一个像素的x坐标
//            @param y        要读取的第一个像素的y坐标
//            @param width    从每一行读取的像素数
//            @param height   要读取的行数
                mBitmap.getPixels(colors, 0, realWidth, i, j, realWidth, realHeight);
                int color = getColor(colors);
                ball.setColor(color);
                //线性增加多少,就缩小多少倍,这种来计算
                ball.x = i * d / ballPoor + d / 2;
                ball.y = j * d / ballPoor + d / 2;
                ball.setR((float) d / 2);
                //x方向速度20或者-20
                ball.setvX((float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random()));
                ball.setvY(rangeInt(-10, 20));
                ball.setaX(0);
                ball.setaY(0.98f);
                ballList.add(ball);
            }
        }
    }
  • 初始动画,并且监听动画变更,改变Ball的位置和invalidate();
    调用invalidate(),会执行绘制的方法,onDraw();
//        创建一个线程池,该线程池根据需要创建新线程,但是将重用以前构造的线程可用。这些池通常会提高性能
//        执行许多短期异步任务的程序。
//        对{@code execute}的调用将重用前面构造的
//        线程(如果可用)。如果没有现有线程可用,则使用一个新线程
//        线程将被创建并添加到池中。线程,
//        * 60秒内未使用的将被终止并移除
//        *缓存。因此,一个闲置足够长的池将会
//        *不消耗任何资源。注意,池与类似
//        *属性,但不同的细节(例如超时参数)
        valueAnimator = ValueAnimator.ofFloat(animBeginValue, animEndValue);
        //线性插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(0);//0   不重复,默认也是0
        valueAnimator.setDuration(duration);
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationFinish(ValueAnimator animation) {
                //动画结束
                Log.i("ball", "动画结束");
            }
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //动画每一帧的改变
                float value = (float) animation.getAnimatedValue();
//                Log.i("ball","动画value=====" +value);
                if (value == animEndValue) {
                    //动画执行结束
                    this.onAnimationFinish(animation);
                }
                //在动画改变的时候,调用绘制
                updateBallState();
                invalidate();
            }
        });
  • 循环绘制所有的粒子
 for (Ball ball : ballList) {
            paint.setColor(ball.getColor());
            canvas.drawCircle(ball.getX(), ball.getY(), ball.getR(), paint);
        }

改进

在布局文件中使用了自定义的view 的时候,宽度和高度,
如果没有使用确切值的情况,默认是填充满屏幕的。

完整代码:

package com.netease.canvas.split;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;

/**
 * 作者:liupeng
 * 创建时间:2019/4/26
 * 描述:优化,确认布局的宽和高度,不然会出现问题
 */
public class LZBallView extends View {
    //粒子的集合,通过获取图片的宽度或者view 的宽度来计算,子线程做这个事情哦
    private List<Ball> ballList = new ArrayList<>();
    private Paint paint;//画笔

    //粒子的直径.设置默认值,后续通过属性来设置
    private float d = 20;
    //动画
    private ValueAnimator valueAnimator;
    //动画时长,后面也是通过属性配置的
    private long duration = 3000;

    private float animBeginValue = 0;
    private float animEndValue = 1;
    //图片
    private Bitmap mBitmap;
    //用于控制粒子的个数,如果是一个像素的一个像素的添加,那么粒子数目较多,默认等于
    private int ballPoor = 10;

    //是否已经开始了动画
    private boolean startAnim;

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

    public LZBallView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    //初始化操作
    private void init() {
        paint = new Paint();
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pic);
//        创建一个线程池,该线程池根据需要创建新线程,但是将重用以前构造的线程可用。这些池通常会提高性能
//        执行许多短期异步任务的程序。
//        对{@code execute}的调用将重用前面构造的
//        线程(如果可用)。如果没有现有线程可用,则使用一个新线程
//        线程将被创建并添加到池中。线程,
//        * 60秒内未使用的将被终止并移除
//        *缓存。因此,一个闲置足够长的池将会
//        *不消耗任何资源。注意,池与类似
//        *属性,但不同的细节(例如超时参数)
        Executors.newCachedThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                calculateBalls();
            }
        });
        valueAnimator = ValueAnimator.ofFloat(animBeginValue, animEndValue);
        //线性插值器
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(0);//0   不重复,默认也是0
        valueAnimator.setDuration(duration);
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationFinish(ValueAnimator animation) {
                //动画结束
                Log.i("ball", "动画结束");
            }
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //动画每一帧的改变
                float value = (float) animation.getAnimatedValue();
//                Log.i("ball","动画value=====" +value);
                if (value == animEndValue) {
                    //动画执行结束
                    this.onAnimationFinish(animation);
                }
                //在动画改变的时候,调用绘制
                updateBallState();
                invalidate();
            }
        });
    }

    //计算粒子数
    private void calculateBalls() {
        if (mBitmap == null || mBitmap.isRecycled()) return;
        int width = mBitmap.getWidth();
        int height = mBitmap.getHeight();
        for (int i = 0; i < width; i += ballPoor) {
            for (int j = 0; j < height; j += ballPoor) {
                Ball ball = new Ball();

                int realWidth = ballPoor;
                int realHeight = ballPoor;
                if (i + realWidth > width) {
                    realWidth = width - i;
                }
                if (j + realHeight > height) {
                    realHeight = height - j;
                }
                //这里的像素值,不准确
                int[] colors = new int[realWidth * realHeight];
//            @param pixels   接收位图颜色的数组
//            @param offset   第一个要写入像素的索引[]
//            @param stride   以像素[]为单位的项数,可在其中跳过行(必须是>=位图的宽度)。可以是负的。
//            @param x        要读取的第一个像素的x坐标
//            @param y        要读取的第一个像素的y坐标
//            @param width    从每一行读取的像素数
//            @param height   要读取的行数
                mBitmap.getPixels(colors, 0, realWidth, i, j, realWidth, realHeight);
                int color = getColor(colors);
                ball.setColor(color);
                //线性增加多少,就缩小多少倍,这种来计算
                ball.x = i * d / ballPoor + d / 2;
                ball.y = j * d / ballPoor + d / 2;
                ball.setR((float) d / 2);
                //x方向速度20或者-20
                ball.setvX((float) (Math.pow(-1, Math.ceil(Math.random() * 1000)) * 20 * Math.random()));
                ball.setvY(rangeInt(-10, 20));
                ball.setaX(0);
                ball.setaY(0.98f);
                ballList.add(ball);
            }
        }
    }

    //获取颜色
    private int getColor(int[] colors) {
        int ar = 0, ag = 0, ab = 0;
        for (int color : colors) {
            int r = (color & 0xff0000) >> 16;
            int g = (color & 0x00ff00) >> 8;
            int b = color & 0x0000ff;
            ar += r;
            ag += g;
            ab += b;
        }
        return Color.rgb(ar / colors.length, ag / colors.length, ab / colors.length);
    }

    private float rangeInt(int i, int j) {
        int max = Math.max(i, j);
        int min = Math.min(i, j) - 1;
        //在0到(max - min)范围内变化,取大于x的最小整数 再随机
        return (int) (min + Math.ceil(Math.random() * (max - min)));
    }

    //动画执行过程中,改变粒子的状态
    private void updateBallState() {
        for (Ball ball : ballList) {
            ball.setX(ball.getX() + ball.getvX());
            ball.setY(ball.getY() + ball.getvY());
            ball.setvX(ball.getvX() + ball.getaX());
            ball.setvY(ball.getvY() + ball.getaY());
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //执行动画
            startAnim = true;
            valueAnimator.start();
        }
        return super.onTouchEvent(event);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int mHeight = MeasureSpec.getSize(heightMeasureSpec);

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制时间过长会直接崩溃的
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        //画布的平移操作
        if (width >= mBitmap.getWidth()) {
            if (height >= mBitmap.getHeight()){
                canvas.translate(width/2 -mBitmap.getWidth()/2, height/2 -mBitmap.getHeight()/2);
            }else{
                canvas.translate(width/2 -mBitmap.getWidth()/2, 0);
            }
        }else {
            //view 的宽度是小于图片的宽度的什么都不做
            if (height >= mBitmap.getHeight()){
                canvas.translate(0, height/2 -mBitmap.getHeight()/2);
            }else{
                canvas.translate(0, 0);
            }

        }

        if (!startAnim) {
            canvas.drawBitmap(mBitmap, 0, 0, paint);
            return;
        }
        for (Ball ball : ballList) {
            paint.setColor(ball.getColor());
            canvas.drawCircle(ball.getX(), ball.getY(), ball.getR(), paint);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (valueAnimator != null) {
            valueAnimator.cancel();
        }
    }

    interface AnimatorUpdateListener extends ValueAnimator.AnimatorUpdateListener {
        void onAnimationFinish(ValueAnimator animation);
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容