【Android效果集】下雨效果

本文参考学习 视频教程-《Android 粒子效果之雨》

效果图:

【Android效果集】下雨效果_第1张图片

本文在《【Android效果集】弹幕效果 》基础上实现,建议先阅读完再看本文。

跟着上一篇介绍弹幕效果的文章相比,这一篇其实和上一篇很类似,虽然效果看起来大相径庭,看下实现就会发现很相似,可以学会来然后举一反三做出很多好玩的动画效果!~

我们首先来分析一下每个雨点效果,每个雨点其实就是一条倾斜直线,从屏幕上/左方出来,到屏幕下/右方消失,期间沿着直线的方向移动。

不是很像弹幕吗?弹幕是从右边出来,水平移动到左边出去。

实现思路

1.在上一篇弹幕项目基础上,重构出一个BaseView,自定义RainView代表一个雨滴,继承自BaseView
2.RainView能够自动从最上面移动到最下边,且有一定倾斜角度,移动过程中一直保持着同一个倾斜角度
3.重构提取出单个雨滴类,将RainView改为包含多个雨滴代表一场雨
4.随机定制化,比如倾斜角度,颜色等

详细过程

1.重构出一个BaseView

先上代码,待会解释为什么要重构。

/** * Created by AZZ on 15/10/20 21:20. */
public abstract class BaseView extends View {

    protected AnimThread animThread;
    protected int windowWidth; //屏幕宽
    protected int windowHeight; //屏幕高

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

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

    /** * 初始化 */
    protected void init() {
        Rect rect = new Rect();
        getWindowVisibleDisplayFrame(rect);
        windowWidth = rect.width();
        windowHeight = rect.height();
    }
    //---------------------------------------------------- 画布操作
    /** * 画子类 */
    protected abstract void drawSub(Canvas canvas);

    @Override
    protected void onDraw(Canvas canvas) {
        drawSub(canvas);
        if (animThread == null) {
            animThread = new AnimThread();
            animThread.start();
        }
    }
    //---------------------------------------------------- 动画操作
    /** * 动画逻辑处理 */
    protected abstract void animLogic();

    /** * 里面根据当前状态判断是否需要返回停止动画 * @return 是否需要停止动画thread */
    protected abstract boolean needStopAnimThread();

    /** * @return 线程睡眠时间,值越大,动画越慢,值越小,动画越快 */
    protected int sleepTime() {
        return 30;
    }

    /** * 动画结束后做的操作,比如回收资源 */
    protected abstract void onAnimEnd();

    class AnimThread extends Thread {
        @Override
        public void run() {
            while(true) {
                //1.动画逻辑
                animLogic();
                //2.绘制图像
                postInvalidate();
                //3.延迟,不然会造成执行太快动画一闪而过
                try {
                    Thread.sleep(sleepTime());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //关闭线程逻辑判断
                if (needStopAnimThread()) {
                    Log.i("BaseView", " -线程停止!");

                    if (mOnAnimEndListener != null) {
                        mOnAnimEndListener.onAnimEnd();
                    }
                    onAnimEnd();
                    break;
                }
            }
        }
    }

    //---------------------------------------------------- 外部监听器,监听动画结束

    private OnAnimEndListener mOnAnimEndListener;
    /** * @param onAnimEndListener 设置滚动结束监听器 */
    public void setOnRollEndListener(OnAnimEndListener onAnimEndListener) {
        this.mOnAnimEndListener = onAnimEndListener;
    }
    /** * 滚动结束接听器 */
    interface OnAnimEndListener {
        void onAnimEnd();
    }
}

可以看到我在BaseView中提取出了init()——初始化,drawSub()——画布操作,animLogic()——动画操作和动画结束监听器。

为什么要这么做呢?我刚刚也讨论过了,下雨效果和弹幕效果实现十分相似,可以说它们的实现代码有很多重合的地方,而这些重复的地方正是上面BaseView里面的代码。我们写代码遇到有大量重复代码的时候怎么办?提取抽象类!正是基于此点考虑才重构的。

2.自定义RainView

我们新建RainView继承BaseView,除了两个构造方法,我们要继承实现的有5个方法:
init() - 在这里面做初始化操作,因为在BaseView中的init()以及获取了屏幕宽高,所以在子类中可以直接使用windowWidthwidthHeight

drawSub() - 在这里面绘制子类图像,待会我们就要这里面绘制雨点那条线。

animLogic() - 看BaseView中知道这个方法是每30ms调用一次,调用完该方法后就会重绘,也就是重新调用drawSub()方法,所以我们需要在animLogic()做一些参数修改,比如坐标的变化。

needStopAnimThread() - 在这个方法做一些边界判断,以及根据判断结果来选择是否要返回true来停止线程动画。

onAnimEnd() - 当needStopAnimThread()返回true时,可以在这个方法中做些操作,比如在弹幕效果中,动画结束时把BarrageView从父控件中移除达到回收资源的效果。

sleepTime() - 这个是可选实现,默认父类中返回30ms,子类中可重写,以达到改变动画执行速率的效果。

public class RainView extends BaseView {

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

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

