Developers using the Mobile Information Device Profile (MIDP) often ask how they can get a MIDlet to display animated images. MIDP 1.0 provides no direct support for animated images (the MIDP 2.0 specification, still in development, does), but it's not that hard to do it yourself.
The basic premise of any animation is to draw separate images quickly enough that the human eye sees movement. The images must of course be drawn in sequence, and the smaller the changes from one image to the next the better.
The first thing to do, then, is to use your favorite painting application to create a series of identically-sized images to compose the animation. Each separate image represents a different frame of the animation. You will need several frames -- the more frames, the smoother the resulting animation. Be sure to save the images in PNG (Portable Network Graphics) format, the only image format supported across all MIDP implementations.
There are two ways to make the images available to the MIDlet that is going to animate them. One way is to place them on a web server and have the MIDlet download them using MIDP's built-in HTTP support. The simpler way, however, is to package them with the MIDlet in its JAR file. If you're using the J2ME Wireless Toolkit, just place the PNG files in your project's res directory.
Animation is mostly a matter of bookkeeping: keeping track of the current frame and advancing to the next frame as appropriate. It makes sense to use a class to do the housekeeping for you, so define an AnimatedImage
class:
import java.util.*; import javax.microedition.lcdui.*; // Defines an animated image, which is just a set // of images of equal size which are drawn in turn // to simulate movement. public class AnimatedImage extends TimerTask { private Canvas canvas; private Image[] images; private int[][] clipList; private int current; private int x; private int y; private int w; private int h; // Construct an animation with no canvas. public AnimatedImage( Image[] images ){ this( null, images, null ); } // Construct an animation with a null clip list. public AnimatedImage( Canvas canvas, Image[] images ){ this( canvas, images, null ); } // Construct an animation. The canvas can be null, // but if not null then a repaint will be triggered // on it each time the image changes due to a timer // event. If a clip list is specified, the image is // drawn multiple times, each time with a different // clip rectangle, to simulate transparent parts. public AnimatedImage( Canvas canvas, Image[] images, int[][] clipList ){ this.canvas = canvas; this.images = images; this.clipList = clipList; if( images != null && clipList != null ){ if( clipList.length < images.length ){ throw new IllegalArgumentException(); } } if( images != null && images.length > 0 ){ w = images[0].getWidth(); h = images[0].getHeight(); } } // Move to the next frame, wrapping if necessary. public void advance( boolean repaint ){ if( ++current >= images.length ){ current = 0; } if( repaint && canvas != null && canvas.isShown() ){ canvas.repaint( x, y, w, h ); canvas.serviceRepaints(); } } // Draw the current image in the animation. If // no clip list, just a simple copy, otherwise // set the clipping rectangle accordingly and // draw the image multiple times. public void draw( Graphics g ){ if( w == 0 || h == 0 ) return; int which = current; if( clipList == null || clipList[which] == null ){ g.drawImage( images[which], x, y, g.TOP | g.LEFT ); } else { int cx = g.getClipX(); int cy = g.getClipY(); int cw = g.getClipWidth(); int ch = g.getClipHeight(); int[] list = clipList[which]; for( int i = 0; i + 3 <= list.length; i += 4 ){ g.setClip( x + list[0], y + list[1], list[2], list[3] ); g.drawImage( images[which], x, y, g.TOP | g.LEFT ); } g.setClip( cx, cy, cw, ch ); } } // Moves the animation's top left corner. public void move( int x, int y ){ this.x = x; this.y = y; } // Invoked by the timer. Advances to the next frame // and causes a repaint if a canvas is specified. public void run(){ if( w == 0 || h == 0 ) return; advance( true ); } }
When you instantiate AnimatedImage
you pass the constructor an array of Image
objects representing the individual animation frames. The images are assumed to be of identical height and width. Use the Image.createImage()
method to load an image from the JAR file:
private Image[] loadFrames( String name, int frames ) throws IOException { Image[] images = new Image[frames]; for( int i = 0; i < frames; ++i ){ images[i] = Image.createImage( name + i + ".png" ); } return images; }
For example, to load the series of frames stored as /images/bird0.png
, /images/bird1.png
, and so on through /images/bird6.png
, and then create an animated image, use:
Image[] frames = loadFrames( "/images/bird", 7 ); AnimatedImage ai = new AnimatedImage( frames ); ai.move( 20, 20 ); // set top-left to 20,20
Note that an AnimatedImage
keeps track of its position and draws itself relative to that position.
You can also pass in an optional Canvas
instance and an optional clip list. If you specify a canvas and use a timer to advance the animation to the next frame automatically, as in the next code sample, the canvas is automatically repainted after the advance. This behavior is completely optional, however -- the application can also choose when to repaint.
Because MIDP 1.0 does not support image transparency, the AnimatedImage
class simulates it using a clip list, a set of rectangular areas within the image. The image is drawn multiple times, each time with the clipping region set to one of the areas in the clip list. The clip list is specified on a per-frame basis, so you need to create an array of integers for each frame of the image. The array size is a multiple of four, because each clipping area consists of four values: the left coordinate, the top coordinate, the width, and the height. The coordinate values are relative to the top left corner of the image. Note that using a clip list slows down the animation. For complex images you may prefer to use vector graphics to do the drawing.
The AnimatedImage
class extends java.util.TimerTask
, which allows you to schedule it with a timer. A previous Tech Tip discussed how to use timers in detail. Here's an example of using one for animation:
Timer timer = new Timer(); AnimatedImage ai = ..... // get the image timer.schedule( ai, 200, 200 );
Every 200 milliseconds or so, the timer invokes the AnimatedImage.run()
method, which causes the animation to move to the next frame. The animation also repaints itself if a canvas is available.
All we need now is a MIDlet to try out the animation. We define a simple extension of Canvas
that lets us "attach" animated images to it:
import java.util.*; import javax.microedition.lcdui.*; // A canvas to which you can attach one or more // animated images. When the canvas is painted, // it cycles through the animated images and asks // them to paint their current image. public class AnimatedCanvas extends Canvas { private Display display; private Image offscreen; private Vector images = new Vector(); public AnimatedCanvas( Display display ){ this.display = display; // If the canvas is not double buffered by the // system, do it ourselves... if( !isDoubleBuffered() ){ offscreen = Image.createImage( getWidth(), getHeight() ); } } // Add an animated image to the list. public void add( AnimatedImage image ){ images.addElement( image ); } // Paint the canvas by erasing the screen and then // painting each animated image in turn. Double // buffering is used to reduce flicker. protected void paint( Graphics g ){ Graphics saved = g; if( offscreen != null ){ g = offscreen.getGraphics(); } g.setColor( 255, 255, 255 ); g.fillRect( 0, 0, getWidth(), getHeight() ); int n = images.size(); for( int i = 0; i < n; ++i ){ AnimatedImage img = (AnimatedImage) images.elementAt( i ); img.draw( g ); } if( g != saved ){ saved.drawImage( offscreen, 0, 0, Graphics.LEFT | Graphics.TOP ); } } }
The code for the AnimatedCanvas
class is quite simple, consisting of an animation registration method and a paint method. Every time the canvas is painted, it erases its background, then cycles through the list of registered animations, directing each to draw itself. Notice that the class uses double buffering to reduce flicker (an earlier Tech Tip discussed double buffering).
import java.io.*; import java.util.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; // MIDlet that displays some simple animations. // Displays a series of birds on the screen and // animates them at different (random) rates. public class AnimationTest extends MIDlet implements CommandListener { private static final int BIRD_FRAMES = 7; private static final int NUM_BIRDS = 5; private Display display; private Timer timer = new Timer(); private AnimatedImage[] birds; private Random random = new Random(); public static final Command exitCommand = new Command( "Exit", Command.EXIT, 1 ); public AnimationTest(){ } public void commandAction( Command c, Displayable d ){ if( c == exitCommand ){ exitMIDlet(); } } protected void destroyApp( boolean unconditional ) throws MIDletStateChangeException { exitMIDlet(); } public void exitMIDlet(){ timer.cancel(); // turn it off... notifyDestroyed(); } // Generate a non-negative random number... private int genRandom( int upper ){ return( Math.abs( random.nextInt() ) % upper ); } public Display getDisplay(){ return display; } // Initialize things by creating the canvas and then // creating a series of birds that are moved to // random locations on the canvas and attached to // a timer for scheduling. protected void initMIDlet(){ try { AnimatedCanvas c = new AnimatedCanvas( getDisplay() ); Image[] images = loadFrames( "/images/bird", BIRD_FRAMES ); int w = c.getWidth(); int h = c.getHeight(); birds = new AnimatedImage[ NUM_BIRDS ]; for( int i = 0; i < NUM_BIRDS; ++i ){ AnimatedImage b = new AnimatedImage( c, images ); birds[i] = b; b.move( genRandom( w ), genRandom( h ) ); c.add( b ); timer.schedule( b, genRandom( 1000 ), genRandom( 400 ) ); } c.addCommand( exitCommand ); c.setCommandListener( this ); getDisplay().setCurrent( c ); } catch( IOException e ){ System.out.println( "Could not load images" ); exitMIDlet(); } } // Load the bird animation, which is stored as a // series of PNG files in the MIDlet suite. private Image[] loadFrames( String name, int frames ) throws IOException { Image[] images = new Image[frames]; for( int i = 0; i < frames; ++i ){ images[i] = Image.createImage( name + i + ".png" ); } return images; } protected void pauseApp(){ } protected void startApp() throws MIDletStateChangeException { if( display == null ){ display = Display.getDisplay( this ); initMIDlet(); } } }
The seven-frame image set gives you an animation of a bird flapping its wings. The MIDlet places five birds at random locations with random refresh speeds. You can improve on this simple example in any number of ways, but it should be enough to get you going.
![]() |