粒子效果之雨的实现

创作启发

在极客学院看到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);这行代码删掉
粒子效果之雨的实现_第1张图片此时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<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。

看一下效果图
粒子效果之雨的实现_第2张图片粒子效果之雨的实现_第3张图片
该项目的示例地址为:https://github.com/Ailsa2019/starfiled ,欢迎大家学习探讨。

你可能感兴趣的:(#,高级UI)