第4章 Java对多媒体的支持

作者:顾振宇
您可以自由下载本文,但请保留作者信息。

4.3 动画制作

    初步掌握了在Java中处理各种媒体的基本技能后,我们接下来要涉及的将是Java多媒
体世界中最吸引人,最精彩的一部分--Java动画技术。这一节里,就让我们一起来由
浅入深地制作几个动画实例,并通过这些实例引出一系列措施用以改进动画的显示效果
,直至真正掌握Java动画技术的关键。

4.3.1 一个简单实例

    其实,Java的动画原理也是很简单的,首先在屏幕上显示动画的第一帧(也就是第一
幅画面),然后每隔很短的时间再显示另外一帧,如此往复。由于人眼的视觉暂停而感
觉好象画面中的物体在运动。我们已经掌握了用paint( )方法去显示静态帧的技能,接下
来的问题就是如何不断地替换上其它帧画面。
    我们可以发现,当用paint( )方法在屏幕上画好一帧画面时,再用鼠标拖动这个窗口
,或用其它窗口覆盖它再移开时,这帧画面并未被破坏,而是很快地就被重新画好了。原
来,是系统发现屏幕上该区域的画面已遭破坏,就自动地再一次调用paint( )方法将该画
面恢复。说得更确切一些,系统其实是去调用repaint( )方法来完成重画任务,而repaint
( )方法又去直接调用了update( )方法,update( )方法则先清除整个applet区域里的内
容,然后再调用paint( )方法,从而完成了一次重画工作。
    至此,我们似乎应该可以确定制作动画的基本方案了,那便是在applet开始运行后
(即start( )方法中),每隔一段时间就取调用repaint( )方法来重画某一帧,而paint( )
方法中只需将相应的帧画到屏幕上。这一方案是否正确呢?我们就先来做一个简单实例试
试看吧。
    这个实例很简单,是在applet中显示一行欢迎标题"Welcome to here!"。与以前不同
的是,这行标题并不是一下子显示出来,而是象打字一般,一个个字母跳出来,然后全部
隐去,再重复刚才的打字效果。用动画的术语来说,第一帧显示空白,第二帧显示"W",
第三帧显示"We",直至最后一帧显示完整个字符串后,再循环到第一帧。根据上述提供的
制作方案,我们很快就可以写出下面的程序:

1:	import java.awt.Color;
2:	import java.awt.Font;
3:	import java.awt.Graphics;
4;	
5:	public class RollingMessage extends java.applet.Applet{
6:	
7:		String s = "Welcome to Here !";
8:		int s_length = s.length();					//字符串长度
9:		int x_character = 0;						//显示到第几个字符
10:		Font wordFont=new Font("TimesRoman" , Font.BOLD , 50);
11:	
12:		public void start() {
13:			while(true) {
14:				if (x_character++ > s_length)
15:					x_character = 1;
16:				repaint ();
17:				try {
18:					Thread.sleep(300);			//暂停300毫秒
19:				} catch (InterruptedException e) {}
20:			}
21:		}
22:	
23:		public void paint (Graphics g) {
24:			g.setFont (wordFont);
25:			g.setColor (Color.red);
26;			g.drawString (s.substring(0,x_character), 8, 50);
27:		}
28:	}

    上述程序中的第18行调用了sleep( )方法,它是Thread类中定义的一个类方法(即含
有static关键字的方法),调用它能使正在运行着的程序暂停指定的毫秒数。如果不调用
sleep( )方法,appplet就会全速运行,必将导致动画的换帧速度太快,用户就来不及看
清动画的内容,得到的只有乱闪的画面。因而,动画的制作过程中需要不断地调整每帧
之间的时延数值,使其达到满意的播放速度。程序中的第17行和第19行可能看起来有点
古怪,其实try和catch是为了让我们能完善的处理Java程序运行时产生的错误,也就是
异常处理(详见第7章)。此时我们只需简单地认为,如果当程序正在执行try中的语句
时发生了异常情况,就由catch中的语句来处理。另外,程序中的第26行用到了String类
中的提取子串的substring( )方法,它的第一个参数是子串的起始字符(包括该字符),
第二个参数表示终止字符(不包括该字符)。因而在paint( )方法中每次都根据不同的
x_character值,显示不同长度的字符串。
    如果真的上机去运行上面这段程序的话,你将会感到十分失望,因为屏幕上一片空白
,什么也没有。问题出在哪里呢?原来,我们的程序中调用repaint( )方法时,系统只是
得到一个重画的请求,并不是立即去完成重画动作,而系统只能保证当它有空时,才能
真正去执行repaint( )方法中的代码,即调用update( )和paint( )方法进行真正的重画
工作。而目前的情况是在start( )方法中用一个while无穷循环独占了系统资源,系统就
没有机会去完成重画工作。更为严重的是,该applet还不能正常结束,因为系统同样也没
有机会去调用stop( )方法。那到底应该怎么办呢?看了下一小节,自然就会得到答案。

