java指南之使用图形:动画
2008-01-05 09:20:46 来源:WEB开发网核心提示:执行动画 所有形式的动画的一个共通点就是它们通过以相对而言较快的速度显示连续的画面创造某种能被觉察的运动效果,计算机动画通常每秒显示10-20帧,java指南之使用图形:动画,比较而言,传统的手工绘制的动画每秒为8帧(低质的动画)到12帧(标准动画)以至到24帧(平滑效果,//imageNumber 是要绘制的图像的序
执行动画
所有形式的动画的一个共通点就是它们通过以相对而言较快的速度显示连续的画面创造某种能被觉察的运动效果。计算机动画通常每秒显示10-20帧。比较而言,传统的手工绘制的动画每秒为8帧(低质的动画)到12帧(标准动画)以至到24帧(平滑效果,现实中的运动效果)。下面的几页将告诉你在java中执行动画所需要知道的所有知识。
--------------------------------------------------------------------------------
开始前: 检查是否有动画工具和诸如 Animator这样的applet,看看你是否可以用它们来代替自己写程序。
--------------------------------------------------------------------------------
使用Timer创建循环的动画
创建一个动画程序的最重要的一步是正确的制订计划。除了动画仅仅直接反应外部事件(例如用户拖动屏幕上的对象)外,程序需要一个动画循环。
动画循环有责任跟踪当前帧并且周期性的请求屏幕更新。对于applet和很多应用程序,你需要一个单独的线程运行动画循环。这一节包含一个范例applet和一个范例程序,它们都使用Timer 对象实现动画循环。你可以将这些例子作为你自己的动画的模板。
在屏幕上移动图像
最简单的动画形式是在屏幕上移动一个固定的图像。在传统的动画世界中,这被称为剪纸动画(cutout animation),因为它通常是通过剪切外形并在镜头前移动达到的。
显示一列图像
这一节告诉你如何执行经典的卡通式的动画:显示一列图像。
增强外观和提高动画性能。
这一节告诉你如何使用MediaTracker,它可以使你直到动画的所有的图像都被加载后才显示动画。你也会得到一些通过合并图像文件和使用压缩方案的方法提高动画性能的提示。
使用Timer创建循环的动画
通过以规则的间隔执行动画的每个程序需要一个动画循环。通常,这个循环应该在自己的线程中。它永远不应该在 paintComponent 方法中,因为它将接管事件分派线程,该线程掌管绘图和事件处理。
Timer类使得实现一个动画循环很轻易。这一节两个基于Timer的动画模板,一个用于applet,另一个用于应用程序。applet版本的照片在下面在运行该applet时,你可以单击它停止动画,再次单击可以让它继续。
这个图片是该applet的GUI。要运行那个applet,单击图片。该applet将在一个新浏览窗口显示。
这个模板动画的行为有点让人厌烦:它简单的显示当前的帧数,使用缺省的每秒10帧的速度。下面的几节建立这个范例,向你展示如何制作图像动画。
你可以从 AnimatorAppletTimer.java得到applet版本模板的代码。应用程序版本模板的代码在 AnimatorapplicationTimer.java中。这一节的余下的部分解释模板中共同的代码.下面是两个模板所做的事情的概览:
public class AnimatorClass ... implements ActionListener {
int frameNumber = -1;
Timer timer;
boolean frozen = false;
JLabel label;
//初始化:
//从用户指定的帧/秒值决定帧间的间隔时间。
...
//设置一个定时器调用这个对象的事件处理器。
timer = new Timer(delay, this);
...
//设置GUI的组件。
public synchronized void startAnimation() {
...
timer.start();
...
}
public synchronized void stopAnimation() {
...
timer.stop();
...
}
public void actionPerformed(ActionEvent e) {
//增加动画帧。
frameNumber++;
//请求重绘帧
label.setText("Frame " + frameNumber);
}
...
//当程序的GUI显示时:
startAnimation();
...
}
初始化实例变量
模板使用了四个实例变量。第一个 (frameNumber)表示当前帧。它被初始化为-1,即使是第一帧数是0。原因是帧数在动画循环的开始就被增加,这个时间先于第一帧被绘制的时间。因此,第一次被绘制的帧数是0。
第二个实例变量(timer)是一个Timer对象,它实现动画循环。它被初始化为每隔delay毫妙执行一次事件。
变量delay是一个局部变量,由用户提供的帧/秒 数初始化。下面的代码将该值转换为帧间的毫秒数:
delay = (fps > 0) ? (1000 / fps) : 100;
前面代码中的? : 符号是 if else 的简短形式。假如用户提供一个大于0的帧/秒值,那么间隔就是1000毫秒除以该值。否则间隔就是100毫秒(10帧/秒)。
第三个实例变量是 (frozen)是一个boolean 值,它被初始化为false。当用户请求停止动画,模板将它设置为 true 。在这一节的后面你会看到有关它的更多信息。
第四个实例变量(label)是执行绘图的组件的一个引用。
动画循环
Timer 对象通过每delay毫秒触发一次事件实现动画循环。对应于每次事件,actionPerformed 方法执行下面的功能:
增加帧数
请求绘制当前帧。
要获得更多有关定时器的信息,请参考 如何使用定时器。
礼貌行为
动画模板有两个出于礼貌的特性。
第一个是运行用户明确的停止(和重新启动)动画,同时applet或者程序维持可见。动画可能会使人心烦意乱,因此给予用户停止动画而将注重力集中在其它的事情上的能力是个不错的主意。这个特性是通过重写 mousePRessed 方法实现的,它根据程序的当前状态决定是停止还是重新启动定时器。下面是实现的代码:
...//初始化代码中:
boolean frozen = false;
...
public synchronized void startAnimation() {
if (frozen) {
//什么也不做。用户请求停止图像变化。
} else {
//开始动画!
...
timer.start();
...
}
public synchronized void stopAnimation() {
...
timer.stop();
...
}
...
//动画组件的鼠标监听器中:
public void mousePressed(MouseEvent e) {
if (frozen) {
frozen = false;
startAnimation();
} else {
frozen = true;
stopAnimation();
}
}
第二个特性是在applet或者程序不可见的时候挂起动画。对于 applet模板,这是通过在stop 和 start 方法中分别调用stopAnimation 和startAnimation方法达到的。对于程序模板,这是通过实现窗口事件处理器达到的,该处理器重定义最小化和恢复,在其中再次分别调用stopAnimation 和startAnimation 。
在两个模板中,假如用户没有停止动画,那么当程序检测到动画不可见时,它通知定时器停止。当用户再次访问动画,程序重新启动动画,除非用户明确请求停止动画。
Moving an Image Across the Screen
This page features an example applet that moves one image (a rocketship) in front of a background image (a field of stars). You could implement this in one of two ways -- either using one label per image, or using one custom component that paints both images. Because this lesson features painting, this section features the custom component approach, as implemented in MovingImageTimer.java.
--------------------------------------------------------------------------------
Note: You can also see an alternate implementation, which uses labels and a layered pane. You can find it in MovingLabels.java, which you can run by visiting MovingLabels.Html.
--------------------------------------------------------------------------------
Below are the two images this applet uses.
rocketship.gif:
starfield.gif:
Here´s a picture of the applet´s GUI. Remember that you can click on the applet to stop or start the animation.
This is a picture of the applet´s GUI. To run the applet, click the picture. The applet will appear in a new browser window.
--------------------------------------------------------------------------------
Note: The rocketship image has a transparent background. The transparent background makes the rocketship image appear to have a rocketship shape, no matter what color background it´s painted on top of. If the rocketship background weren´t transparent, then instead of the illusion of a rocketship moving through space, you´d see a rocketship on top of a rectangle moving through space.
--------------------------------------------------------------------------------
The code for performing this animation isn´t complex. Essentially, it´s a copy of the animation template that, instead of using a label to perform animation, uses a custom component. The custom component is a JPanel subclass that paints two images, one of which has a position that depends on the current frame number. Here is the code that paints the custom component:
...//Where the images are initialized:
Image background = getImage(getCodeBase(),
"images/rocketship.gif");
Image foreground = getImage(getCodeBase(),
"images/starfield.gif");
...
public void paintComponent(Graphics g) {
super.paintComponent(g); //paint any space not covered
//by the background image
int compWidth = getWidth();
int compHeight = getHeight();
//If we have a valid width and height for the
//background image, paint it.
imageWidth = background.getWidth(this);
imageHeight = background.getHeight(this);
if ((imageWidth > 0) && (imageHeight > 0)) {
g.drawImage(background,
(compWidth - imageWidth)/2,
(compHeight - imageHeight)/2, this);
}
//If we have a valid width and height for the
//foreground image, paint it.
imageWidth = foreground.getWidth(this);
imageHeight = foreground.getHeight(this);
if ((imageWidth > 0) && (imageHeight > 0)) {
g.drawImage(foreground,
((frameNumber*5)
% (imageWidth + compWidth))
- imageWidth,
(compHeight - imageHeight)/2,
this);
}
}
You might think that this program doesn´t need to clear the background, since it uses a background image. However, clearing the background is still necessary. One reason is that the applet usually starts painting before the images are fully loaded. If the rocketship image loaded before the background image, you would see parts of multiple rocketship until the background image loaded. Another reason is that if the applet painting area were wider than the background image, for some reason, then you´d see multiple rocketships to either side of the background image.
You could solve the first problem by delaying all painting until both images are fully loaded. The second problem could be solved by scaling the background image to fit the entire applet area. You´ll learn how to wait for images to be fully loaded in Improving the Appearance and Performance of Image Animation, later in this lesson. Scaling is described in Displaying Images.
显示一列图像
这一节的范例给出显示一列图像的基础,下一节有关于增强外观和动画性能的提示。这一节只显示applet代码,对于应用程序的代码很相似,除了应该使用加载图像 中描述的方法加载图像。
下面是这个applet使用的十个图像。
T1.gif: T2.gif: T3.gif: T4.gif: T5.gif:
T6.gif: T7.gif: T8.gif: T9.gif: T10.gif:
下面是applet的图片。记住你可以点击applet让它停止和重新开始动画。
这个图片是该applet的GUI。要运行那个applet,单击图片。该applet将在一个新浏览窗口显示。
这个范例的代码在 ImageSequenceTimer.java中,它甚至比前一个移动图像的范例的代码还简单。下面是和移动图像的范例的代码明显不同的地方:
. . .//在初始化代码中:
Image[] images = new Image[10];
for (int i = 1; i <= 10; i++) {
images[i-1] = getImage(getCodeBase(), "images/duke/T"+i+".gif");
}
. . .//在paintComponent方法中:
g.drawImage(images[ImageSequenceTimer.frameNumber % 10],
0, 0, this);
实现这个范例的另一个方法是使用一标签显示图像,不使用自定义绘图代码,使用setIcon 方法改变要显示的图像。
增强外观和动画性能
你可能注重到了上一页的动画的两个问题:
在图像还在加载时,程序部分的显示某些图像,有些根本就不显示。
加载图像花费的时间太长。
显示部分图像的问题很轻易修正,使用 MediaTracker 类。 MediaTracker 也能减少图像加载的时间。处理图像加载缓慢的另一个方法是以某种方式改变图像格式;这一页对此给出一些建议。
--------------------------------------------------------------------------------
注重: ImageIcon 类 在创建时通过Swing自动使用一个MediaTracker下载图像数据。更多信息请参看 如何使用图标。
--------------------------------------------------------------------------------
使用MediaTracker下载图像并延迟显示图像
MediaTracker 类可以让你很轻易的下载一组图像并且得到图像被全部加载完的时间。通常,图像数据在第一次显示时才被下载,为了请求一组图像异步地被预先加载,使用下面的MediaTracker 一个方法: checkID(anInt, true) 或者 checkAll(true)。同时加载数据(等待数据到达)使用 waitForID 或者 waitForAll。 MediaTracker 的数据加载方法使用几个后台进程下载数据,这可以加快下载速度。
要检查图像加载的状态,你可以使用MediaTracker的 statusID 或者 statusAll 方法。仅仅想检查是否有图像还需要加载,可以使用 checkID 或者checkAll 方法。
MTImageSequenceTimer.java 是使用MediaTracker的 waitForAll 和checkAll 方法的一个修正版。在所有的图像完全加载前,这个applet只是简单的显示一个"Please wait..." 消息。参看 MediaTracker API 文档 获得一个立即绘制一个背景图像但是延迟绘制动画图像的范例。
下面是applet的截图:
这个图片是该applet的GUI。要运行那个applet,单击图片。该applet将在一个新浏览窗口显示。
下面是改变的代码,它使用一个 MediaTracker 帮助推迟图像的显示。不同的地方使用黑体标记了。
...//声明实例变量:
MediaTracker tracker;
tracker = new MediaTracker(this);
...//在init方法中:
for (int i = 1; i <= 10; i++) {
images[i-1] = getImage(getCodeBase(),
"images/duke/T"+i+".gif");
}
...//在buildUI 方法中,
//它被init和main方法调用,
//运答应我们将这个例子作为一个applet或者应用程序运行:
for (int i = 1; i <= 10; i++) {
tracker.addImage(images[i-1], 0);
}
...//在actionPerformed 方法的开始:
try {
//开始下载图像,等待它们被加载完毕。
tracker.waitForAll();
} catch (InterruptedException e) {}
...//在paintComponent方法中:
//假如并非所有的图像被加载完,清除背景并显示状态字符串。
if (!tracker.checkAll()) {
g.clearRect(0, 0, d.width, d.height);
g.drawString("Please wait...", 0, d.height/2);
}
//假如所有的图像都被加载,绘制它们。
else {
...//same code as before...
}
加快图像的加载
无论你是否使用MediaTracker,使用URL加载图像(就像通常的applet所做的那样)通常要花费一段较长的时间。这些时间的大多是初始化HTTP连接占用的,每一个图像文件需要一个单独的HTTP连接,而每个连接可能花费几秒进行初始化。
避免这个性能损失的要害是将所有的图像放在一个文件里面。你可以使用一个JAR 文件做到这个,就像 将Applet的文件合并到一个文件里面 和 使用 JAR文件: 基础中描述的那样。
另一个可能有用的性能策略是将一组图像合并到一个图像文件中。可以到达这个目的的一个简单的方法是创建一个图像序列 --一个文件包含一行图像。下面是一个图像序列的例子:
jack.gif:
要从一个图像序列中绘制一个图像,你首先需要将绘图区的大小设置为一个图像的大小。然后绘制图像序列,(假如必要的话)向左移位,因此在绘图区只有你想要显示的图像出现。例如:
//imageStrip 是一个表示图像序列的Image对象。
//imageWidth 是序列中的一个单独的图像的大小。
//imageNumber 是要绘制的图像的序号(从0到numImages)
int stripWidth = imageStrip.getWidth(this);
int stripHeight = imageStrip.getHeight(this);
int imageWidth = stripWidth / numImages;
g.clipRect(0, 0, imageWidth, stripHeight);
g.drawImage(imageStrip, -imageNumber*imageWidth, 0, this);
假如你想图像加载得更快,你应该考虑图像压缩方案,非凡是帧内压缩。
更多精彩
赞助商链接