您的位置:首页 > 移动开发 > Android开发

【Android效果集】下雨效果

2015-10-23 00:12 531 查看
本文参考学习视频教程-《Android 粒子效果之雨》

效果图:



本文在《【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()
以及获取了屏幕宽高,所以在子类中可以直接使用
windowWidth
widthHeight


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;
}
}


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

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

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

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

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;
}
}


可以看到只是在原来的单个基础上扩展成了组,现在的效果就基本形成了。



6.随机定制化

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

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);
}
...
}


效果就变成为了:



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

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

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




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

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



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

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



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

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

如果你有任何问题,欢迎留言告诉我!~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息