4.3.2 引入线程机制

    以前的应用程序一般只有一条控制链,从程序初始化开始,然后调用其它方法,进行
数据处理,最后输出结果信息,直至退出操作系统,我们说这种应用程序就是单线程
(single thread)的。而Java中的多线程(multithread)则允许在一个程序里的同一时
刻中,并发运行各自的线程代码,并且各线程间还可以做到不相互影响。因此,如果程序
中发现有可以同时运行的操作时,就可以启动一个新的线程去完成。
    当然,由于机器的硬件资源(如CPU)是固定的,并不会因为程序中采用了多线程而
使运行速度加快,但是给用户的整体感觉会好些。例如,某些集成开发环境软件,在进
行编译过程的同时,还可响应用户的其它操作请求,如继续编辑等,而不必等待漫长的
编译过程完全结束再进行下一步操作。如果程序的执行瓶颈不在CPU的话,采用多线程还
可以提高程序的执行效率,例如通过网络获取多个数据文件,利用多线程比用单线程的
顺序载入要快得多(假设瓶颈也不在网络带宽)。关于多线程的内容在第7章中还会详细
介绍。
    上一小节的例子不能运行的原因就在于整个applet只用了一个线程,所以一旦start()
方法进入死循环后,整个线程就卡在那里。因此,我们可以考虑再产生一个新的线程,
由它来专门执行while循环,定时发出重画请求,而系统就让原来的线程进行paint( )操
作,这样两不相误,动画效果自然也就产生了。
    说实话,在Java中编写多线程applet是非常容易的,下面我们就来一步步的修改刚才
的错误程序:

1. 实现Runnable接口

    在Java中有两种方法可将一个程序变成多线程的程序。第一种是继承Thread类;第
二种就是实现Runnable接口。由于Java不支持多重继承,而我们的applet已经继承了
java.awt.Applet类,所以就不能再去继承Thread类。这时,只有去实现Runnable接口来
实现多线程,因此需将该applet的说明改为:

	public class RollingMessage extends java.applet.Applet implements Runnable {
		. . .
	}

2. 声明一个Thread类型的实例变量

    该实例变量设为Thread类型,用来存放新的线程对象:
	Thread runThread;
    由于Thread类在java.lang程序包中,因而可以不用在程序头部指明import这个类。

3. 覆盖start( )方法

    在start( )方法中只需做一件事,那就是生成一个新线程并启动这个线程:
	public void start() {
		if(runThread==null){
			runThread = new Thread(this);
			runThread.start();
		}
	}

    这里用到了Thread类的构造方法,它的调用格式为:
	Thread(Runnable target)
    由于实现Runnable接口的正是RollingMessage类,因此target的参数值就设为this,
即本对象。生成浏览一个Thread对象后,只要调用Thread类中的start( )方法,就启动
了该线程。具体的说,Thread类中的start( )方法实际上是去调用Runnable接口中定义
的run( )方法,从而完成了启动新线程的任务,同时立刻又将程序的控制权交回原来的
线程。因而,可以这样说,当runThread.start();这个语句执行完后,该applet就有了两
个线程,一个运行原来applet中本身的代码,另一个运行下面就要讲到的run( )方法中
的代码。

4. 实现run( )方法


    既然我们把start( )方法中的代码改为只生成并启动一个新的线程,那么我们原来
的applet中start( )方法里面的代码都放到哪里去了呢?那就是放在一个新的方法run()
中,事实上这也是Runnable接口中唯一定义的方法。而run( )方法中的代码可以是applet
中任何想分配给另一线程所做的工作。在本例中,就是把原来start( )方法中的主循环
代码,全部放入了run( )方法里,因此也可以说run( )方法中的代码才是这个applet真
正的核心:

	public void run() {
		while(true) {
			if (x_character++>s_length)
				x_character = 0;
			repaint ();
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {}
		}
	}

5. 覆盖stop( )方法

    既然在applet的start( )方法中生成并启动了一个新的线程,相应地,我们也应该在
applet被挂起时,停止这一线程的运行:

	public void stop() {
		if(runThread!=null){
			runThread.stop();
			runThread=null;
		}
	}

    这里调用了Thread对象的stop( )方法,就停止了该线程的运行,紧接着下一行就将
这个Thread对象设为null,这是为了让系统把这个无用的Thread当作垃圾收集掉,释放
内存。一旦用户重新回到该Web页面,applet又会在start( )方法中重新产生新的线程并
启动它。
    下面就是这一例子改正后的正确代码,其显示效果如图4-15所示。

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;

