#|粒子效果之雨的实现

创作启发 在极客学院看到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 * * 2019/3/7 0007 * * 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); 这行代码删掉
#|粒子效果之雨的实现
文章图片
此时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 * * 2019/3/7 0007 * * 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 * * 2019/3/7 0007 * * 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 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 ,欢迎大家学习探讨。

    推荐阅读