    /** * 初始化 */
    @Override
    protected void init() {
        super.init();
    }

    /** * 画子类 * @param canvas */
    @Override
    protected void drawSub(Canvas canvas) {
    }

    /** * 动画逻辑处理 */
    @Override
    protected void animLogic() {
    }

    /** * 里面根据当前状态判断是否需要返回停止动画 * * @return 是否需要停止动画thread */
    @Override
    protected boolean needStopAnimThread() {
        return false;
    }

    /** * 动画结束后做的操作,比如回收资源 */
    @Override
    protected void onAnimEnd() {
    }
}

3.绘制第一条雨点

首先我们需要画条线,一条线有两个坐标点(两点确定一条直线),(startX,startY)->(stopX,stopY),当stopX > startX并且stopY > startY的时候,就画出了一条倾斜的直线。

而如果我们想让一条倾斜的直线倾斜着移动,怎么做?难道还要算角度和比例吗?

其实很简单,只需要让“这条线”上的两个坐标点的x坐标加上一个deltaX (deltaX = stopX - startX),让两个坐标点的y坐标加上一个deltaY (deltaY = stopY - startY)

public class RainView extends BaseView {
    private int startX;
    private int startY;
    private int stopX;
    private int stopY;

    private int deltaX = 20;
    private int deltaY = 30;

    private Paint paint;

    @Override
    protected void init() {
        startX = 0;
        startY = 30;

        stopX = startX + deltaX;
        stopY = startY + deltaY;

        paint = new Paint();
        if (paint !=null) {
            paint.setColor(0xffffffff); //白色
        }
    }

    @Override
    protected void drawSub(Canvas canvas) {
            canvas.drawLine(startX, startY, stopX, stopY, paint);   
    }

    @Override
    protected void animLogic() {
        startX += deltaX;
        stopX += deltaX;
        startY += deltaY;
        stopY += deltaY;
    }

}

这个时候运行,以及能看到一条雨点滴落啦!~(对了前提别忘了把自定义View加到主布局里去)

4.提取出单个雨点的相关属性,重构RainLine类表示单个雨点

为什么要重构出一个RainLine类呢?为什么不和之前弹幕一样,一个BarrageView就是一条弹幕呢?

这是因为在下雨的场景中,雨点的数量是非常庞大的,从几百到几千都是可能的,而我们在RainView里面是用线程刷新重绘来实现动画的,当同时有几百个RainView在一个场景下时,也就是说系统同时运行着几百个线程,这是非常可怕的。事实上开始时我确实是这么做的,实验后发现线程数超过100程序就卡的不行了。

我们提取出专门的一个雨点类,然后在RainView中的一个线程里重绘几千个雨点都是没有问题的。

public class RainLine {
    private Random random = new Random();

    private int startX;
    private int startY;
    private int stopX;
    private int stopY;

    private int deltaX = 20;
    private int deltaY = 30;

    private int maxX; //x最大范围
    private int maxY; //y最大范围


    public RainLine(int maxX, int maxY) {
        this.maxX = maxX;
        this.maxY = maxY;
        initRandom();
    }

    public void initRandom() {

        startX = random.nextInt(maxX);
        startY = random.nextInt(maxY);

        stopX = startX + deltaX;
        stopY = startY + deltaY;
    }

    /** * 随机初始化 */
    public void resetRandom() {
        if (random.nextBoolean()) { //随机 true, 雨点从x轴出来
            startY = 0;
            startX = random.nextInt(maxX);
        } else { //随机 false,雨点从y轴出来
            startX = 0;
            startY = random.nextInt(maxY);
        }
        stopX = startX + deltaX;
        stopY = startY + deltaY;
    }
    /** * 下雨 */
    public void rain() {
        startX += deltaX;
        stopX += deltaX;
        startY += deltaY;
        stopY += deltaY;
    }

