您的位置:寻梦网首页编程乐园Java天地Core Java>Java对多媒体的支持
Java 天地
Java对多媒体的支持

Java语言的内置类库对多媒体技术的支持能力相当强, 尤其是对文本、图形、图像、声音等媒体的处理与展示均提供了极其方便而又丰富的接口。 更令人兴奋不已的是, 综合运用这些媒体所编制出来的一个个Java小应用程序(Applet), 使向来冷冰冰的静态的Web主页(Homepage) 上居然展现出一番热热闹闹的动态的新景观, 这便是著名的Java动画。 在这一章里, 我们就将进入到

1 图形与文本

如何利用Java的绘图方法来绘制各式各样图形以及显示各种文本字体, 并配以所喜爱颜色, 可以说是Java多媒体技术中的一项基本功, 也是这一节将要介绍的主要内容。

1.1 图形的绘制

Java语言的类库中提供了丰富的绘图方法(method), 其中大部分对图形、文本、图像(image)的操作方法都定义在Graphics类中。 我们已经知道, Graphics类又是java.awt程序包的一部分, 因此, 每当我们要进行图形、文本、图像的处理时, 不要忘了在Java源文件的头部先写上:

import java.awt.Graphics;
在这里要特别指出的是, 当我们想要在屏幕上绘制图形、文本、图像时, 并不需要直接使用new来产生一个Graphics类的对象实例, 而在java.awt.Applet类的paint( )方法中, 我们已经得到了一个Graphics对象的引用, 这是系统直接将生成好的Graphics对象通过参数形式传递给paint( )方法。 因此, 我们只要在这个对象上进行图形、文本及图像的绘制操作, 就可以在屏幕上看到所显示的结果。

1. 图形坐标系统

为了将某一图形在屏幕上绘制出来, 我们首先要碰到的问题也许就是“画在哪个位置”, 为了解决这个问题就必须有一个精确的图形坐标系统来将该图形定位。

  与大多数其它计算机图形系统所采用的二维坐标系统一样, Java的坐标原点(0,0)在屏幕的左上角, 水平向右为X轴的正方向, 竖直向下为Y轴的正方向, 每个坐标点的值表示屏幕上的一个象素点的位置, 因此, 所有坐标点的值都取整数。 图4-1表示用此图形坐标系统在屏幕上绘制一个矩形。

2. 画线

在Java的Graphics类中提供画线功能的是drawLine( )方法, 其调用格式如下:

drawLine(int x1,int y1,int x2,int y2)

该方法需要设置四个参数, 其中x1,y1表示线段的一个坐标点, x2,y2表示线段的另一个坐标点。 如下面这段程序画出两条线段, 其显示结果如图4-2所示。

图4-1 图形坐标系统

1: import java.awt.Graphics;
2: public class Lines extends java.applet.Applet{
3:   public void paint(Graphics g){ 
4:      g.drawLine(30,30,70,70);
5:      g.drawLine(60,50,60,50);
6:   }
7: }

图4-2 一条线段与一个点

由于Graphics类不专门提供画点的方法, 所以程序中第5行将线段的两个点的坐标均设为(60,50), 因而就相当于在此处画了一个点。

3. 矩形

Graphics类中提供了三种类型的矩形, 它们分别是普通矩形、圆角矩形和立体矩形。 而每一种矩形都提供两种不同风格的方法, 一种是仅画出矩形的边框;另一种是不仅画出边框, 并且还用相同的颜色将整个矩形区域填满。

(1)普通矩形

画普通矩形需调用drawRect( )或fillRect( )方法, 它们的调用格式如下:

     drawRect(int x, int y, int width, int height)        //边框型风格
     fillRect(int x, int y, int width, int height)        //填充型风格 

其中头两个参数分别表示矩形左上角的x坐标和y坐标, 后两个参数分别表示矩形的宽度和高度。 如下面的paint( )方法画出两个矩形, 其显示结果如图4-2所示。

   public void paint(Graphics g){
     g.drawRect(40,20,60,40);
     g.fillRect(120,20,60,40);
   }

