粒子效果之雨的实现

创作启发

在极客学院看到FreeHeart老师讲解的《Android粒子效果之雨》,跟着学习了一下。但是运行效果中,雨点数量明显大于设定的数量且越来越多。为了解决这个问题,所以自己重新写了一遍代码,进行了封装,实现自己喜欢的效果。


接下来,让我们一步一步实现这个效果吧。

第一步:封装一个基础View

首先,新建一个 BaseRainView.class ,并继承系统自带的 View

import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * @author ailsa
 * <p>
 * 2019/3/7 0007
 * <p>
 * BaseRainView,下雨效果的基础View
 */
public class BaseRainView extends View {

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

    public BaseRainView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    
}

接下来,我们需要用到 onDraw() 方法进行雨点的绘制,所以需要在BaseRainView中添加onDraw()方法。在下图中我们可以看到父类View中的onDraw()方法没有实现的内容,所以在BaseRainView.class的onDraw()中可以将super.onDraw(canvas);这行代码删掉
View类中的onDraw()方法是空实现此时BaseRainView.class中的onDraw()方法没有任何实现代码

 @Override
 protected void onDraw(Canvas canvas) {
        
 }

我们知道,雨点是从上到下降落的,所以我们自onDraw()中绘制的雨点也应该是从上到下不断移动的,那么我们可以用什么实现呢??

——我们可以使用postInvalidate()方法不断调用onDraw()进行重绘,重绘是通过改变雨点的位置来实现每次的绘制位置的不同,所以我们还需要使用一个Thread进行位置的改变(UI线程不允许操作数据)。所以我们在BaseRainView.class中添加如下代码

class MThread extends Thread {
	@Override
    public void run() {
		while (true) {
			postInvalidate();
           	try {
				Thread.sleep(30);
           	} catch (InterruptedException e) {
               	e.printStackTrace();
           	}
      	}
  	}
}

我们还差些什么呢??

——没有实现雨点绘制的代码,包括使用画笔画布绘制雨点,雨点位置的改变,多个雨点的效果。为此,我们需要三个方法 initRainDrops()drawRainDrops()moveRainDrops() 用来实现这些功能。此时,BaseRainView.class的所有内容如下

/**
 * @author ailsa
 * <p>
 * 2019/3/7 0007
 * <p>
 * BaseRainView,下雨效果的基础View
 */
public abstract class BaseRainView extends View {
    /**
     * 自定义线程,实现雨的移动效果
     */
    private MThread thread;

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

    public BaseRainView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 初始化所有雨点 [ 子类实现 ]
     */
    protected abstract void initRainDrops();

    /**
     * 绘制所有雨点 [ 子类实现 ]
     *
     * @param canvas 画布
     */
    protected abstract void drawRainDrops(Canvas canvas);

    /**
     * 移动所有雨点 [ 子类实现 ]
     */
    protected abstract void moveRainDrops();

    @Override
    protected void onDraw(Canvas canvas) {
        if (thread == null) {
            initRainDrops();            // 初始化所有雨点
            thread = new MThread();
            thread.start();
        } else {
            drawRainDrops(canvas);      // 绘制所有雨点
        }
    }

