第四讲 动画的制作
■ 前言
在前一讲中我们讲述了如何利用低级API制作图表应用程序的方法。如果仅仅是图表内容的表示,那么在WEB上就可以用静止的页面表示。但是在手机应用程序中表示的话,就能让图表内容具有动感。这就是应用程序与WEB的区别,这也正是其优点所在。在本讲中我们将进一步讲述如何在手机应用程序中制作动画,并详细介绍其具体操作方法。
■ 4-1 动画
基本上,动画能够表现出文字、图形、图片等的描绘位置和画面自身的连续变化。目前的程序中,按下按键,就会实行既定的方法。直到实行完既定方法之后才能进行其他方法操作。但是,表示动画过程中,不实行动画表示以外的方法就没有什么意义,因此应该有必要在表示动画的同时也可以实行其他的方法。所以掌握线程和计时器技术的知识和应用是必不可少的。接下来,我们将详细介绍线程和计时器。
■ 4-1-1 线程
线程是执行程序的最小单位,用来执行Standalone的Java应用程序的main()方法。当你需要同步执行多个操作,可以用多线程并发执行。使用这种线程的程序被称为“多线程程序”。
■ 4-1-1-1 线程的使用
首先我们尽快学习怎样使用“线程”。有两种方法能使用线程:Runnable接口的使用方法和Thread类的继承类的定义方法。在此,我们使用前一个方法—实现Runnable接口的方法。
实现Runnable接口的类必须要覆盖run()方法。覆盖的run()方法中记述了用线程执行的处理。(ex. 1)
class TestCanvas extends Canvas implements Runnable{ /** * 线程执行的处理 **/ public void run(){ //记述处理 . . } /** * 描绘方法 **/ public void paint(Graphics g){ //描绘处理 . . } } |
ex. 1
另外一种方法是使用线程类Thread。如下所示:用构造函数传递一个参数到该类的实例,该参数是实现Runnable接口的。使用start()方法就启动该线程了。(ex. 2)
Runnable runnable = new TestCanvas();//实现Runnable类 Thread thread = new Thread(runnable); thread.start(); |
ex. 2
■ 4-1-1-2 制作简单的动画
接着我们来看线程如何被用于制作简单的动画。在下面的实例中,球从左向右运动。(ex. 3)
import javax.microedition.lcdui.Display; import javax.microedition.midlet.MIDlet; import javax.microedition.midlet.MIDletStateChangeException; /** * 球飞出去的实例动画 */ public class MovingBall extends MIDlet { private Display display; private MovingBallCanvas canvas; /** * 构造函数 */ public MovingBall(){ display = Display.getDisplay(this); canvas = new MovingBallCanvas(); } /** * 运行时的处理 */ protected void startApp() throws MIDletStateChangeException { display.setCurrent(canvas); } protected void pauseApp() {} protected void destroyApp(boolean arg0) throws MIDletStateChangeException {} } import javax.microedition.lcdui.Canvas; import javax.microedition.lcdui.Graphics; /** * 球飞出去的实例动画的canvas。 */ public class MovingBallCanvas extends Canvas implements Runnable{ private int x;//球的X坐标 /** * 构造函数 */ public MovingBallCanvas(){ Thread th = new Thread(this); th.start(); } /** * 线程运行的处理 */ public void run() { while(true){ x += 3;//更改球的坐标 repaint();//再次描绘 try{ Thread.sleep(100);//100毫秒待机 }catch(InterruptedException e){ } } } /** * 描绘方法 */ protected void paint(Graphics g) { //清除画面 g.setColor(255,255,255);//白色 g.fillRect(0,0,getWidth(),getHeight()); //表示球 g.setColor(255,0,0); g.fillArc(x,50,20,20,0,360); } } |
paint()中g.fillArc(x,50,20,20,0,360)的变量X会改变球的位置,在run()中的while循环里该变量是递增的。这样,球就可以从左向右运动了(図 1)。
Figure 1 |
■ 4-1-1-3 线程的待机处理
我们看看run()是如何支持等待机制的。如果没有任何等待,球会迅速从左向右飞而我们无法察觉。为此,执行Thread中的静态方法sleep。在其参数中指定等待时间(单位毫秒)。(ex. 4)
Thread.sleep(100);//100毫秒待机 |
ex.4
如此例所示,处理和处理之间能够待机100毫秒,按另外观点看,与在100毫秒的定期间隔下运行处理是一样的。定期间隔下的处理,使用名为Timer的类同样能够实现。下面说明一下Timer。
■ 4-1-2 Timer
JAVA提供了计时器功能,它能有规则地、重复地执行或者在指定时间里执行。线程的运算可以是连续的,而不仅仅是按规则的进度。计时器的便利在于系统已经为你准备好特定的方法用于在指定时间或定时执行。
■ 4-1-2-1 时间的使用
Timer类和TimerTask类能够实现计时器的功能。
实例化一个TIMER的类,再调用一个继承了TIMERTASK的类做SCHEDULE。这样,TimerTask子类的run()就会在指定时间或周期被Timer实例呼叫。(表 1)
Timer | 提供计时器功能的类。调用TimerTask子类,用schedule方法设定周期。 |
TimerTask | 抽象类。在Timer类中调用该子类,在指定时间或周期,执行抽象方法的run()方法。 |
Table 1
Timer
在Timer类中主要使用下面的方法:
• void schedule(TimerTask task,Date date)
• void schedule(TimerTask task,Date date,long period)
• void schedule(TimerTask task,long delay)
• void schedule(TimerTask task,long delay,long period)
• void cancel()
这些schedule方法用于设定任务及何时执行。若不设定第三个自变量period,在第二个自变量所指定的时间任务只执行一次。否则第二个自变量所指定的时间之后,任务会反复执行, 自变量period是反复执行时的延时。另外,呼叫出cancel方法,则会结束计时器程序。
TimerTask
TimerTask是抽象类。需要定义其子类。在所继承的类中必须重载抽象方法run(),必须根据计时器的schedule定义具体的操作。
在TimerTask类中主要使用如下的方法。
• abstract void run()• void cancel()
按照Timer 类中所设定的schedule执行run()方法。用cancel()方法能够结束任务。
现在来看看用线程实现的圆球运动的动画--在这里我们也可以使用计时器来实现。如下所示:(ex. 5)
import javax.microedition.lcdui.Display; import javax.microedition.midlet.*; /** * 球飞出去的动画实例计时器版 */ public class TimerMovingBall extends MIDlet { private Display display; private TimerMovingBallCanvas canvas; /** * 构造函数 */ public TimerMovingBall(){ display = Display.getDisplay(this); canvas = new TimerMovingBallCanvas(); } /** * 运行时的处理 */ protected void startApp() throws MIDletStateChangeException { display.setCurrent(canvas); } protected void pauseApp() {} protected void destroyApp(boolean arg0) throws MIDletStateChangeException {} } import java.util.*; import javax.microedition.lcdui.*; /** * 球飞出去的实例动画的canvas。 */ public class TimerMovingBallCanvas extends Canvas { private int x; //球的x坐标 private Image img; private Timer timer; private TimerTask task; /** * 构造函数 */ public TimerMovingBallCanvas() { // 读取画像 try{ img = Image.createImage("/back.PNG"); }catch(Exception e){ e.printStackTrace(); } //Timer,TimerTask的设定 timer = new Timer(); task = new TimerMovingBallTask(this); timer.schedule(task, 100, 100); //从第101毫秒开始在每100毫秒运行任务 } /** * 描绘方法 */ protected void paint(Graphics g) { g.drawImage(img, 0, 0, Graphics.LEFT | Graphics.TOP); //表示球 g.setColor(255, 0, 0); g.drawString(“with Timer”,0,0,Graphics.TOP|Graphics.LEFT); g.fillArc(x, 50, 40, 40, 0, 360); } /** * 能够变化球的X坐标 */ public void increment() { x += 3; } } /** * 计时器任务根据计时器所设定的时间表,运行run()方法。 */ class TimerMovingBallTask extends TimerTask { private TimerMovingBallCanvas canvas; /** * 构造函数 */ public TimerMovingBallTask(TimerMovingBallCanvas canvas) { this.canvas = canvas; } /** * 呼叫计时器时所进行的处理 */ public void run() { canvas.increment(); canvas.repaint(); } } |
ex.5
TimerMovingCanvas类的构造函数中进行下面的计时器设定。
//Timer,TimerTask的设定 timer = new Timer(); task = new TimerMovingBallTask(this); timer.schedule(task, 100, 100); //从第101毫秒开始在每100毫秒运行任务 |
在TimerMovingBallTask中,从TimerTask继承的run()被调用,X坐标的递增使得球运动起来。这样有规律地使X坐标递增,就使得圆球在画面上从左到右移动。(图2)。
public void run() { canvas.increment(); canvas.repaint(); } |
図 2 |
■ 双缓冲
一旦进行动画就要频繁地反复操作描绘处理,画面处理中可能会突然发生屏幕闪烁。为了能够控制屏幕的闪烁,一般会使用双缓冲的手法。用双缓冲存储器并不是直接在画面上进行描绘,而是在被称为屏幕外画像的虚拟画像上描绘全部之后,由于是表示画面,所以能控制屏幕的闪烁。(图 3)
Figure 3 |
根据手机机种的不同,事先将Canvas类与双缓冲存储器相对应的情况也是有的。换言之,即使实现方没有明确地定义双缓冲存储器,则在用双缓冲存储器的机制进行描绘的情况下,没必要特别关心闪烁的解决方案,但是要在不同的机型上运行。N800的机型已经能适应此双缓冲存储器。为此,开发N800应用程序的时候,有时会有必要在这阐述双缓冲存储器的处理。此时,应该知道终端是否适应双缓冲存储器、以及是否有必要分开处理。
使用Canvas类的isDoubleBufferd()方法就能知道是否适应双缓冲存储器。由于使用这种方法可以区分开适应双缓冲存储器的情况和不适应适应的情况,因此无论在什么样的环境下都能够有效控制闪烁现象。示例如下:(ex. 6)
class DoubleBufferdCanvas extends Canvas{ Image offImg = null;//关闭画面的图片 /** * 表示canvas之前所运行的方法 */ protected void showNotify() { if(!isDoubleBuffered()){//关闭画面的图片 offImg = Image.createImage(getWidth(),getHeight()); } } /** * 描绘方法 */ protected void paint(Graphics g) { Graphics bg = null; if(offImg != null){ bg = offImg.getGraphics(); }else{ bg = g; } //使用bg描绘 bg.setColor(255,255,255); bg.fillRect(0,0,getWidth(),getHeight()); bg.setColor(255,0,0); bg.drawArc(x,y,30,30,0,360);//描画圆 //将 off screen image导入画面 if(!isDoubleBuffered()){ g.drawImage(offImg,0,0,Graphics.TOP|Graphics.LEFT); bg = null; } } } |
ex.6
■ 制作应用程序
4-3-1 时钟的应用程序
那么,现在我们就作为示范使用动画制作应用程序吧。在本讲中将要制作的应用程序就是模拟表应用程序。利用秒表,描绘出模拟表。在描绘模拟表针时使用三角函数。类结构表如下:(表 2)
Clock | 继承了MIDlet的类 |
ClockCanvas | 表示时钟的canvas |
ClockTimerTask | 继承了TimerTask并定义秒表任务的类 |
TrigonometricFunctions | 保持三角函数的类 |
Table 2
4-3-2 时间的设定
在模拟表应用程序中秒针一秒动一下。因此,使用秒表,要设定每秒不同时刻的画面更新。在ClockCanvas类的paint方法内取得时刻,描绘钟表。秒表任务如下所示:(ex. 7)
/********************************************** * 秒表任务 **********************************************/ class ClockTimerTask extends TimerTask { private ClockCanvas canvas; /** * 构造函数 */ public ClockTimerTask(ClockCanvas canvas) { this.canvas = canvas; } /** * 在每个指定时间内运行 */ public void run() { canvas.repaint();//再次描绘canvas } } |
ex.7
定义完秒表任务后,就要在秒表上设定秒表任务。用ClockCanvas类的构造函数进行如下设定。(ex. 8)
/** * 构造函数 */ public ClockCanvas() { //设定秒表 timer = new Timer(); timerTask = new ClockTimerTask(this); timer.schedule(timerTask, 1000, 1000);//Once every 1,000 ms } |
ex.8
4-3-3 描画钟表
设定完秒表后开始描绘钟表。下面就是钟表的框架 (clock.png)。(图 4)
Figure 4 |
接下来描画秒针。因为要根据时刻变化秒针的位置,所以有必要根据时刻计算秒针的位置。在这儿用Graphics类的drawLine方法描绘秒针。在表的中心坐标上固定住线的始点,从时刻中计算出线的终点。
使用三角函数计算秒针终点的坐标。表的中心坐标是(x,y),秒针的长度设为1,秒针的角度设为θ,那么终点的坐标就是(x+l*cos(θ), y+l*sin(θ))。(图5)
Figure 5 |
三角函数的实际计算,在J2SE中,Math类有sin,cos方法,所以通常会使用这些方法,而J2ME中是没有这种方法的。因此,在本讲中定义了名为TrigonometricFunctions的类,将预先计算好的sin,cos值扩大10000倍排列并保持在这个类中。所谓扩大10000倍,是由于J2ME不支持double 和float 等小数点型,所以不能原封不动地保存小数点sin,cos 。因此,用MIDP处理小数时,将小数扩大几倍变成整数加以保持,实际上在使用时,用事先乘出的数值再除以所乘的数值,计算并使用由此而得出的实际值。
然而,在J2ME下处理含有小数的数值时,由于要将小数四舍五入成整数所以就会产生误差。例如: 0.12341234…四舍五入成整数就是0,这与原来的0.12341234…是有误差的。由于要尽量减少误差,所以计算之前要尽可能的乘以大数值并且有必要保存其整数。例如:在先前的0.12341234…基础上乘以1000后就变成了123.41234…,小数点以下四舍五入后就是123。将123除以1000后就是0.123。这与开始的数值之间的误差仅为0.00041234…,原封不动的将原来的数值四舍五入后数值0产生的误差是0.00041234…,二者相比则前者的误差要小的多。总之,小数上乘以的数值越大四舍五入后与原来的数值之间的误差就越小。
先讲sin,cos的数值扩大10000倍,秒针坐标计算的最后在除以10000。N800画面尺寸的纵长为180,那么进行 180*x(只有X为小数)计算时的最大误差是
0.0000999. . .*180 = 0.017999. . . is approximately 0.0180总是比1小,没有四舍五入的误差。
接下来描绘秒针。SecondAngle作为秒针的角度, SECOND_LENGTH是秒针的长度,表的中心坐标是(CENTER_X,CENTER_Y)。(ex. 9)
int secondX = CENTER_X + TrigonometricFunctions.COS[secondAngle] * SECOND_LENGTH / 10000; int secondY = CENTER_Y - TrigonometricFunctions.SIN[secondAngle] * SECOND_LENGTH / 10000; g.drawLine(CENTER_X,CENTER_Y,secondX,secondY); |
ex.9
下面是ClockCanvas类的源码。画面上有表的中心坐标。
import java.util.*; |
4-3-4 完成
以下是所制作的程序的源码。
运行结果如下所示: (図 6)。
Figure 6 |
4-3-4 扩展函数
在本讲中制作的模拟表应用程序是实现了表的最小功能的示范应用程序。在本应用程序中,也可以自己增加闹钟和跑秒等扩展功能,这些自己都尝试一番哦。
4-4 总结
在本讲中我们讲述了制作动画应用程序的具体方法。使用动画,就要对画像赋予动感,这比游戏还要复杂,所以应该能够制作图表应用程序。要运行动画,线据和秒表是不可缺少的基本技术,但是线据知识是一门深奥的技术。参照本讲中的样品,加深线据方面的知识,就能够制作成不错的应用程序。在下一讲中,我们将就效果音和BGM、声音等技术进行详细讲解。敬请关注!