图4-3 普通矩形的例子


(2)圆角矩形

圆角矩形, 也就是矩形的四个顶角呈圆弧状, 每个圆弧其实是由四分之一的椭圆弧所构成。 画圆角矩形的两个方法的调用格式如下:

 drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)
 fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight)

我们可以看出, 它们除了具有和普通矩形含义相同的前四个参数外, 还多了两个用来描述圆角性质的参数。 其中arcWidth代表了圆角弧的横向直径;arcHeight代表了圆角弧的纵向直径。 例如图4-4中左边一个圆角矩形所设的圆角参数为(arcWidth=80, arcHeight=30), 其效果就相当于该圆角弧存在于一个长40宽15的小矩形中;而右边一个圆角矩形的圆角参数为(arcWidth=100, arcHeight=60), 并且和整个圆角矩形的width和height参数值相等, 因而该圆角矩形实际变成了一个椭圆。


图4-4 圆角矩形的参数设置

下面的paint( )方法画出三个圆角矩形, 显示效果如图4-5所示。 我们不难发现, 随着arcWidth和arcHeight值的增大, 矩形的圆角就越圆。

 public void paint(Graphics g){
   g.drawRoundRect(20,20,80,60,20,20);
   g.fillRoundRect(120,20,80,60,40,30);
   g.drawRoundRect(220,20,80,60,60,40);}


图4-5 圆角矩形的例子

(3)立体矩形

立体矩形也可以说是三维矩形。 其实, Java中的立体矩形并非真正的三维图形, 而仅仅是在矩形的边框上增加一点阴影, 使矩形看上去相对表平面好象有凸出或凹下的效果, 其调用方法的格式如下:

 draw3DRect(int x, int y, int width, int height, boolean raised)
 fill3DRect(int x, int y, int width, int height, boolean raised)

这两个方法中的前四个参数与drawRect( )方法中所用的参数含义是一样的, 第五个参数raised便是定义该立体矩形是具有凸出(值为true)还是凹下(值为false)的效果。 例如, 下面的paint( )方法中, 分别画了一个凸出的和一个凹下的矩形。 其显示效果如图4-6所示。 确实, 由于Java立体矩形中的阴影实在太薄, 立体效果当然也就不太明显, 图4-6右边是一个放大了的凹角形状。

 public void paint(Graphics g){
   g.draw3DRect(20,20,80,60,true);
   g.fill3DRect(120,20,80,60,false);
 }

图4-6 立体矩形的例子

4. 多边形

多边形的画法通常是给出一组坐标点, 再用直线段将这些点依次连接起来。 Graphics类中也提供两个方法来画多边形, 一个是边框型drawPolygon( )方法, 另一个是填充型fillPolygon( )方法, 并且每一种方法都有两种不同的参数类型。 第一种参数类型的调用格式为:

 drawPolygon(int xPoints[],int yPoints[],int nPoints)
 fillPolygon(int xPoints[],int yPoints[],int nPoints)

其中xPoints参数是一个整数数组, 用以存放多边形坐标点的X坐标值, yPoints参数存放相应的一组Y坐标值, nPoints则表示共有几个坐标点。 如下面的paint( )方法分别画了一个边框型和一个填充型的多边形, 其显示效果如图4-7所示。

 public void paint(Graphics g){
   int Poly1_x[]={30,63,115,72,67};
   int Poly1_y[]={40,20,95,74,106};
   int Poly1_pts=Poly1_x.length;
   int Poly2_x[]={180,213,265,222,217};
   int Poly2_y[]={40,20,95,74,106};
   int Poly2_pts=Poly2_x.length;
   g.drawPolygon(Poly1_x,Poly1_y, Poly1_pts);
   g.fillPolygon(Poly2_x,Poly2_y, Poly2_pts);
 }

图4-7 多边形的例子