    class MThread extends Thread {
        @Override
        public void run() {
            while (true) {
                moveRainDrops();            // 移动所有雨点
                postInvalidate();           // 调用onDraw()重绘
                try {
                    Thread.sleep(30); // 休眠30ms后再次执行移动逻辑
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

【注意】 initRainDrops()这行代码一定要放在onDraw()方法中,且只调用一次就够了。如果放在moveRainDrops()的while循环中,将导致每一次运行Thread都会向list中添加item。最终,整个界面的雨点会越来越多。这就是我写本文的初衷。

至此,BaseRainView的封装就实现好了,让我们开始下一步。


第二步:实现单个雨点的绘制

我们新建一个 RainDrop.class 文件,实现单个雨点下落效果。

我们思考一下,这个文件里需要什么内容呢??

——需要绘制出一个雨点,实现该雨点移动(下落)。所以我们自定义两个方法 drawSingleRainDrop(Canvas canvas)moveSingleRainDrop()

我们可以用canvas的drawLine()方法绘制一条直线(雨点),该方法需要5个参数startX,startY,stopX,stopY,paint,所以我们定义这5个参数,并进行初始化。

 /**
  * 画笔在画布x/y方向的起始、终止位置
  */
 private int startX;
 private int startY;
 private int stopX;
 private int stopY;
 /**
  * 画笔
  */
 private Paint paint;
 /**
  * 参数初始化
  */
 private void init() {
	paint = new Paint();
    paint.setColor(0xffffffff);
  	startX = 100;
    startY = 100;
    stopX = startX;
    stopY = startY + 30;
 }

接下来,我们自定义一个drawDrop(Canvas canvas)方法,使用canvas绘制雨点形状

/**
 * 绘制单个雨点
 *
 * @param canvas 画布
 */
 void drawSingleRainDrop(Canvas canvas) {
	canvas.drawLine(startX, startY, stopX, stopY, paint);
 }

为了实现雨点的移动效果,我们还需要自定义一个moveDrop()方法,该方法确定了雨点每次移动多少距离,雨点移出屏幕后应从屏幕上方再次向下移动

/**
 * 单个雨点的移动逻辑
 */
 void moveSingleRainDrop() {
	startY += 30;
    stopY += startY;
    if (startY > height) {
		init();
    }
 }

因为需要判断屏幕的高度,所以我们还需要外部传入一个height参数

/**
 * 屏幕高度
 */
private int height;

RainDrop(int height) {
	this.height = height;
    init();
}

第三步:实现多个雨点的绘制

我们新建一个 RainView.class ,继承自BaseRainView,实现一场雨的效果。

import android.content.Context;
import android.graphics.Canvas;
import android.support.annotation.Nullable;
import android.util.AttributeSet;

/**
 * @author ailsa
 * <p>
 * 2019/3/7 0007
 * <p>
 * RainView,下雨效果的具体实现 
 */
public class RainView extends BaseRainView {
    
    public RainView(Context context) {
        super(context);
    }

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

    @Override
    protected void initRainDrops() {

    }

    @Override
    protected void drawRainDrops(Canvas canvas) {

    }

    @Override
    protected void moveRainDrops() {

    }
}

我们可以使用List来存储多个雨点。接下来,我们声明一个List并在构造函数里初始化

/**
 * 雨点集合
 */
private List<RainDrop> rainDrops;

public RainView(Context context) {
	super(context);
	rainDrops= new ArrayList<>();
}

public RainView(Context context, @Nullable AttributeSet attrs) {
	super(context, attrs);
    rainDrops= new ArrayList<>();
}

我们使用for循环向List中添加雨点

@Override
protected void initRainDrops() {
	for (int i = 0; i < 5; i++) {
		rainDrops.add(new RainDrop(getHeight()));
    }
}

我们使用for循环在drawRainDrops()中绘制所有雨点

@Override
protected void drawRainDrops(Canvas canvas) {
	for (RainDrop rainDrop : rainDrops) {
		rainDrop.drawSingleRainDrop(canvas);
    }
}

同样的,我们使用for循环在moveRainDrops()中移动所有的雨点

@Override
protected void moveRainDrops() {
	for (RainDrop rainDrop : rainDrops) {
		rainDrop.moveSingleRainDrop();
	}
}

这样,整个逻辑就搭建完成了。但是我们在模拟器中看到的只是一个雨点,并没有多个雨点的效果呀,这是为什么呢??

因为我们绘制的所有雨点的位置都是一样的,所有雨点重叠在一起,导致我们只看到一个雨点的效果。

那我们可以怎么解决呢??

要想各个雨点独立,最主要的就是改变它们的位置,但是我们不可能给每个雨点单独初始化位置,如果有成百上千的雨点,每个都初始化位置,可想而知,该是多么糟糕呀。我们可以使用随机数Random,如此每个雨点都能有一个不同的位置。所以我们来改一下RainDrop.class文件。

优化
  1. 使用随机数初始化雨点位置
/**
 * 随机数
 */
private Random random;
/**
 * 参数初始化
 */
private void init() {
	random = new Random();
	startX = random.nextInt(width);
    startY = random.nextInt(height);
	...
}

我们可以看到,startY 的随机数范围是整个屏幕的高度,startX 的随机数范围是整个屏幕的宽度,所以我们还需要外部传入一个width参数

/**
 * 屏幕宽度
 */
private int width;

RainDrop(int height, int width) {
	this.height = height;
    this.width = width;
    init();
}
  1. 使用speed参数控制雨点下落速度

我们想要雨点每次下落的速度也不一致,实现更加真实的效果,所以我们需要使用一个speed参数,也用random随机生成

/**
 * 速度
 */
private float speed;
/**
 * 参数初始化
 */
private void init() {
	speed = 0.2f + random.nextFloat();
}
/**
 * 单个雨点的移动逻辑
 */
void move() {
    startY += 30 * speed;
    stopY += 30 * speed;
    if (startY > height) {
		init();
    }
}
  1. 提取offsetX、offsetY

我们看到RainDrop文件中雨点每次移动都有一个偏移量,我们可以将这个偏移量提取成参数

/**
 * 线条在x/y方向的偏移量
 */
private int offsetX;
private int offsetY;

如果我们想要实现雨点垂直下落的效果,那么只需要从外部传入一个offsetY值,初始化offsetX为0即可

RainItem(int height, int width,int offsetY) {
	this.height = height;
    this.width = width;
    // 参数初始化
    offsetX = 0;
    this.offsetY = offsetY;
    init();
}

如果我们想实现雨点倾斜下落效果(风吹),需要从外部传入offsetX值和offsetY值

RainItem(int height, int width, int offsetX, int offsetY) {
	this.height = height;
    this.width = width;
    this.offsetX = offsetX;
    this.offsetY = offsetY;
    init();
}

此时,我们还需要修改一下init()、move()方法中的内容

/**
 * 参数初始化
 */
private void init() {
	startX = random.nextInt(width);
    startY = random.nextInt(height);
    stopX = startX + offsetX;
    stopY = startY + offsetY;
    speed = 0.2f + random.nextFloat();
}
/**
 * 单个雨点的移动逻辑
 */
void move() {
	startX += offsetX * speed;
    stopX += offsetX * speed;
    startY += offsetY * speed;
    stopY += offsetY * speed;
    if (startY > height) {
		init();
    }
}

至此,RainDrop.class文件的内容就修改完毕了。

我们再来看一下RainView文件,里面有一个数值也可以提取成参数,即List的item数量

/**
 * 雨点数量
 */
private int rainDropCount= 20;

@Override
protected void initRainDrops() {
	for (int i = 0; i < rainDropCount; i++) {
		itemList.add(new RainItem(getHeight(), getWidth()));
    }
}

如此,所有的修改都完成了,我们得到了一个相对完美的自定义View。

看一下效果图
垂直下雨效果倾斜下雨效果
该项目的示例地址为:https://github.com/Ailsa2019/starfiled ,欢迎大家学习探讨。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值