public class RollingMessage extends java.applet.Applet implements Runnable {
	Thread runThread;
	String s = "Welcome to here !";
	int s_length = s.length();
	int x_character = 0;
	Font wordFont=new Font("TimesRoman" , Font.BOLD , 50);

	public void start() {
		if(runThread==null){
			runThread = new Thread(this);
			runThread.start();
		}
	}

	public void stop() {
		if(runThread!=null){
			runThread.stop();
			runThread=null;
		}
	}

	public void run() {
		while(true) {
			if (x_character++>s_length)
				x_character = 0;
			repaint ();
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {}
		}
	}

	public void paint (Graphics g) {
		g.setFont (wordFont);
		g.setColor (Color.red);
		g.drawString (s.substring(0,x_character), 8, 50);
	}
}

图4-15 简单动画例子的执行结果

4.3.3 初识闪烁问题

    虽然,我们刚才的applet已经能动起来,可是挑剔的用户马上就会发现屏幕上的动
画会一闪一闪的,如果你的机器速度较慢的话,这一现象就更明显。确实,有效的解决
闪烁问题一直是动画制作的关键技术,因为没有人会愿意观看刺眼的一闪一闪的画面。
    既然想要解决这一问题,那就先要找到产生闪烁现象的原因在哪里。还记不记得动
画过程是如何工作的:applet调用repaint( )方法通知系统进行重画,repaint( )方法
实际调用了update( )方法先清洗整个applet区域,然后再调用paint( )方法绘制屏幕。
很明显,问题就出在update( )方法里,它每次清除屏幕,使画面每次都从有内容,到
全空白,再画上内容,无疑造成了闪烁。下面就是update( )方法的缺省代码:

	public void update(Graphics g){
		g.setColor(getBackground());		//将背景色置为当前绘图颜色
		g.fillRect(0,0,width,height);		//用背景色填充整个applet区域
		g.setColor(getForeground());		//将当前绘图颜色设回前景色
		paint(g);						//进行重画
	}

其中width和height是指整个applet的高度和宽度。
    很显然,我们必须覆盖这一方法,去改变它每次都呆板的用背景色去填充一下整个
applet区域。那我们是否真的每次都需清除屏幕吗?回想一下,我们的applet每一次重
画的内容都比前一次多一个字符。所以,如果前一个画面不清除,后一个画面叠加上去
并不会破坏整个画面的内容,只有在全部字符都显示完全,才需要真正的清洗一下屏幕
。因此,只在真正需要清除屏幕的时候才去做清除的动作,这应该说是消除闪烁的一个
原则。下面,就是我们覆盖缺省的update( )方法后的新代码:

	public void update(Graphics g){
		if (x_character= =0){
		g.setColor(getBackground());
		g.fillRect(0,0,appletWidth,appletHeight);
		g.setColor(getForeground());
		}
		paint(g);
	}

另外有一点要改进的就是,为了能增加applet的灵活性和实用性,我们要尽量从HTML文件
给applet传递各种配置参数,如要显示的字符串、字体、颜色、尺寸等信息,这样就不必
每次小的改动都需要重新编译applet源代码。
下面就是本实例经改进后的程序代码:

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;

public class RollingMessage extends java.applet.Applet implements Runnable {
	Thread runThread;
	String s;			// 要显示的字符串
	int s_length;		// 字符串的长度
	int x_character=0,	// 当前显示到第几个字符串
		y_coord,		// 字符串的Y坐标位置
		textcolor,		// 字符串的颜色值(16进制整数rrggbb)
		backcolor,	// applet背景颜色值(16进制整数rrggbb)
		delay;		//每帧画面的时延(毫秒)
	int appletWidth, appletHeight;
	String font_name;	// 字体的名称
	int font_size;		// 字体的尺寸
	Font wordFont;
	FontMetrics wordMetrics;

	public void init() {
		String temp;
		appletWidth = size().width;
		appletHeight = size().height;

		temp=getParameter("font");
		font_name= (temp= =null) ? "TimesRoman" : temp;
		temp = getParameter("fontsize");
		font_size= (temp= =null) ? 12 : Integer.parseInt( temp );		//转换为10进制整数
		wordFont = new Font(font_name, Font.PLAIN, font_size);
		if (wordFont = = null)
			wordFont = getFont();
		wordMetrics = getFontMetrics (wordFont);
		temp = getParameter("text");
		s= (temp= =null) ? "Message goes here...   " : temp;
		s_length=s.length();
		temp = getParameter("textcolor");
		textcolor= (temp= =null) ? 0 : Integer.parseInt( temp ,16 );	//转换为16进制整数
		temp = getParameter("backcolor");
		backcolor= (temp= =null) ? 0xffffff : Integer.parseInt( temp ,16 );
		temp = getParameter("delay");
		delay= (temp= =null) ? 100 : Integer.parseInt( temp );
		y_coord = appletHeight/2 + (wordMetrics.getHeight()-wordMetrics.getDescent())/2;
	}