    /** * @return 是否出界 */
    public boolean outOfBounds() {
        if (getStartY() >= maxY || getStartX() >= maxX) {
            resetRandom();
            return true;
        }
        return false;
    }
}

除了重构,我还加了几个方法,

首先,构造方法传入了maxXmaxY,也就是屏幕宽高,为后面越界处理做准备。

initRandom() - 初始化的时候雨点随机在屏幕的各个地方。

rain() - 下雨方法也就是把在RainViewanimLogic()里面的操作提取出来。

outOfBounds() - 用于判断雨点是否超过界限,超过界限有两种,最右边和最下边都算越界,当越界时我们让雨点重新回到屏幕最上方或最左方,然后重新开始动画。

resetRandom() - 随机重置,当越界后调用此方法可达到重利用。这个方法里面重置雨点起始位置分两种,一个从最上面出来(x轴),一个从最左边出来(y轴)。

5.在RainView里定义多个雨点对象

现在我们要制造下雨场景只需要制造多个雨点对象,然后像最开始控制一个雨点那样去修改代码。

public class RainView extends BaseView {
    private ArrayList<RainLine> rainLines;
    private static final int RAIN_COUNT = 1000; //雨点个数

    @Override
    protected void init() {
        super.init();
        rainLines = new ArrayList<RainLine>();
        for (int i = 0; i < RAIN_COUNT; i++) {
            rainLines.add(new RainLine(windowWidth, windowHeight));
        }
        ...
    }

    @Override
    protected void drawSub(Canvas canvas) {
        for(RainLine rainLine : rainLines) {
            canvas.drawLine(rainLine.getStartX(), rainLine.getStartY(), rainLine.getStopX(), rainLine.getStopY(), paint);
        }
    }

    /** * 动画逻辑处理 */
    @Override
    protected void animLogic() {
        for(RainLine rainLine : rainLines) {
            rainLine.rain();
        }
    }

    @Override
    protected boolean needStopAnimThread() {
        for(RainLine rainLine : rainLines) {
            if (rainLine.getStartY() >= getWidth()) {
                rainLine.resetRandom();
            }
        }
        return false;
    }
}

可以看到只是在原来的单个基础上扩展成了组,现在的效果就基本形成了。
【Android效果集】下雨效果_第2张图片

6.随机定制化

如果你觉得所有雨点都是一个方向地不真实,你可以在RainLine中改变deltaXdeltaY为随机值。

    public void initRandom() {

        ...

        deltaX = random.nextInt(20);
        deltaY = random.nextInt(30);

        ...
    }


    public void resetRandom() {
        if (random.nextBoolean()) { //随机 true, 雨点从x轴出来
            ...
            deltaX = random.nextInt(20);
        } else { //随机 false,雨点从y轴出来
            ...
            deltaY = random.nextInt(30);
        }
        ...
    }

效果就变成为了:

【Android效果集】下雨效果_第3张图片

(我好像已经学会了飘雪效果了?)

这场雨看起来像毛毛雨是因为我们y值给的太少,因为移动速度也就是rain()方法里面,y轴方向上的增量就是deltaY,并且stopY = startY + deltaY,所以当deltaY比较小时,雨点既比较短小,又下降比较慢,所以看起来像毛毛雨(飘雪)了。

更改的办法有几个,可以在算下降速度时乘以个比例值,也可以像我这样比较简单的做法,将deltaY = random.nextInt(30);改为deltaY = 20 + random.nextInt(30);

【Android效果集】下雨效果_第4张图片

(是不是更逼真了,像大暴雨!因为我偷偷把雨点数增加了哈)

还可以改为随机颜色,剩下的就自己去试了。

【Android效果集】下雨效果_第5张图片

(好像又已经学会了礼花的效果了?)

还可以随心所欲地乱改。。。

【Android效果集】下雨效果_第6张图片

(我家电视屏幕又花屏了!)

源码地址:https://github.com/Xieyupeng520/AZBarrage/tree/rainview(^3^依旧求星星)

如果你有任何问题,欢迎留言告诉我!~

你可能感兴趣的:(动画,android,效果,自定义view,下雨)