可以看出, 边框型多边形并不自动关闭多边形的最后一条边, 而仅是一段开放的折线。 所以, 若想画封闭的边框型多边形, 不要忘了在数组的尾部再添上一个起始点的坐标。 上述两个画多边形方法的第二种参数传递形式为:


drawPolygon(Polygon p)
fillPolygon(Polygon p)

其中Polygon是定义在java.awt中的一个类, 它的构造方法也有两种不同的参数传递形式, 一种与drawPolygon( )方法的第一种调用格式一样:

 Polygon(int xPoints[],int yPoints[],int nPoints)

另一种调用格式则是创建一个空的多边形(无参数):

Polygon( )

那为什么要另外创建Polygon对象?生成一个空的Polygon对象又有何用呢?原来, Polygon类中提供了一系列特有的方法, 可以方便的进行与多边形相关的操作, 象其中的addPoint( )方法可将多边形的坐标点动态地增加到Polygon对象中。 如下面列出的paint( )方法所执行的结果与图4-7所示的结果是一样的。

 public void paint(Graphics g){
   int Poly1_x[]={30,63,115,72,67};
   int Poly1_y[]={40,20,95,74,106};
   int Poly1_pts=Poly1_x.length;
   Polygon poly1= new Polygon(Poly1_x,Poly1_y, Poly1_pts);
   Polygon poly2= new Polygon();
   poly2.addPoint(180,40);
   poly2.addPoint(213,20);
   poly2.addPoint(265,95);
   poly2.addPoint(222,74);
   poly2.addPoint(217,106);
   g.drawPolygon(poly1);
   g.fillPolygon(poly2);
 }

5. 椭圆

在Java中绘制椭圆的方法是给出该椭圆的外接矩形作为参数, 其调用格式与画普通矩形的方法相似:

 drawOval(int x, int y, int width, int height)  //边框型风格
 fillOval(int x, int y, int width, int height)  //填充型风格

这里要特别注意:x和y不是椭圆的圆心坐标, 而是该椭圆外接矩形的左上角。 因此, 画椭圆时, 把它先看作是一个矩形将有助于在坐标系统中定位。 另外, Graphics类不专门提供画圆的方法, 而只需将width与height参数置成相等就可以了。 例如, 下面的paint( )方法, 画出一个圆和一个用颜色填充的椭圆, 其显示效果如图4-8所示。

 public void paint(Graphics g){
   g.drawOval(30,20,60,60);
   g.fillOval(130,20,80,60);
 }

图4-8 椭圆的例子

6. 画弧

弧是椭圆的一部分, 因而画弧的方法就相当于先画一个椭圆, 而后取该椭圆中所需要的一部分。 它们的调用格式如下:

 drawArc(int x, int y, int width, int height,int startAngle, int arcAngle)   //边框型风格
 fillArc(int x, int y, int width, int height,int startAngle, int arcAngle)  //填充型风格

其中前四个参数的含义与画椭圆一样, 因此也必须用矩形的观点来确定弧在坐标系统中的位置。 后两个参数就是用来定义椭圆的一部分:startAngle参数表示该弧从什么角度开始, arcAngle参数表示从startAngle开始转了多少度。 如图4-9的弧度坐标系所示, 水平向右表示0度, 逆时钟方向为正角度值, 顺时钟方向为负角度值。

图4-9 弧度坐标系

如果startAngle和arcAngle中有任一值大于360度的话, 都会被转换为0到360度之间的数值。 因此若要画满整个椭圆, arcAngle需设为360的整数倍, 若设为370度则相当于只画了10度。 另外fillArc( )方法的效果并不是填充弧的两个端点直接连线所围的区域, 而是填充弧的两端点与圆心连线所围的扇形区域, 象一个饼图。 下面的paint( )方法画了图4-9中的几段弧, 其显示效果如图4-10所示。

 public void paint(Graphics g){
   g.drawArc(10,20,100,60,35,65);
   g.drawArc(110,20,100,60,35,-140);
   g.fillArc(210,20,100,60,35,65);
   g.fillArc(310,20,100,60,35,-140);
 }