	public void start() {
		if(runThread==null){
			runThread = new Thread(this);
			runThread.start();
		}
	}

	public void stop() {
		if(runThread!=null){
			runThread.stop();
			runThread=null;
		}
	}


	public void run() {
		while(true) {
			if (x_character++>s_length)
				x_character = 0;
			repaint ();
			try {
				Thread.sleep( delay );
			} catch (InterruptedException e) {}
		}
	}

	public void paint (Graphics g) {
		g.setFont (wordFont);
		g.setColor (Color.red);
		g.drawString (s.substring(0,x_character), 8, y_coord);
	}

	public void update(Graphics g){
		if (x_character = = 0){
		g.setColor(getBackground());
		g.fillRect(0,0,appletWidth,appletHeight);
		g.setColor(getForeground());
		}
		paint(g);
	}
}

下面是测试这一applet的HTML语言的例子:

<applet code=RollingMessage.class width=400 height=60>
<param name=font value="TimesRoman">
<param name=fontsize value=30>
<param name=text value="Welcome to here !">
<param name=textcolor value="ff0000">
<param name=backcolor value="ffffff">
<param name=delay value=300>
<img src="java/rollingmessage.gif" WIDTH=251 HEIGHT=24 BORDER=0>
<h5>You need Java to use the RollingMessage applet!
</applet>

4.3.4 放映图像

    有些人可能会觉得真正的动画应该是每一帧都有自己的图像,而不是仅仅跳出几个字
符。那好吧,我们下面就来制作一个放映图像的动画例子,那就是一个会走的数码钟,
其显示效果如图4-16所示。

图4-16 会走的数码钟
    在放映图像前我们自然应该先准备好各个图像文件,本例的applet所需要的图像清单
是:'0'到'9'共十幅数码管图像(文件名为lcd0.gif、lcd1.gif、...、lcd9.gif),一
个冒号图像(colon.gif文件),一个钟的边框(frame.gif文件)。这些清单如图4-17
所示。

图4-17 数码钟所用的各图像文件

    然后我们把这些图像都放在本applet目录下的dcimages目录下,由于我们把数码管图
