效果图:
本文在《【Android效果集】弹幕效果 》基础上实现,建议先阅读完再看本文。
跟着上一篇介绍弹幕效果的文章相比,这一篇其实和上一篇很类似,虽然效果看起来大相径庭,看下实现就会发现很相似,可以学会来然后举一反三做出很多好玩的动画效果!~
我们首先来分析一下每个雨点效果,每个雨点其实就是一条倾斜直线,从屏幕上/左方出来,到屏幕下/右方消失,期间沿着直线的方向移动。
不是很像弹幕吗?弹幕是从右边出来,水平移动到左边出去。
1.在上一篇弹幕项目基础上,重构出一个BaseView,自定义RainView代表一个雨滴,继承自BaseView
2.RainView能够自动从最上面移动到最下边,且有一定倾斜角度,移动过程中一直保持着同一个倾斜角度
3.重构提取出单个雨滴类,将RainView改为包含多个雨滴代表一场雨
4.随机定制化,比如倾斜角度,颜色等
先上代码,待会解释为什么要重构。
/** * 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里面的代码。我们写代码遇到有大量重复代码的时候怎么办?提取抽象类!正是基于此点考虑才重构的。
我们新建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() {
}
}
首先我们需要画条线,一条线有两个坐标点(两点确定一条直线),(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加到主布局里去)
为什么要重构出一个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轴)。
现在我们要制造下雨场景只需要制造多个雨点对象,然后像最开始控制一个雨点那样去修改代码。
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;
}
}
可以看到只是在原来的单个基础上扩展成了组,现在的效果就基本形成了。
如果你觉得所有雨点都是一个方向地不真实,你可以在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^依旧求星星)
如果你有任何问题,欢迎留言告诉我!~