图4-10 弧的例子

7. 复制与清除图形

当我们需要在屏幕上重复绘制一些相同的图形时, 也可采用Graphics类中的copyArea( )方法, 它能将屏幕上某一矩形区域里的内容复制到屏幕的另一区域。 其调用格式如下:

  copyArea(int x, int y, int width, int height, int dx, int dy)

前四个参数我们应该是相当熟悉了, 它定义了要被复制的屏幕的矩形区域。 最后两个参数则表示新区域与原始屏幕区域的偏移距离:若dx,dy为正值, 则表示新区域相对于原区域的右方及下方所偏移的像素值;反之, 它们取负值则分别表示相对左方及上方的偏移量。

若要清除屏幕的某一矩形区域所画的内容, 就要选用clearRect( )方法, 它用当前的背景颜色来填充整个矩形区域。 其调用格式为:

clearRect(int x, int y, int width, int height

可以看出这四个参数定义了所要清除的矩形区域。 例如, 想要清除整个applet区域, 可先调用Applet类的size( )方法得到整个applet的宽度和高度(该方法没有参数, 返回值是一个Dimension对象, 该对象具有width和height实例变量), 再调用clearRect( )方法就可以了:

g.clearRect(0, 0, size( ).width, size( ).height);

4.1.2 文本与字体

Graphics类也提供了在屏幕上显示文本的方法, 但若要使文本的显示更具特色, 让它满足某种字体、某种风格及尺寸大小的要求, 就需要用字体类Font来定义。

1. 设置文本信息

当我们想要在屏幕上输出文本信息时, 首先要确定的就是采用何种字体, 例如中文的“宋体”、“楷体”, 或是英文的“TimesRoman”体、“Courier”体等等, 接着再决定该字体输出时采用哪种风格, 是斜体型还是粗体型等等, 最后还要确定该字体的大小尺寸。 所有这些都由Font类来定义, 我们不难猜出其构造方法的调用格式:

  Font(String name, int style, int size)
不错, 它的三个参数就是我们先前所说的字体名、字体风格和尺寸大小。 并且Font类中已定义了类变量来表示字体的style值, 如Font.BOLD(表示粗体)、Font.ITALIC(表示斜体)、Font.PLAIN(表示普通体)。 由于它们被定义为整数常量, 因此可以进行相加运算来生成复合style, 例如想让style即是粗体又是斜体, 可以这样写:
Font fn = new Font("TimesRoman", Font.BOLD+Font.ITALIC, 28);

虽然我们定义了所需的字体, 但其显示结果有时也并非如愿。 因为运行该applet的客户端系统有可能并未安装该字体, 这时Java就会以缺省字体来替代它。 因此, 不妨先查看一下客户端系统目前究竟支持哪些字体, 这就要用到java.awt.Toolkit类中的getFontlist( )方法, 它返回系统目前可用的字体列表, 然后就可决定到底选用哪种字体。 例如:

  Toolkit systk = Toolkit.getDefaultToolkit( );
  String fonts = systk.getFontList( );

2. 显示文本

创建了Font对象以后, 我们就可以利用Graphics类中提供的drawString( )、drawChars( )等方法来显示字符串与字符。 当然, 首先还要用setFont( )方法, 将所创建的Font对象设为当前所用的字体。 下面就是Graphics类中这三个方法的调用格式:

  setFont(Font font);
  drawString(String str, int x, int y);
  drawChars(char data[], int offset, int length, int x, int y);

其中setFont( )方法的参数就是一个创建好的Font对象, 表明系统当前选用哪个Font对象所定义的字体信息。

drawString( )方法中的str即是要显示的字符串, x,y指明字符串显示的起始位置坐标, 具体的说, x表示第一个字符的左边界, y表示整个字符串的基线(baseline, 见图4-12)位置坐标。 因此, 这里的坐标并不是通常意义上的矩形区域的左上角。

drawChars( )方法则是用来显示多个字符的, 也就是从给定的字符数组中抽取连续的一部分显示在屏幕上。 其中data参数就是给定的原始字符数组, offset表示从第几个字符位置开始显示, length表示共显示几个字符, x与y参数的含义与drawString( )方法一样, 代表显示在屏幕上的起始位置。 如下面的程序显示了一些不同的文本字体, 其显示结果如图4-11所示。

import java.awt.Graphics;import java.awt.Font;
public class Fonts extends java.applet.Applet{
  public void paint(Graphics g){
    Font ftp20 = new Font("TimesRoman",Font.PLAIN,20);
    Font fai15 = new Font("Arial",Font.ITALIC,15);
    Font fcb24 = new Font("Courier",Font.BOLD,24);
    Font fsib30 = new Font("宋体",Font.ITALIC+Font.BOLD,30);
    g.setFont(ftp20);
    g.drawString("Font name TimesRoman , style plain , size 20",10,20);
    g.setFont(fai15);
    g.drawString("Font name Arial , style italic , size 15",10,50);
    g.setFont(fcb24);
    g.drawString("Font name Courier , style bold , size 24",10,80);
    g.setFont(fsib30);
    g.drawString("字体名 宋体, 风格 斜体+粗体, 尺寸 30",10,120);
  }
}

图4-11 各种字体的例子

3. 获取字体信息

(1)获取基本信息  如果不清楚系统当前所用的字体信息, 可以先调用Graphics类中的getFont( )方法, 该方法无参数, 它返回系统当前所用的Font对象, 然后就可以调用Font类中提供的几个方法来获取该字体的基本信息。 表 4-1列出了Font类中的这些方法。

表4-1 Font类提供的一些主要方法

(2)获取详细信息 有时候, 我们为了在屏幕上更精确地定位文本, 还需要了解所选字体的更详细的信息, 例如整个字符串到底有多高, 有多宽, 两行字符串的间隙有多少等等。 这时, 我们需要用到一个新的类FontMetrics来提供这一信息。 我们可以调用Graphics类中的getFontMetrics( )方法来获取关于当前字体的FontMetrics对象(该方法也无参数)。 然后就可以利用表4-2所示的FontMetrics类中所提供的方法来获取更详细的字体信息。 图4-12中给出了字体中关于Ascent、Descent、Leading等概念的示意图。

4.1.3 颜色的设置

现在, 让我们改变一下总是在灰色背景上用黑色绘图以及显示文本的习惯, 而给我们的applet增添一些五彩缤纷的色彩。 与设置字体信息相似, 要设置新的颜色, 必须先创建Color对象, 然后再调用Graphics类中设置颜色的方法来将生成的 Color对象设为当前所用的绘图颜色。

图4-12字体

1. 创建Color类

Java中每一种颜色都看成是由红(R)、绿(G)、蓝(B)三原色组合而成的。 因此Color类的构造方法采用如下格式:

  Color(int r, int g, int b); 

其中每个参数的值都在0到255之间, 数值越大就表明这种颜色的成份越重。 例如(0,0,0) 代表黑色, (255,0,0)代表红色。 当然最终在屏幕上是否能显示所定义的颜色还取决于客户端系统的调色板所支持的颜色种类的多少。 若客户端系统的调色板并不支持当前所定义的颜色值, 就会在调色板中挑选最接近的颜色来代替。

Color类中还定义了一些标准颜色的Color对象存储在类变量中, 使的这些标准颜色的引用显得更为方便。 这些类变量如表4-3所示。

2. 设置当前颜色

为了能使用刚才生成好的Color对象来显示文本及绘制图形, 还需调用Graphics类中的setColor( )方法把这个对象设置为系统当前所用的绘画颜色, 其调用格式为:

setColor(Color c);

例如, 想要用蓝色来显示文本, 最简单的办法是直接引用标准色的类变量:

setColor(Color.blue);

另外, Java还提供了设置整个applet的背景和前景的方法, 它们分别是setBackground( )方法和setForeground( )方法, 它们都被定义在java.awt.Component类中, 因此该方法能被其子类(包括Applet类及Applet类的子类)自动继承, 它们的调用格式与setColor( )方法一样:

  setBackground(Color c);
  setForeground(Color c);

其中setForeground( )方法将影响到applet中所有已经用其它任何颜色所绘制的图形及显示的文本, 把它们一下子都变为该方法所定义的前景颜色, 而不需用该颜色重新一一绘制。

有“set”必有相应的“get”, Java中还提供了getColor( )方法(Graphics类中)、 getBackground( )方法和getForeground( )方法(Component类中)来分别获取当前的绘图颜色、applet背景及前景颜色的对象。 下面, 我们写一段程序来显示一排用随机定义的颜色所填充的小方块, 它们的显示效果如图4-13所示。

import java.awt.Graphics;import java.awt.Color;
public class Colors extends java.applet.Applet{
  public void paint(Graphics g){
    int red,green,blue;
    for (int i=10;i<400;i+=40){
      red="(int)Math.floor(Math.random()*256);"  
      green="(int)Math.floor(Math.random()*256);"
      blue="(int)Math.floor(Math.random()*256);"
      g.setColor(new Color(red,green,blue)); 
      g.fillRect(i,20,30,30); 
    }
  }
} 

图4-13 使用绘图颜色的例子


图像与声音

与其它语言相比较, 如果说Java对图形和文本媒体的支持并不占明显优势的话, 那么Java对图像与声音媒体的支持真可谓是技高一筹, 显示图像与播放声音就如同显示一行文本一样方便。 同时, 正由于在Java动画中灵活的运用图像和声音媒体, 才使得Web页面更具魅力。

4.2.1 图像文件的显示

正如上一节所介绍的, Graphics类中确实提供了不少绘制图形的方法, 但如果用它们在applet运行过程中实时地绘制一幅较复杂的图形(例如一条活泼可爱的小狗), 就好比是在用斧头和木块去制造航天飞机。 因此, 对于复杂图形, 大部分都事先用专用的绘图软件绘制好, 或者是用其它截取图像的工具(如扫描仪、视效卡等)获取图像的数据信息, 再将它们按一定的格式存入图像文件。 applet运行时, 只要找到图像文件存贮的位置, 将它装载到内存里, 然后在适当的时机将它显示在屏幕上就可以了。

1. 图像文件的装载

Java目前所支持的图像文件格式只有两种, 它们分别是GIF和JPEG格式(带有.GIF、.JPG、.JPEG后缀名的文件)。 因此若是其它格式的图像文件, 就先要将它们转换为这两种格式。 能转换图像格式的软件有很多, 如PhotoStyler等。 Applet类中提供了getImage( )方法用来将准备好的图像文件装载到applet中, 但我们必须首先指明图像文件所存贮的位置。

由于Java语言是面向网络应用的, 因此文件的存贮位置并不局限于本地机器的磁盘目录, 而大部分情况是直接存取网络中Web服务器上的图像文件, 因而, Java采用URL(Universal Resource Location, 统一资源定位器)来定位图像文件的网络位置。 因此, Java专门提供了URL类来管理URL信息(关于该类的详细介绍见下一章)。

表示一个URL信息可分为两种形式: 一种称为绝对URL形式, 它指明了网络资源的全路径名。 如:

绝对URL:“http://www.xyz.com/java/imgsample/images/m1.gif” 另一种称为相对URL形式, 分别由基准URL(即base URL)再加上相对于基准URL下的相对URL这两部分组成, 例如上面的例子可表示为:

基准URL:“http://www.xyz.com/java/imgsample/”
相对URL:“images/m1.gif”

现在, 我们可以来看一下getImage( )方法的调用格式了:

  Image getImage(URL url);
  Image getImage(URL url, String name);
我们可以发现, 这两种调用格式的返回值都是Image对象。 确实, Java特别提供了java.awt.Image类来管理与图像文件有关的信息, 因此执行与图像文件有关的操作时不要忘了import这个类。 getImage( )方法的第一种调用格式只需一个URL对象作为参数, 这便是绝对URL。 而后一种格式则带有两个参数, 第一个参数给出的URL对象是基准URL, 第二个参数是字符串类型, 它描述了相对于基准URL下的路径和文件名信息, 因此这两个参数的内容综合在一起就构成了一个绝对URL。 例如, 下面两种写法所返回的结果是一样的:
  Image img=getImage(new URL("http://www.xyz.com/java/imgsample/images/m1.gif");
  Image img=getImage(new URL("http://www.xyz.com/java/imgsample/"),"images/m1.gif");

表面看来, 好象第一种调用格式较方便一些, 但实际上第二种调用格式用得更普遍, 因为这种格式更具灵活性。 原来, Applet类中提供了两个方法来帮助我们方便地获取基准URL对象, 它们的调用格式如下:

  URL getDocumentBase( );
  URL getCodeBase( );

其中getDocumentBase( )方法返回的基准URL对象代表了包含该applet的HTML文件所处的目录, 例如该文件存贮在"http://www.xyz.com/java/imgsample/m1.html"中, 则该方法就返回"http://www.xyz.com/java/imgsample/"路径。 而getCodeBase( )方法返回的基准URL对象代表了该applet文件(.class文件)所处的目录。 它是根据HTML文件的"APPLET"标记中的CODEBASE属性值计算出来的, 若该属性没有设置, 则同样返回该HTML文件所处的目录。 好了, 现在我们应该可以感受到基准URL的灵活性了吧。 只要我们写下语句:

  Image img = getImage(getDocumentBase( ),"images/m1.gif");

那么即使整个imgsample目录移到别处任何地方, 也可以正确装载图像文件, 而采用对于绝对URL形式则需要重新修改applet代码并重新编译。

2. 图像文件的显示

getImage( )方法仅仅是将图像文件从网络上装载进来, 交由Image对象管理。 那我们怎样把得到的Image对象中的图像显示在屏幕上呢?这又要回到我们的老朋友Graphics类中来了, 因为Graphics类提供了一个drawImage( )方法, 它能完成将Image对象中的图像显示在屏幕的特定位置上, 就象显示文本一样方便。 drawImage( )方法的调用格式如下:

boolean drawImage(Image img, int x, int y, ImageObserver observer);

其中img参数就是要显示的Image对象。 x和y参数是该图像左上角的坐标值。 observer参数则是一个ImageObserver接口(interface), 它用来跟踪图像文件装载是否已经完成的情况, 通常我们都将该参数置为this, 即传递本对象的引用去实现这个接口。 除了将图像文件照原样输出以外, drawImage( )方法的另外一种调用格式还能指定图像显示的区域大小.

  boolean drawImage(Image img, int x, int y, 
           int width, int height, ImageObserver observer)

这种格式比第一种格式多了两个参数width和height, 即表示图像显示的宽度和高度。 若实际图像的宽度和高度与这两个参数值不一样时, Java系统会自动将它进行缩放, 以适合我们所定的矩形区域。

有时, 我们为了不使图像因缩放而变形失真, 可以将原图的宽和高均按相同的比例进行缩小或放大。 那么怎样知道原图的大小呢? 只需调用Image类中的两个方法就可以分别得到原图的宽度和高度。 它们的调用格式如下:

  int getWidth(ImageObserver observer);
  int getHeight(ImageObserver observer);

同drawImage( )方法一样, 我们通常用this作为observer的参数值。 下面的程序段给出了一个显示图像文件的例子, 其显示结果如图4-14所示。

import java.awt.Graphics;import java.awt.Image;
public class Images extends java.applet.Applet{
  Image img;
  public void init(){
    img=getImage(getCodeBase(),"man.gif");
  }
  public void paint(Graphics g){
    int w=img.getWidth(this);
    int h=img.getHeight(this);
    g.drawImage(img,20,10,this); //原图
    g.drawImage(img,200,10,w/2,h/2,this); //缩小一半
    g.drawImage(img,20,200,w*2,h/3,this); //宽扁图
    g.drawImage(img,350,10,w/2,h*2,this); //瘦高图
  }
}

图4-14 显示图像文件

4.2.2 声音文件的播放

对声音媒体的直接支持可以说是Java的一大特色, 尤其是在动画中配上声音效果, 就可以使人在视觉上和听觉上均得到美的享受, 那才叫过瘾。

Java中播放声音文件与显示图像文件一样方便, 同样只需要先将声音文件装载进来, 然后播放就行了。

Java目前支持的声音文件只有一种格式, 那就是SUN公司的AU格式(.AU文件), 也称为u-law格式。 由于AU格式的声音仅有8KHz的采样频率且不支持立体声效果, 所以音质不算太好。 唯一的好处就是AU声音文件的尺寸比其它格式小, 有利于网上传输。

一般, 我们较熟悉的大都是WAV格式的声音文件, 因此必须先将它们转换为AU格式(可以选用Goldwave软件来进行这种格式转换)。 声音文件准备好以后, 就可以考虑将它装载进来并播放。 在Applet类中提供的play( )方法可以将声音文件的装载与播放一并完成, 其调用格式如下:

  void play(URL url);
  void play(URL url, String name);

可见, play( )方法的调用格式与getImage( )方法是完全一样的, 也采用URL来定位声音文件。 例如, 某声音文件audio.au与applet文件存放在同一目录下, 可以这样写:

  play(getCodeBase( ),"audio.au");

一旦play( )方法装载了该声音文件, 就立即播放。 如果找不到指定URL下的声音文件, play( )方法不返回出错信息, 只是听不到想听的声音而已。

由于play( )方法只能将声音播放一遍, 若想循环播放某声音作为背景音乐, 就需要用到功能更强大的AudioClip类, 它能更有效地管理声音的播放操作。 因为它被定义在java.applet程序包中, 所以使用该类的话, 不要忘了在程序头部加上:

  import java.applet.AudioClip;
为了得到AudioClip对象, 我们可以调用Applet类中的getAudioClip( )方法。 它能装载指定URL的声音文件, 并返回一个AudioClip对象, 其调用格式如下:
  AudioClip getAudioClip(URL url);
  AudioClip getAudioClip(URL url, String name);

得到AudioClip对象以后, 就可以调用AudioClip类中所提供的各种方法来操作其中的声音数据, 这些方法如表4-4所示。

如果getAudioClip( )方法没有找到所指定的声音文件, 就会返回null值。 所以, 在调用表4-4中所列的方法前, 应该先检查一下得到的AudioClip对象不是null, 因为在null对象上调用上述方法将导致出错。

如果需要的话, 我们还可以在applet中同时装载几个声音文件来一起播放, 到时候, 这些声音将混合在一起, 就象二重奏一样。 另外还有一点要说明的是, 如果我们使用AudioClip对象的loop( )方法来重复播放背景音乐时, 千万不要忘记在适当的时候调用AudioClip对象的stop( )方法来结束放音, 否则的话, 即使用户离开这一Web页面, 该声音也不会停止, 这无疑将会惹恼用户。 因此, 一般我们都在applet的stop( )方法中添上停止播放的代码。 例如, 下面这段程序将播放两段声音, 一段是连续播放的背景音乐, 另一段是讲话录音。

import java.applet.AudioClip;public class Audios extends java.applet.Applet{
  AudioClip bgmusic,speak;
  public void init(){
    bgmusic=getAudioClip(getDocumentBase(),"space.au");
    speak=getAudioClip(getDocumentBase(),"intro.au");
  }
  public void start(){
    if(bgmusic!=null)
    bgmusic.loop();
    if(speak!=null)
    speak.play();
  }
  public void stop(){
    if(bgmusic!=null)
    bgmusic.stop(); //关闭背景音乐, 切记。
  }
}

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对象列入跟踪范围后, 我们就可以在开始放映动画前调用MediaTracker类提供的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 < 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<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); 
   } 
}