像的文件名取得相似,因而可以创建一个Image数组digit_image作为实例变量,并在init()
方法中通过一个循环,将这十幅图像装载进来,如:

	Image[]	digit_image = new Image[10];
	public void init()
	{
		for (int i = 0; i <10; i++) { digit_image[i]="getImage(getCodeBase()," "dcimages/lcd" + i + ".gif"); . . . } 同样,用getImage( )方法装载其它两幅图像。下一步的工作就是要确定数码钟里每 幅图显示的起始位置,这里主要是指X方向的坐标值(因为Y方向的坐标值是一样的)。 我们把这些信息存放在一个整型数组(image_start_x)中,其中每个值的含义依次表示 下列位置:小时数的十位数码、小时数的个位数码、第一个冒号、分钟数的十位数码、 分钟数的个位数码、第二个冒号、秒钟数的十位数码、秒钟数的个十位数码。 然后同样采用多线程机制,在run( )方法中控制定时,一秒钟重画一次。而在paint() 方法中则先获取当前的时间,然后用drawImage( )方法在相应的位置画出边框、相应的 六个数码管图像及两个冒号。 接下来的问题就是如何得到当前的时间呢?这就要用到java.util程序包里的Data类 (别忘了import它哟)。这个类的构造方法中有一种不带任何参数的调用格式,它就会创 建一个表示当前日期和时间的对象,得到这一对象后就可以调用Data类中提供的getHours() 方法、getMinutes( )方法与getSeconds( )方法来获取当前时刻的小时数、分钟数和秒数。 例如: Date now="new" Date( ); int hour="now.getHours(" ); int minute="now.getMinutes(" ); int second="now.getSeconds(" ); 另外,我们为了将该applet的宽度和高度就设置为数码钟边框所围的区域,就调用了 Applet类中的resize( )方法,该方法传递两个整型参数,分别将该applet重新设置为所 指定的高度和宽度。调用这一方法将覆盖HTML文件中"applet"标记的width属性和height 属性的值,这样就可以防止用户对这两个属性值的错误设置。 最后,我们要注意的问题自然又是关于闪烁。由于我们每次都在一系列固定的位置上 放置大小相同的图片,因而根本不需每次用背景色清除画面。所以update( )方法只要简 单地调用paint( )方法就可以了: public void update(Graphics g) { paint(g); } 好了,下面我们就列出该数码钟的全部代码: import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.util.Date; public class DigClk extends java.applet.Applet implements Runnable { Thread timer="null;" Image[] digit_image="new" Image[10]; // 数码(0-9)图像数组 Image colon_image, // 冒号图像 frame_image; // 边框图像 int digit_height="21;" // 数码(及冒号)高度 int digit_width="16;" // 数码宽度 int colon_width="9;" // 冒号宽度 int offset="4;" // 边框厚度 int applet_width; int applet_height; int[] image_start_x="new" int[8]; // 数码或冒号的水平起始位置数组 public void init() { for (int i="0;" i < 10; i++){ digit_image[i]="getImage(getCodeBase()," "dcimages/lcd" + i + ".gif"); } colon_image="getImage(getCodeBase()," "dcimages/colon.gif"); frame_image="getImage(getCodeBase()," "dcimages/frame.gif"); applet_width="(2" * offset) + (6 * digit_width) + (2 * colon_width);//计算applet宽度 applet_height="(2" * offset) + (digit_height); //计算applet高度 image_start_x[0]="offset;" // 填充起始位置数组 for (int i="1;" i < 8; i++){ if ((i="=" 3) || (i="=" 6)) // 下一位置是冒号 image_start_x[i]="image_start_x[i" 1] + colon_width; else // 下一位置是数码 image_start_x[i]="image_start_x[i" 1] + digit_width; } } public void start() { if (timer="=" null){ timer="new" Thread(this); timer.start(); } } public void run() { while (timer !="null){" try{ timer.sleep(1000); //1秒延时 }catch (InterruptedException e){ } repaint(); } } public void stop() { if (timer !="null){" timer.stop(); timer="null;" } } public void paint(Graphics g) { Date now="new" Date(); // 获取当前日期和时间的对象 int hour="now.getHours();" // 取小时数 int minute="now.getMinutes();" // 取分钟数 int second="now.getSeconds();" // 取秒钟数 int i="0;" // 水平起始位置数组的索引 g.drawImage(frame_image, 0, 0, this); g.drawImage(digit_image[hour / 10], image_start_x[i++], offset, this); g.drawImage(digit_image[hour % 10], image_start_x[i++], offset, this); g.drawImage(colon_image, image_start_x[i++], offset, this); g.drawImage(digit_image[minute / 10], image_start_x[i++], offset, this); g.drawImage(digit_image[minute % 10], image_start_x[i++], offset, this); g.drawImage(colon_image, image_start_x[i++], offset, this); g.drawImage(digit_image[second / 10], image_start_x[i++], offset, this); g.drawImage(digit_image[second % 10], image_start_x[i], offset, this); } public void update(Graphics g) { paint(g); } } 

4.3.5 使用媒体跟踪器

    如果我们真的上机运行上面的数码钟实例时,准会被动画刚开始运行时的画面乱动
现象吓一跳:各图片并非一下子显示出来,而都从顶部开始参差不齐地慢慢显示,同时
整个画面都伴随着强烈的闪烁,如图4-18所示。直至画面全部显示完整后,才恢复正常。

图4-18 数码钟刚开始运行时的画面乱动现象
    这是为什么呢?显然这是因为图像数据尚未完全准备好就迫不急待地开始显示了,当
然在开始时只能显示一小半画面了。那我们在init( )方法里不是用getImage( )方法把所
有图像都装载进来了吗?原来,getImage( )方法被调用时,仅仅是立刻生成一个Image对
象返回给调用者,但这并不表示图像文件已经被装载到内存中,而是与此同时,系统马上
产生了另一个线程去真正读取该图像文件的数据。因而,往往程序已经运行到getImage( )
后面的语句时,系统却还正在装载图像文件的数据,尤其是从网络上装载图像文件时,这
种情况会更加严重。那么怎样才能让图像文件的数据都到齐了才播映呢?
    Java提供了一个java.awt.MediaTracker类,可用它来跟踪媒体装载的情况,其构造
方法的调用格式为:

		MediaTracker(Component comp)

    它需要一个Component对象作为参数,来表明此媒体跟踪器是为谁服务的,我们就自
然用this作为参数值(因为我们的applet正是java.awt.Component类中的一个子类)。一
旦创建了一个MediaTracker对象后,我们就可以调用它的addImage( )方法将某Image对象
列入跟踪监控的范围,其调用格式如下:

	void addImage(Image image, int id) 

    这里的第一个参数表示需监控的Image对象,第二个参数则表示这个Image对象所属的
被监控组的编号。将该Image对象列入跟踪范围后,我们就可以在开始放映动画前调用Media
Tracker类提供的waitForID( )方法来等待图像数据的全部到达。该方法的调用格式如下:

	void waitForID(int id)

    这里的id参数就是需等待的图像组的编号,一旦调用了该方法,系统就会一直在这
里等待,直到该组中所有图像文件的数据全部装载好以后,才会去执行下一条语句。
好了,下面我们就来一步一步地修改数据钟的代码,使其加入媒体跟踪器的功能:

1. 程序头部加入:

	import java.awt.MediaTracker;

2. 在applet中加入一个实例变量:

	MediaTracker tracker;

3. 将init( )方法修改为:

	public void init()
	{
		tracker = new MediaTracker(this);				//创建媒体跟踪器
		for (int i = 0; i <10; i++){ digit_image[i]="getImage(getCodeBase()," "dcimages/lcd" + i + ".gif"); tracker.addImage(digit_image[i], 0); //将该图像列入第0组跟踪范围 } colon_image="getImage(getCodeBase()," "dcimages/colon.gif"); tracker.addImage(colon_image, 0); frame_image="getImage(getCodeBase()," "dcimages/frame.gif"); tracker.addImage(frame_image, 0); applet_width="(2" * offset) + (6 * digit_width) + (2 * colon_width);//计算applet宽度 applet_height="(2" * offset) + (digit_height); //计算applet高度 image_start_x[0]="offset;" // 填充起始位置数组 for (int i="1;" i < 8; i++){ if ((i="=" 3) || (i="=" 6)) // 下一位置是冒号 image_start_x[i]="image_start_x[i" 1] + colon_width; else // 下一位置是数码 image_start_x[i]="image_start_x[i" 1] + digit_width; } } 4. 将run( )方法修改为: public void run() { try{ tracker.waitForID(0); //等待0组图像媒体数据的到达 }catch(InterruptedException e){ return; //若发生异常就马上返回 } while (timer !="null){" try{ timer.sleep(1000); //1秒延时 }catch (InterruptedException e){ } repaint(); } } 

4.3.6 移动型动画

    在这一小节中我们来尝试另一种类型的动画,那就是动画中的物体在进行各种动作
同时还不断地改变自身所处的位置,也就是所谓的移动型动画。

图4-19 鸵鸟走路姿势的图片
    例如,我们接下来要做的一个实例是一只快乐的鸵鸟在美丽的大草原上散步,它循环
不断的从屏幕左端走入,再从屏幕的右端走出。这里我们先准备了五幅鸵鸟走路姿势的
图片,如图4-19所示。另外,还有一张草原背景图片,如图4-20所示。

图4-20 草原背景图
    其实,移动型动画的制作也很简单,只不过除了需要知道当前采用哪一幅动作图像
以外,还需要给出物体当前应处的位置。例如,我们把所有的鸵鸟动作图像都存放在一
个Image数组walkerImgs中,还用了一个整型实例变量xpos来记住鸵鸟当前水平方向的X
坐标值,而当前所选的动作图像则由Image类的对象currentImg来指示。并且,xpos与
currentImg的值均在run( )方法中事先计算好,如:

		for(xpos= - birdImg_width;xpos<=applet_width;xpos+=walk_step){ //计算位置 currentImg="walkerImgs[i];" repaint(); i="(i+1)%walkerImgs.length;" //计算下一帧是哪幅图像 try{Thread.sleep(delay);} catch(InterruptedException e){} } 这里xpos的初值不取0而取为负的鸵鸟图宽度值birdImg_width,只是为了让鸵鸟能 从屏幕左侧自然的切入,不造成整幅图像突然出现在屏幕左侧的感觉。walk_step实例变 量表示图像每次移动的距离,delay实例变量用来控制走路节奏的快慢。 而在paint( )方法中我们必须先画上草原背景图像,然后再把鸵鸟图像覆盖上去。由 于每一次鸵鸟图像放置的位置都不相同,也就等于每次都把背景图像给破坏了,所以背 景图像也必须每次都重画。因而paint( )方法只需写为: public void paint(Graphics g){ g.drawImage(bgImage,0,0,this); g.drawImage(currentImg,xpos,ypos,this); } 这里还有一个小小的问题,那就是一般图像都是以矩形的状态展现在我们面前,如 果我们直接将鸵鸟图像贴在草原背景上,如图4-21所示,那鸵鸟的白色矩形背景与草原 背景相互冲突,完全破坏了整个动画的意境。 那有什么办法能将这块白色背景去掉,只留下一个活生生的鸵鸟图案呢?幸好GIF图 像格式的89a版本支持一种透明背景技术,它能清除整幅图像的矩形背景,使其变为透明 背景。因此必须先将上述的鸵鸟图片都转换为透明背景图,然后再贴到草原背景上,那 动画效果就显得天衣无缝了,如图4-22所示。能制作透明背景图的软件有GIFTRANS及 GIF CONSTRUCTION SET等。 

图4-21 鸵鸟图像的白色背景与草原背景图相互冲突

图4-22 将鸵鸟图像制成透明背景图
下面,我们就列出本动画实例的全部代码:

import java.awt.MediaTracker;
import java.awt.Graphics;
import java.awt.Image;

public class Walker extends java.applet.Applet implements Runnable{
	MediaTracker tracker;
	Image walkerImgs[]=new Image[5];		//存放鸵鸟走路姿势图像
	Image currentImg;						//当前放映的鸵鸟动作图像
	int xpos,ypos=0;						//鸵鸟动作图像显示的位置
	int walk_step=20;						//鸵鸟图像每次移动的距离
	int delay = 250;						//每帧时延(毫秒)
	Thread runThread;
	Image bgImage;						//存放草原背景图像
	int applet_width,applet_height;
	int birdImg_width;						//鸵鸟图像宽度

	public void init(){
		tracker = new MediaTracker(this);
		for (int i=0;i 

xiaoyu

walkerImgs.length;i++){					//获取鸵鸟动作图像
			walkerImgs[i]=getImage(getCodeBase(),"images/bird"+i+".gif");
			tracker.addImage(walkerImgs[i], 0);				//列入0组跟踪范围
		}
		bgImage = getImage(getCodeBase(),"images/"+"bg.gif");	//获取草原背景图像
		tracker.addImage(bgImage, 0);						//列入0组跟踪范围
		applet_width=size().width;
		applet_height=size().height;
	}

	public void start (){
		if (runThread==null){
		runThread=new Thread(this);
		runThread.start();
		}
	}

	public void stop(){
		if (runThread!=null){
			runThread.stop();
			runThread=null;
		}
	}

	public void run(){
		try{
			tracker.waitForID(0);
		}catch(InterruptedException e){
			return;
		}
		birdImg_width=walkerImgs[0].getWidth(this);
		int i=0;
		while(true){
			for(xpos=-birdImg_width;xpos<=applet_width;xpos+=walk_step){//计算位置 currentImg="walkerImgs[i];" repaint(); i="(i+1)%walkerImgs.length;" //计算下一帧是哪幅图像 try{Thread.sleep(delay);} catch(InterruptedException e){} } } } public void paint(Graphics g){ g.drawImage(bgImage,0,0,this); g.drawImage(currentImg,xpos,ypos,this); } public void update(Graphics g){ paint(g); } } 

4.3.7 双缓冲技术

 
   虽然我们在上一例鸵鸟动画中覆盖了update( )方法,阻止系统每次都用背景色去清洗
屏幕这一无用的操作,但是动画效果还是不尽人意:闪烁得厉害。那既然采用了同样的
方法为什么数码钟闪烁的情况要好得多呢?关键在于鸵鸟动画里的图像文件都较大,系
统每次在屏幕上画起来速度就慢,闪烁当然厉害了。
    这一节中,我们介绍一种消除闪烁的最佳法宝--双缓冲(double-buffering)技术
。说它最佳,是因为利用此技术制作出来的动画能最大限度地避免闪烁现象。但这种技
术的缺点就是需要占用大量的内存。因此只有当你的动画实在为严重的闪烁问题所困扰
时,才应考虑使用这种技术。
    由于我们已经发现,闪烁问题的根源就在于系统每次直接在屏幕上作画的速度太慢
(尤其是要画的图像又大又多时),因此双缓冲技术就考虑在屏幕外面创建一个虚拟的
备用屏幕,系统直接在备用屏幕上作画,等画完以后就将备用屏幕中的点阵内容直接切
换给当前屏幕。这种直接切换准备好的画面页的速度与在屏幕上当场一一作画的速度相
比,当然快了许多。

下面,我们就来一步一步地修改上面的鸵鸟动画代码,使其实现双缓冲技术。

1. 首先,我们在applet中声明两个实例变量:
	Image offScreenImg;			//存放备用屏幕的内容
	Graphics offScreenG;			//备用屏幕的绘图上下文环境

2. 在init( )方法中加入下列语句:
	try {
		offScreenImg = createImage (applet_width,applet_height);//创建备用屏幕
		offScreenG = offScreenImg.getGraphics ();	//获取备用屏幕的绘图上下文环境
	} 
	catch (Exception e) {
		offScreenG = null;		//若出错,就置备用屏幕的绘图上下文环境为null
	}
其中createImage方法是定义在java.awt.Compont类中的方法,由于我们的applet是它的
子类,从而就自然地继承了该方法。此方法是用来创建一个空的可以在上面进行绘图的
Image对象,它的两个整型参数分别表示所创建的该Image对象的宽度和高度,我们这里
就设为整个applet屏幕的宽和高,以便将它作为一个备用屏幕。紧接着,调用Image对象
中的getGraphics( )方法,它是用来获取一个可以在该Image对象上进行绘图的绘图上下
文环境,也就是一个Graphics对象offScreenG,以后凡是调用offScreenG中的任何绘图
方法(如drawImage( )方法)都将作用在备用屏幕(offScreenImg)上。如果上述执行
过程中发生错误,我们就将这个Graphics对象设为null值。

3. 将update( )方法改为:

	public void update(Graphics g)
	{
		if (offScreenG!=null) {					//如果备用屏幕创建成功
			paint(offScreenG);
			g.drawImage(offScreenImg,0,0,this);	//将备用屏幕内容画到当前屏幕来
		} else
			paint(g);
	}

    即如果备用屏幕创建成功的话,就将备用屏幕的绘图上下文环境offScreenG传递给
paint( )方法,这样一来,paint( )方法中所画的内容都将画在备用屏幕上。然后再调用
drawImage( )方法将备用屏幕offScreenImg中的内容画到当前屏幕上。当然,如果创建
备用屏幕不成功的话,就和以前一样,将系统生成的当前屏幕的绘图上下文环境g传递给
paint( )方法。
    好了,实现双缓冲技术就是这样地简单。我们下面就列出改进后的鸵鸟动画程序的
原代码。另外,我们还在其中加入了一段播放背景音乐的代码,以使这个动画现得更加
有声有色。

import java.awt.MediaTracker;
import java.awt.Graphics;
import java.awt.Image;
import java.applet.AudioClip;

public class Walker extends java.applet.Applet implements Runnable{
	Image offScreenImg;					//存放备用屏幕的内容
	Graphics offScreenG;					//备用屏幕的绘图上下文环境
	MediaTracker tracker;					//媒体跟踪器
	Image walkerImgs[]=new Image[5];		//存放鸵鸟走路姿势图像
	Image currentImg;						//当前放映的鸵鸟动作图像
	int xpos,ypos=0;						//鸵鸟动作图像显示的位置
	int walk_step=20;						//鸵鸟图像每次移动的距离
	int delay = 250;						//每帧时延(毫秒)
	Thread runThread;
	Image bgImage;						//存放草原背景图像
	int applet_width,applet_height;
	int birdImg_width;						//鸵鸟图像宽度
	AudioClip bgmusic;

	public void init(){
		tracker = new MediaTracker(this);
		for (int i=0;i 

Xiaoyu

walkerImgs.length;i++){					//获取鸵鸟动作图像
			walkerImgs[i]=getImage(getCodeBase(),"images/bird"+i+".gif");
			tracker.addImage(walkerImgs[i], 0);				//列入0组跟踪范围
		}
		bgImage = getImage(getCodeBase(),"images/"+"bg.gif");	//获取草原背景图像
		tracker.addImage(bgImage, 0);						//列入0组跟踪范围
		bgmusic=getAudioClip(getDocumentBase(),"space.au");	//获取背景音乐
		applet_width=size().width;
		applet_height=size().height;
		try {
			offScreenImg = createImage (applet_width,applet_height);//创建备用屏幕
			offScreenG = offScreenImg.getGraphics ();//获取备用屏幕的绘图上下文环境
		}
		catch (Exception e) {
			offScreenG = null;		//若出错,就置备用屏幕的绘图上下文环境为null
		}
	}

	public void start (){
		if (runThread==null){
			runThread=new Thread(this);
			runThread.start();
		}
	}

	public void stop(){
		if (runThread!=null){
			if(bgmusic!=null)
				bgmusic.stop();
			runThread.stop();
			runThread=null;
		}
	}

	public void run(){
		try{
			tracker.waitForID(0);			//等待0组中所有图像的到达
		}catch(InterruptedException e){
			return;
		}
		if(bgmusic!=null)
			bgmusic.loop();
		birdImg_width=walkerImgs[0].getWidth(this);
		int i=0;
		while(true){
			for(xpos=-birdImg_width;xpos<=applet_width;xpos+=walk_step){//计算位置 currentImg="walkerImgs[i];" repaint(); i="(i+1)%walkerImgs.length;" //计算下一帧是哪幅图像 try{Thread.sleep(delay);} catch(InterruptedException e){} } } } public void paint(Graphics g){ g.drawImage(bgImage,0,0,this); g.drawImage(currentImg,xpos,ypos,this); } public void update(Graphics g){ if (offScreenG!="null)" { //如果备用屏幕创建成功 paint(offScreenG); g.drawImage(offScreenImg,0,0,this); //将备用屏幕内容画到当前屏幕来 } else paint(g); } } 

练习题

1. 分别用drawOval( )、drawArc( )和drawRoundRect( )方法画一个半径为50的圆。
2. 怎样才能获取客户端系统所支持的字体类型清单?
3. 在编写循环播放背景音乐的applet中,特别要注意哪个问题?为什么?
4. 为什么要采用多线程机制来制作Java动画?简述制作Java动画的基本步骤。
5. 在何种情况下需要使用媒体跟踪器?为什么?
6. 简述双缓冲技术的基本原理。
7. 用双缓冲技术改写4.3.5小节中的数码钟程序,使其能更好地消除闪烁现象。