别的不说,先上效果图
图中的上下两行分别代表的是分钟的数字和秒钟的数字,这个东西是我一个前端的同学首先用JS在网页先实现了效果的,于是我去尝试在Android端实现这个东西,代码我已上传在csdn,欢迎大家一起讨论。 点击这里下载源代码
接下来该讲讲在这次实现功能的过程中遇到的一些问题。由于学到了比较多的东西,一些东西我就精简而过。
首先,要做这个东西,就得先想怎么去实现它,自定义View的实现首当其冲,由于从逻辑上来讲,每个小球都得是一个单独跳动的对象,所以,不得不为每一个小球实例化一个对象,这样一来,在写的时候也会容易有思路,因为就像单独判断一个小球的运动而已。因为要形成动画,所以必须每隔一段时间就重绘一次,这个时候就需要有一个计时操作了。
一开始我用的是在自定义View里面放Handler来实现耗时操作,这显然非常不科学,我就不讲了。。
第二次改,用了List存放每一个运动的小球,在Activity中进行计时操作来实现对控制每个小球运动。
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
for(Ball ball:list)
ball.falling();
}
};
一开始每出现什么问题,但是到后来,运动的小球越来越多,资源却没有被回收,导致应用非常消耗资源,所以就想,必须加判断,如果小球不在屏幕内了,将它移出Layout,也移出List。这时候问题就来了,非UI线程中不能进行UI操作,无法从Layout中remove掉没有用的小球,那就想别的办法!于是又用回了Handler。
mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
if (msg.what == 1) {
for (Ball ball : list) {
if (ball.outOfScreen()) {
//若小球已不在屏幕内,移出界面,并从list中移出
layout.removeView(ball);
list.remove(ball);
} else
//否则继续下掉
ball.falling();
}
sendEmptyMessageDelayed(1, 20);//延迟20毫秒再次发送消息
}
};
};
可是这个时候问题又来了!List遍历过程不能中不能删除当前元素,否则会有java.util.ConcurrentModificationException异常,除了Iterator的remove()方法可以实现,一波三折,最终确定为
mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
if (msg.what == 1) {
Ball ball;
for (Iterator iterator = mMovingBall.iterator();iterator.hasNext();) {
ball = (Ball) iterator.next();
if (ball.outOfScreen()) {
//若小球已不在屏幕内,移出界面,并从list中移出
mContainer.removeView(ball);
iterator.remove();
} else
//否则继续下掉
ball.falling();
}
sendEmptyMessageDelayed(1, 20);//延迟20毫秒再次发送消息
}
};
};
这里移出小球后,由于java的垃圾回收机制,内存总会被回收的(不保证什么时候回收)。
到这一步解决完了之后,小球的掉落动画明显变得很流畅了。
至于动画的完成,很明显就是调用invalidate()方法,并改变小球的圆心坐标,这时候快速重绘就能形成动画的效果,再判断小球是否触及屏幕底部,如果触及,将竖直方向的速度变为负即可,这样就完成了抛物线式的弹跳(期间可能要考虑“能量消耗”,所以适当减速也可,但是我没有做,因为往往看不出效果的时候,小球就已经跳出屏幕了)。
第一次对View重写,有一个东西也不是很理解,在重新调用invalidate()方法时,为什么上一次onDraw()画的东西不会还留在屏幕上,于是我觉得应该仔细看看invalidate()的实现。
查看了View的源代码,发现invalidate()实际上调用的是draw()方法,而在draw()方法中有以下注释
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
onDraw过程中绘制的图形是在canvas即画布上的,而画布实质上指的是View的背景,draw()过程的第一步就重新绘制了background,所以原先画的图形并不会保留下来,到第三步中有
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
这时候再调用onDraw(),则后来再绘制的图形就覆盖在新的背景上了(若无属性设置背景,则背景为透明)。