You are on page 1of 281

Game Canvas Basics

Many, perhaps most, of the early commercial applications developed on the J2ME platform were games. With all the obvious interest in gaming - and the many devicespecific gaming extensions developed by different handset vendors - it was no surprise to anyone that the Java Community Process group responsible for defining MIDP 2.0 introduced basic gaming capabilities. Let's take a look at the core of those capabilities, the GameCanvas class. The GameCanvas class, found in the javax.microedition.lcdui.game package, extends the venerable Canvas class. As you know, Canvas lets an application draw screens using the low-level user-interface API, and also receive key and pointer events directly. The shortcoming of Canvas is that it gives the application no control over when a canvas repaints itself - all it can do is request a repaint - or over how quickly key and pointer events get delivered to the canvas. This lack of control can cause action games and others that are speed-sensitive to feel sluggish and unresponsive. GameCanvas was designed specifically to fix these weak points. The first thing to remember, though, is that a game canvas is still a canvas, so all the usual behavior is there. The showNotify() and hideNotify() methods are still called when the canvas is shown and hidden. Key and pointer events are still delivered, except for certain key events which optionally can be suppressed. You still have to do all the drawing yourself, and you can still attach Command objects to the canvas. Here's a very simple test canvas you can use inside a MIDlet you run with the J2ME Wireless Toolkit 2.0 or higher:
import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.GameCanvas; public class DummyGameCanvas extends GameCanvas { public DummyGameCanvas( boolean suppress ){ super( suppress ); } private String getAction( int key ){ int action = getGameAction( key ); switch( action ){ case DOWN: return "DOWN"; case UP: return "UP"; case LEFT: return "LEFT"; case RIGHT: return "RIGHT";

case FIRE: return case GAME_A: return case GAME_B: return case GAME_C: return case GAME_D: return } return ""; }

"FIRE"; "GAME_A"; "GAME_B"; "GAME_C"; "GAME_D";

protected void hideNotify(){ System.out.println( "hideNotify" ); } protected void keyPressed( int key ){ System.out.println( "keyPressed " + key + " " + getAction( key ) ); } protected void showNotify(){ System.out.println( "showNotify" ); } }

Two differences appear when you compare this class to a standard canvas. First, the constructor takes a boolean to indicate whether certain key events are to be suppressed or not. Second, no paint() method is needed; GameCanvas supplies its own implementation. But the most significant difference - the game loop - doesn't show up in DummyGameCanvas. In general, MIDP user-interface components are event-driven - the system invokes methods directly, in response to device events. These events are queued and delivered to the application one at a time, and there may be a delay between the time the event occurs and the time the application receives it, which particularly affects painting events. The GameCanvas is different: It lets the application poll for key events quickly and repaint the canvas in a timely fashion. This polling and repainting is normally done in a loop on a separate thread, hence the term game loop. To poll for key events, use getKeyStates(). It returns a bit mask representing the change in state of the action keys - defined by the Canvas class - since the last call to getKeyStates(). Each key's bit value in the mask will be 1 if it's currently down, or has been pressed since the last call, otherwise it will be 0. For example:

int state = getKeyStates(); if( ( state & FIRE_PRESSED ) != 0 ){ // user has pressed the FIRE key }

This lets the application check the key state in a tight loop and respond quickly to any changes. Note that key events are still received by the game canvas as usual, except that you can suppress events involving the action keys - and customarily you do. By contrast, you never suppress key events for triggering commands. To paint itself smoothly, a game canvas uses a technique called double buffering: You perform drawing operations in an off-screen buffer, then quickly copy from the buffer to the visible area of the canvas. The canvas automatically creates and manages the off-screen buffer. The application draws into the buffer using a Graphics instance obtained from getGraphics(). (Note that each call returns a new instance, so you should call the method once outside the game loop and save the reference.) To update the display after drawing, a call to flushGraphics() will force an immediate repaint that's based on the current contents of the off-screen buffer. The second example is a game canvas that scrolls a randomly generated star field:
import java.util.Random; import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.GameCanvas; // A simple example of a game canvas that displays // a scrolling star field. The UP and DOWN keys // speed up or slow down the rate of scrolling. public class StarField extends GameCanvas implements Runnable { private static final int SLEEP_INCREMENT = 10; private static final int SLEEP_INITIAL = 150; private static final int SLEEP_MAX = 300; private private private private Graphics Random int volatile Thread graphics; random; sleepTime = SLEEP_INITIAL; thread;

public StarField(){ super( true ); graphics = getGraphics();

graphics.setColor( 0, 0, 0 ); graphics.fillRect( 0, 0, getWidth(), getHeight() ); } // When the game canvas is hidden, stop the thread. protected void hideNotify(){ thread = null; } // The game loop. public void run(){ int w = getWidth(); int h = getHeight() - 1; while( thread == Thread.currentThread() ){ // Increment or decrement the scrolling interval // based on key presses int state = getKeyStates(); if( ( state & DOWN_PRESSED ) != 0 ){ sleepTime += SLEEP_INCREMENT; if( sleepTime > SLEEP_MAX ) sleepTime = SLEEP_MAX; } else if( ( state & UP_PRESSED ) != 0 ){ sleepTime -= SLEEP_INCREMENT; if( sleepTime < 0 ) sleepTime = 0; } // Repaint the screen by first scrolling the // existing starfield down one and painting in // new stars... graphics.copyArea( 0, 0, w, h, 0, 1, Graphics.TOP | Graphics.LEFT ); graphics.setColor( 0, 0, 0 ); graphics.drawLine( 0, 0, w, 1 ); graphics.setColor( 255, 255, 255 ); for( int i = 0; i < w; ++i ){ int test = Math.abs( random.nextInt() ) % 100; if( test < 4 ){ graphics.drawLine( i, 0, i, 0 ); } }

flushGraphics(); // Now wait... try { Thread.currentThread().sleep( sleepTime ); } catch( InterruptedException e ){ } } } // When the canvas is shown, start a thread to // run the game loop. protected void showNotify(){ random = new Random(); thread = new Thread( this ); thread.start(); } }

Building a Java ME Canvas based calendar/date picker


From Forum Nokia Wiki
Reviewer Approved

Contents
[hide]

1 Introduction 2 Source code: CalendarWidget class 3 Sample usage 4 Resources

Introduction
Here's a calendar component usable on J2ME Canvas. You can use it, for example, as a simple and intuitive date picker.

You can see this component in action here: Calendar Widget in action.

Source code: CalendarWidget class


This class represents the Calendar itself.
import java.util.Calendar; import java.util.Date; import javax.microedition.lcdui.Canvas;

import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; public class CalendarWidget { }

Let's define some first variables to hold labels we'll use in our calendar: month and week day labels
static final String[] MONTH_LABELS = new String[]{ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; static final String[] WEEKDAY_LABELS = new String[]{ "M", "T", "W", "T", "F", "S", "S" };

Now we'll define some appearance-related properties, that will be customizable to make the date picker appear as you prefer.
/* starting week day: 0 for monday, 6 for sunday */ public int startWeekday = 0; /* elements padding */ public int padding = 1; /* cells border properties */ public int borderWidth = 4; public int borderColor = 0x0000ff; /* weekday labels properties */ public Font weekdayFont = Font.getDefaultFont(); public int weekdayBgColor = 0x0000ff; public int weekdayColor = 0xffffff; /* header (month-year label) properties */ public Font headerFont = Font.getDefaultFont(); public int headerBgColor = 0x0000ff; public int headerColor = 0xffffff; /* cells properties */ public Font font = Font.getDefaultFont(); public int foreColor = 0x000000; public int bgColor = 0x9999ff; public int selectedBgColor = 0xffff00; public int selectedForeColor = 0xff0000;

Now we'll define some internal properties that we'll use to handle size/time properties.
/* internal properties */ int width = 0; int height = 0; int headerHeight = 0;

int weekHeight = 0; int cellWidth = 0; int cellHeight = 0; /* internal time properties */ long currentTimestamp = 0; Calendar calendar = null; int weeks = 0;

Now, let's define a simple constructor, accepting a Date instance as only argument
public CalendarWidget(Date date) { calendar = Calendar.getInstance(); //we'll see these 2 methods later setDate(date); initialize(); }

Then, we'll define some useful methods to set and get the selected date
public Date getSelectedDate() { return calendar.getTime(); } public void setDate(Date d) { currentTimestamp = d.getTime(); calendar.setTime(d); //weeks number can change, depending on week starting day and month total days this.weeks = (int)Math.ceil(((double)getStartWeekday() + getMonthDays()) / 7); } public void setDate(long timestamp) { setDate(new Date(timestamp)); }

Here 2 methods we'll use to initialize calendar properties. This methods will be called by constructor, and should be called each time you change some public properties (like padding, fonts or border width) so that sizes can be correctly recalculated.
void initialize() { //let's initialize calendar size this.cellWidth = font.stringWidth("MM") + 2 * padding; this.cellHeight = font.getHeight() + 2 * padding; this.headerHeight = headerFont.getHeight() + 2 * padding;

this.weekHeight = weekdayFont.getHeight() + 2 * padding; this.width = 7 * (cellWidth + borderWidth) + borderWidth; initHeight(); } void initHeight() { this.height = headerHeight + weekHeight + this.weeks * (cellHeight + borderWidth) + borderWidth; }

These 2 methods will be used to get, respectively, the number of days and the start week day of the current month.
int getMonthDays() { int month = calendar.get(Calendar.MONTH); switch(month) { case 3: case 5: case 8: case 10: return 30; case 1: return calendar.get(Calendar.YEAR) % 4 == 0 && calendar.get(Calendar.YEAR) % 100 != 0 ? 29 : 28; default: return 31; } } int getStartWeekday() { //let's create a new calendar with same month and year, but with day 1 Calendar c = Calendar.getInstance(); c.set(Calendar.MONTH, calendar.get(Calendar.MONTH)); c.set(Calendar.YEAR, calendar.get(Calendar.YEAR)); c.set(Calendar.DAY_OF_MONTH, 1); return (c.get(Calendar.DAY_OF_WEEK) + 5) % 7; }

Now we must handle key events, to allow user to select the date they want. We handle left/right keys shifting current date by one day, while up/down keys will shift the current date by 7 days.
public void keyPressed(int key) { switch(key) { case Canvas.UP:

go(-7); break; case Canvas.DOWN: go(7); break; case Canvas.RIGHT: go(1); break; case Canvas.LEFT: go(-1); break; } } void go(int delta) { int prevMonth = calendar.get(Calendar.MONTH); setDate(currentTimestamp + 86400000 * delta); //we have to check if month has changed //if yes, we have to recalculate month height //since weeks number could be changed if(calendar.get(Calendar.MONTH) != prevMonth) { initHeight(); } }

Now that we've finished with utility functions, we can finally paint our date picker!
public void paint(Graphics g) { //painting background g.setColor(bgColor); g.fillRect(0, 0, width, height); //painting header (month-year label) g.setFont(headerFont); g.setColor(headerColor); g.drawString(MONTH_LABELS[calendar.get(Calendar.MONTH)] + " " + calendar.get(Calendar.YEAR), width / 2, padding, Graphics.TOP | Graphics.HCENTER); //painting week days labels g.translate(0, headerHeight); g.setColor(weekdayBgColor); g.fillRect(0, 0, width, weekHeight); g.setColor(weekdayColor); g.setFont(weekdayFont); for(int i = 0; i < 7; i++) { g.drawString(WEEKDAY_LABELS[(i + startWeekday) % 7], borderWidth + i * (cellWidth + borderWidth) +

cellWidth / 2, padding, Graphics.TOP | Graphics.HCENTER ); } //painting cells borders g.translate(0, weekHeight); g.setColor(borderColor); for(int i = 0; i <= weeks; i++) { g.fillRect(0, i * (cellHeight + borderWidth), width, borderWidth); } for(int i = 0; i <= 7; i++) { g.fillRect(i * (cellWidth + borderWidth), 0, borderWidth, height - headerHeight - weekHeight); } //painting days int days = getMonthDays(); int dayIndex = (getStartWeekday() - this.startWeekday + 7) % 7; g.setColor(foreColor); int currentDay = calendar.get(Calendar.DAY_OF_MONTH); for(int i = 0; i < days; i++) { int weekday = (dayIndex + i) % 7; int row = (dayIndex + i) / 7; int x = borderWidth + weekday * (cellWidth + borderWidth) + cellWidth / 2; int y = borderWidth + row * (cellHeight + borderWidth) + padding; //if this is the current day, we'll use selected bg and fore colors if(i + 1 == currentDay) { g.setColor(selectedBgColor); g.fillRect( borderWidth + weekday * (cellWidth + borderWidth), borderWidth + row * (cellHeight + borderWidth), cellWidth, cellHeight); g.setColor(selectedForeColor); } g.drawString("" + (i + 1), x, y, Graphics.TOP | Graphics.HCENTER);

//if this is the current day, we must restore standard fore color if(i + 1 == currentDay) { g.setColor(foreColor); } } //let's translate back! g.translate(0, - headerHeight - weekHeight); }

Sample usage
Here's how you can initialize and customize your Canvas based date picker:
calendar = new CalendarWidget(new Date()); calendar.headerFont = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_LARGE); calendar.weekdayFont = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_MEDIUM); calendar.weekdayBgColor = 0xccccff; calendar.weekdayColor = 0x0000ff; calendar.headerColor = 0xffffff; calendar.initialize();

This example illustrates how to create a game using GameCanvas class. In this example we are extending GameCanvasclass to draw the circle and rotate the circle continuously. The GameCanvasclass has following methods:

flushGraphics():- This is the void type method, it flushes to display on the off-screen buffer. flushGraphics(int x, int y, int width, int height):- This is the void type method, it flushes to display of specified region on the off-screen buffer. getGraphics():- This is used to get the graphics objects. getKeyStates():- This is the integer type variable, it is used to find the states of the key. paint(Graphics g):- This is also the void type method, it is used to paint the canvas.

Other commands, input event, etc methods inherited from Canvas class. The Canvas class has following methods:

getGameAction(int keyCode) getHeight() getKeyCode(int gameAction) getKeyName(int keyCode) getWidth() hasPointerEvents() hasPointerMotionEvents() hasRepeatEvents() hideNotify() isDoubleBuffered() keyPressed(int keyCode) keyReleased(int keyCode) keyRepeated(int keyCode) paint(Graphics g) pointerDragged(int x, int y) pointerPressed(int x, int y) pointerReleased(int x, int y) repaint() repaint(int x, int y, int width, int height) serviceRepaints() showNotify()

Source Code of CanvasGame.java


import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; import javax.microedition.midlet.*; public class CanvasGame extends MIDlet{ private Command back;

private Display display; final SweepGame game = new SweepGame(); public void startApp() { back = new Command("Back", Command.BACK, 0); game.start(); game.addCommand(back); game.setCommandListener(new CommandListener(){ public void commandAction(Command c, Displayable s) { game.stop(); notifyDestroyed(); } }); display = Display.getDisplay(this); display.setCurrent(game); } public void pauseApp() {} public void destroyApp(boolean unconditional) {} } class SweepGame extends GameCanvas implements Runnable { private boolean move; private int radius; private int diameter; private int interval; public SweepGame() { super(true); radius = 0; diameter = 10; interval = 0; } public void start() { move = true; Thread t = new Thread(this); t.start(); } public void stop() { move = false; } public void render(Graphics g) { int width = getWidth(); int height = getHeight(); g.setColor(183,251,121); g.fillRect(0, 0, width - 1, height - 1); int x = diameter; int y = diameter; int w = width - diameter * 2; int h = height - diameter * 2; for (int i = 0; i < 17; i=i+2) { g.setColor(((17 - i) * 15 - 7),20,((17 - i) * 15 - 7)); g.fillArc(x, y, w, h, radius + i * 10, 10); g.fillArc(x, y, w, h, (radius + 180) % 360 + i * 10, 10); } }

public void run() { Graphics g = getGraphics(); while (move) { radius = (radius + 1) % 360; render(g); flushGraphics(); try { Thread.sleep(interval); } catch (InterruptedException ie) {} } } }

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

Animation
With animation, there are normally two main problems: Display flickering and synchronization of painting with calculation of new frames. We will first address how to get the actual painting and application logic in sync, and then solve possible flickering.

Synchronization of Frame Calculation and Drawing


When you perform animations, you can first calculate the display content and then call repaint() in order to paint the new frame. But how do you know that the call

to paint() has finished? One possibility would be to callserviceRepaints(), which blocks until all pending display updates are finished. The problem withserviceRepaints() is that paint() may be called from another thread. If the thread callingserviceRepaints() holds any locks that are required in paint(), a deadlock may occur. Also, calling serviceRepaints() makes sense only from a thread other than the event handling thread. Otherwise, key events may be blocked until the animation is over. An alternative toserviceRepaints() is calling callSerially() at the end of the paint() method. ThecallSerially() method lets you put Runnable objects in the event queue. The run() method of the Runnable object is then executed serially like any other event handling method. In the run()method, the next frame can be set up, and a new repaint can be requested there. To demonstrate this execution model, you will build a simple stopwatch that counts down a given number of seconds by showing a corresponding pie slice using the fillArc() method, as shown in Figure 3.21.

Post a comment Email Article Print Article Share Articles

Figure 3.21 A very simple stopwatch. The Canvas implementation stores the current slice in degree, the start time, the total amount of seconds and the MIDlet display in local variables. In order to make use of callSerially(), your Canvasimplements the Runnable interface: class StopWatchCanvas extends Canvas implements Runnable { int degree = 360; long startTime; int seconds; Display display; When the StopWatchCanvas is created, you store the given display and seconds. Then, the current time is determined and stored, too: StopWatchCanvas (Display display, int seconds) { this.display = display; this.seconds = seconds;

startTime = System.currentTimeMillis (); } In the paint() method, you clear the display. If you need to draw more than 0 degrees, you fill a corresponding arc with red color and request recalculation of the pie slice using callSerially(). Finally, you draw the outline of the stopwatch by setting the color to black and calling drawArc(): public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g.setColor (255, 0, 0); g.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g.setGrayScale (0); g.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360); } This method is invoked by the event handling thread as a result of the previousdisplay.callSerially(this) statement. In this case, it just calculates a new pie slice and requests a repaint(): public void run () { int permille = (int) ((System.currentTimeMillis () - startTime) / seconds); degree = 360 - (permille * 360) / 1000; repaint (); } } As always, you need a MIDlet to actually display your StopWatchCanvas implementation. The following code creates a stopwatch set to 10 seconds when the application is started: import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class StopWatch extends MIDlet { public void startApp () { Display display = Display.getDisplay (this); display.setCurrent (new StopWatchCanvas (display, 10)); } public void pauseApp () { } public void destroyApp (boolean forced) {

} }

Avoiding Flickering
On some devices, the stopwatch implementation will flicker. This is due to the fact that the display is cleared completely before a new stopwatch is drawn. However, on some other devices, the stopwatch will not flicker because those devices provide automated double buffering. Before the screen is updated, all drawing methods are performed in a hidden buffer area. Then, when the paint() method is finished, the complete display is updated from the offscreen buffer at once. The method isDoubleBuffered()in the Canvas class is able to determine whether the device screen is double buffered. In order to avoid flickering of your animation in all cases, you can add your own offscreen image, which is allocated only if the system does not provide double buffering: Image offscreen = isDoubleBuffered () ? null : Image.createImage (getWidth (), getHeight ()); In the paint() method, you just check if the offscreen image is not null, and if so, you delegate all drawing to your offscreen buffer. The offscreen buffer is then drawn immediately at the end of thepaint() method, without first clearing the screen. Clearing the screen is not necessary in that case since the offscreen buffer was cleared before drawing and it fills every pixel of the display: public void paint (Graphics g) { Graphics g2 = offscreen == null ? g : offscreen.getGraphics (); g2.setGrayScale (255); g2.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g2.setColor (255, 0, 0); g2.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g2.setGrayScale (0); g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360); if (offscreen != null) g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT); } Listing 3.3 gives the complete source code for the buffered stopwatch.

Listing 3.3 BufferedStopWatch.java The Complete Source Code of the Buffered Stopwatch
import javax.microedition.midlet.*;

import javax.microedition.lcdui.*; class BufferedStopWatchCanvas extends Canvas implements Runnable { int degree = 360; long startTime; int seconds; Display display; Image offscreen; BufferedStopWatchCanvas (Display display, int seconds) { this.display = display; this.seconds = seconds; if (!isDoubleBuffered () && false) offscreen = Image.createImage (getWidth (), getHeight ()); startTime = System.currentTimeMillis (); } public void paint (Graphics g) { Graphics g2 = offscreen == null ? g : offscreen.getGraphics (); g2.setGrayScale (255); g2.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g2.setColor (255, 0, 0); g2.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g2.setGrayScale (0); g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360); if (offscreen != null) g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT); } public void run () { int permille = (int) ((System.currentTimeMillis () - startTime) / seconds); degree = 360 - (permille * 360) / 1000; repaint (); }

public class BufferedStopWatch extends MIDlet { public void startApp () { Display display = Display.getDisplay (this); display.setCurrent (new BufferedStopWatchCanvas (display, 10)); } public void pauseApp () { } public void destroyApp (boolean forced) { } }

This Example shows how to use of timer class. In this example we are using the Timer class to create the time of execution of application and the TimerTask class is used to create the counter of timer which is executed after 5000 ms as below:

private class TestTimerTask extends TimerTask{ public final void run(){ form.append("Timer Execute Count: " + ++count + "\n"); } }

in the above source code we execute the counter of the timer on the mobile window as follows:

TimerMIDlet.java
import java.util.*; import javax.microedition.midlet.*;

import javax.microedition.lcdui.*; public class TimerMIDlet extends MIDlet implements CommandListener{ private Display display; private Form form; private Command exit, stop; private Timer timer; private TestTimerTask task; private int count = 0;

public TimerMIDlet(){ display = Display.getDisplay(this); form = new Form("Timer Example"); exit = new Command("Exit", Command.EXIT, 1); stop= new Command("Stop", Command.STOP, 2); form.append("Please wait for timer.. \n"); form.addCommand(exit); form.addCommand(stop); form.setCommandListener(this); } public void startApp (){ timer = new Timer(); task = new TestTimerTask(); timer.schedule(task,5000); display.setCurrent(form); } public void pauseApp (){ } public void destroyApp (boolean unconditional){ notifyDestroyed(); } public void commandAction(Command c, Displayable d){ String label = c.getLabel(); if (label.equals("Stop")){ timer.cancel(); }else if (label.equals("Exit")) { destroyApp(true); } } private class TestTimerTask extends TimerTask{ public final void run(){ form.append("Timer Execute Count: " + ++count + "\n"); } } }

In this J2ME application, we are going to read the specified file. This example shows you how to read the data of the specified file. To implement this type of logic we need to create a text file in the res folder of the directory. In this application we are creating a "help.txt" file and read the data of this file by the help of j2me midlet.

For Details you can download the source code and use it on j2me environment.

ReadFile.java
import java.io.*; import javax.microedition.midlet.*;

import javax.microedition.lcdui.*; public class ReadFile extends MIDlet implements CommandListener{ private Display display; private Form form; private Command read, exit; private Alert alert; public ReadFile(){ display = Display.getDisplay(this); read = new Command("Read", Command.SCREEN, 1); exit = new Command("Exit", Command.EXIT, 1); form = new Form("Read File"); form.addCommand(exit); form.addCommand(read); form.setCommandListener(this); } public void startApp(){ display.setCurrent(form); } public void pauseApp(){} public void destroyApp(boolean unconditional){ notifyDestroyed(); } public void commandAction(Command c, Displayable s){ String label = c.getLabel(); if (label.equals("Read")){ String string = file(); if (string != null){ alert = new Alert("Reading", string, null, null); alert.setTimeout(Alert.FOREVER); display.setCurrent(alert, form); } } else if (label.equals("Exit")){ destroyApp(false); } } private String file(){ InputStream is = getClass().getResourceAsStream("help.txt"); StringBuffer sb = new StringBuffer(); try{ int chars, i = 0; while ((chars = is.read()) != -1){ sb.append((char) chars); } return sb.toString(); }catch (Exception e){} return null; } }

n this application we are going to generate the random number using Random class. The Random class within the java.util package and we can use it by importing the java.util.Random package.

public void random(){ Random number = new Random(); float f = number.nextFloat(); number.setSeed(System.currentTimeMillis()); item.setText(""+(f*100.0f)%100); }

In the above source code nextFloat returns the next pseudorandom, uniformly distributed float value from this random number generator's sequence and the setSeed method is used to sets the seed of this random number generator using a single long seed and finally by the using of canvas class we have to display the random number on the mobile window. The Application is as follows:

RandomNumber.java
import java.util.*; import javax.microedition.lcdui.*;

import javax.microedition.midlet.*; public class RandomNumber extends MIDlet implements CommandListener{ private Display display; private Command exit, generate; private StringItem item; private Form form; public RandomNumber(){ display=Display.getDisplay(this); form = new Form("RandomNumber"); exit = new Command("Exit", Command.EXIT,0); generate = new Command("Generate", Command.OK,1); item = new StringItem("Number ",""); form.addCommand(exit); form.addCommand(generate); form.setCommandListener(this); form.append("This Random Number Generated By Mr. Sandeep Kumar Suman, Software Developer, Roseindia Technology Pvt Ltd. Mobile No:+919313985248"); form.append(item); } public void startApp(){ display.setCurrent(form); } public void pauseApp(){} public void destroyApp(boolean unconditional){ notifyDestroyed(); } public void commandAction(Command c, Displayable d){ String label = c.getLabel(); if(label.equals("Exit")){ destroyApp(false); }else if(label.equals("Generate")){ random(); } } public void random(){ Random number = new Random(); float f = number.nextFloat(); number.setSeed(System.currentTimeMillis()); item.setText(""+(f*100.0f)%100); } }

With the help of the canvas class, we can draw as many as graphics we want in our application. Similarly we can also set the different alignment to the text. In this J2ME Midlet we are going to set the text at different locations

such as right, left, top right and top left etc.. to set the alignment we have used "g.drawString();" and defined different values in it with the height and width within it. Just find out the different methods that are used in this small j2me example..

int width = getWidth(); int height = getHeight(); g.setColor(0, 0, 255); g.fillRect(0, 0, width, height); g.setColor(255, 0, 0); g.drawString("TOP_LEFT", 0, 0, Graphics.TOP | Graphics.LEFT); g.drawString("TOP_RIGHT", width, 0, Graphics. TOP | Graphics.RIGHT); g.drawString("BOTTOM_LEFT", 0, height, Graphics.BOTTOM | Graphics.LEFT); g.drawString("BOTTOM_RIGHT", width, height, Graphics.BOTTOM | Graphics.RIGHT); g.drawString("BASELINE_HCENTER", width / 2, height / 2, Graphics.BASELINE | Graphics.HCENTER);

The Application is going to look like as follows:

Source Code of CornerText.java


import import import import import import import javax.microedition.lcdui.*; javax.microedition.midlet.*; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable;

import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import javax.microedition.midlet.MIDlet; public class CornerText extends MIDlet{ private Display display; public void startApp(){ Canvas canvas = new CanvasText(); display = Display.getDisplay(this); display.setCurrent(canvas); } public void pauseApp(){} public void destroyApp(boolean unconditional){} } class CanvasText extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); g.setColor(0, 0, 255); g.fillRect(0, 0, width, height); g.setColor(255, 0, 0); g.drawString("TOP_LEFT", 0, 0, Graphics.TOP | Graphics.LEFT); g.drawString("TOP_RIGHT", width, 0, Graphics. TOP | Graphics.RIGHT); g.drawString("BOTTOM_LEFT", 0, height, Graphics.BOTTOM | Graphics.LEFT); g.drawString("BOTTOM_RIGHT", width, height, Graphics.BOTTOM | Graphics.RIGHT); g.drawString("BASELINE_HCENTER", width / 2, height / 2, Graphics.BASELINE | Graphics.HCENTER); } }

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&

Pointer Example
This type of example is basically used in touch screen technology, here we are trying to create a pointer which location is based on the screen touch pointer. The source code for this small example of touch screen application.

public void pressPointer (int x, int y) { action = "Pointer Pressed"; this.x = x; this.y = y; repaint (); }

is used to initialize the pointer location when pressed. The source code
public void releasePointer (int x, int y) { action = "Pointer Released"; this.x = x; this.y = y; repaint (); }

is used to released the pointer from his location. And the source code
public void dragPointer (int x, int y) { action = "Pointer Repeated"; this.x = x; this.y = y; repaint (); }

is used when repeating the pointer, then it relocate the location of the pointer. The Application is as follows:

Source Code of PointerExample.java


import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class PointerExample extends MIDlet{ private Display display;

public void startApp(){ display = Display.getDisplay(this); display.setCurrent (new PointerCanvas()); } public void pauseApp(){} public void destroyApp (boolean forced){} } class PointerCanvas extends Canvas { String action = "Press Pointer!"; int x; int y; public void pressPointer (int x, int y) { action = "Pointer Pressed"; this.x = x; this.y = y; repaint (); } public void releasePointer (int x, int y) { action = "Pointer Released"; this.x = x; this.y = y; repaint (); } public void dragPointer (int x, int y) { action = "Pointer Repeated"; this.x = x; this.y = y; repaint (); } public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth(), getHeight()); g.setGrayScale (0); g.drawString (action + " " + x + "/" + y, 0, 0, Graphics.TOP|Graphics.LEFT); g.drawLine (x-4, y, x+4, y); g.drawLine (x, y-4, x, y+4); } }

Different Size of Font MIDlet Example

This example simply shows the different size of font. There are three attributes are used for the different font sizes. That are:
Font.SIZE_LARGE); Font.SIZE_MEDIUM); Font.SIZE_SMALL);

The SIZE_LARGE is used to large size of font, the SIZE_MEDIUM is used to medium size of font and the SIZE_SMALL is used to small size of font, which shows in the mobile figure below:

Source Code of FontSize.java


import import import import java.io.*; java.lang.*; javax.microedition.io.*; javax.microedition.rms.*;

import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class FontSize extends MIDlet { public static final boolean COLOR = false; public static final boolean DEBUG = false; private Display display = null; private FontCanvas fontCanvas = null; private boolean painting = false; public FontSize() { display = Display.getDisplay(this); fontCanvas = new FontCanvas(this); } public void startApp() throws MIDletStateChangeException { display.setCurrent(fontCanvas); } public void pauseApp() {} protected void destroyApp(boolean unconditional) throws MIDletStateChangeException {} class FontCanvas extends Canvas { private FontSize parent = null; private int width = getWidth(); private int height = getHeight(); public FontCanvas(FontSize parent) { this.parent = parent; } public void paint(Graphics g) { g.setColor(255, 128, 0); g.fillRect(0, 0, width, height); Font font1 = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_LARGE); Font font2 = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_MEDIUM); Font font3 = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL); int position = 0; if(COLOR){ g.setColor(255, 255, 255); }else{ g.setColor(192, 192, 192); g.fillRect(0, position, width, font1.getHeight()); } if(COLOR){ g.setColor(255, 255, 255); }else{ g.setColor(0, 0, 0); }

g.setFont(font1); g.drawString("LARGE SIZE FONT", 0, position, Graphics.LEFT | Graphics.TOP); position = position + font1.getHeight() + 10; g.setFont(font2); g.drawString("MEDIUM SIZE FONT", 0, position, Graphics.LEFT | Graphics.TOP); g.setColor(0, 0, 0); position = position + font1.getHeight() + 10; g.setFont(font3); g.drawString("SMALL SIZE FONT", 0, position, Graphics.LEFT | Graphics.TOP); position = position + font1.getHeight() + 10; g.drawLine(0, font3.getHeight() + position - 1, width, font3.getHeight() + position - 1); painting = false; } } }

********************************************

MIDP Programming with J2ME


Images
The Graphics class also provides a method for drawing images. As shown in the final version of TeleTransfer application, Images can be predefined and contained in the JAR file of the MIDlet. The only file format that is mandatory for MIDP is the Portable Network Graphics (PNG) file format. The PNG format has several advantages over other graphics formats; for example, it is license free and supports true color images, including a full transparency (alpha) channel. PNG images are always compressed with a loss-less algorithm. The algorithm is identical to the algorithm used for JAR files, so the MIDP implementation can save space by using the same algorithm for both purposes. An image can be loaded from the JAR file using the static method Image.create (String name). The name parameter denotes the filename of the image in the JAR file. Please note that this create() method may throw an IOException. The drawImage() method in Graphics requires an Image object, the coordinates, and an integer denoting the alignment as parameters. The alignment parameter is similar the alignment of drawString(), except that the BASELINE constant is not supported. An additional alignment constant available for images only is VCENTER, which forces the image to be vertically centered relative to the given coordinates. Figure 3.16 shows the valid constant combinations and the corresponding anchor points.

Figure 3.16 Alignment constant combinations valid for images and the corresponding anchor points. The following example first loads the image logo.png from the MIDlet JAR file in the constructor, and then displays the image three times. One image is drawn in the upper-left corner, one in the lower-right corner, and one in the center of the display, as shown in Figure 3.17:
import java.io.*; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; class ImageDemoCanvas extends Canvas { Image image; public ImageDemoCanvas () { try { image = Image.createImage ("/logo.png"); } catch (IOException e) { throw new RuntimeException ("Unable to load Image: "+e); } } public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ());

g.drawImage (image, 0, 0, Graphics.TOP | Graphics.LEFT); g.drawImage (image, getWidth () / 2, getHeight () / 2, Graphics.HCENTER | Graphics.VCENTER); g.drawImage (image, getWidth (), getHeight (), Graphics.BOTTOM | Graphics.RIGHT); } }

Figure 3.17 Output of the ImageDemo example. Images can also be created at runtime from scratch. The static method Image.create (int width, int height) creates a new dynamic image of the given size. In contrast to images loaded from a JAR file, these images are mutable. Mutable images can be modified by calling getGraphics (). The Graphics object returned can be used for modifying the image with all the methods provided by the Graphics class. Please note that images loaded from a JAR file cannot be modified. However, it is possible to create a mutable image, and then draw any other image in the mutable image. By modifying the constructor of the previous example canvas as follows, the image drawn in the paint() method is created and filled at runtime instead of loading an image from the JAR file:
public ImageDemoCanvas () { image = Image.createImage (10,10); image.getGraphics ().fillArc (0,0,10,10,0, 360); }

The disadvantage of mutable images is that they cannot be used in high-level GUI elements since it is possible to modify them at any time, possibly leading to inconsistent display of widgets. For that reason, another static create method, createImage(Image image), is provided that creates an immutable image from another image.

Interaction
Because the Canvas class is a subclass of Displayable, it provides the same support for commands as the high-level screen classes. Here, you will concentrate on the additional interaction possibilities the Canvas class offers: direct key input and pointer support.

Please note that all input events and command notifications and the paint() method are called serially. That means that the application manager will call none of the methods until the previous event handling method has returned. So all these methods should return quickly, or the user will be unable to interact with the application. For longer tasks, a separate thread can be started.

Key Input
For key input, the Canvas class provides three callback methods: keyPressed(), keyReleased(), and keyRepeated(). As the names suggest, keyPressed() is called when a key is pressed, keyRepeated() is called when the user holds down the key for a longer period of time, and keyReleased() is called when the user releases the key. All three callback methods provide an integer parameter, denoting the Unicode character code assigned to the corresponding key. If a key has no Unicode correspondence, the given integer is negative. MIDP defines the following constant for the keys of a standard ITU-T keypad: KEY_NUM0, KEY_NUM1, KEY_NUM2, KEY_NUM3, KEY_NUM4, KEY_NUM5, KEY_NUM6, KEY_NUM7, KEY_NUM8, KEY_NUM9, KEY_POUND, and KEY_STAR. Applications should not rely on the presence of any additional key codes. In particular, upper- and lowercase or characters generated by pressing a key multiple times are not supported by low-level key events. A "name" assigned to the key can be queried using the getKeyName() method. Some keys may have an additional meaning in games. For this purpose, MIDP provides the constants UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C, and GAME_D. The "game" meaning of a keypress can be determined by calling the getGameAction() method. The mapping from key codes to game actions is device dependent, so different keys may map to the same game action on different devices. For example, some devices may have separate cursor keys; others may map the number pad to four-way movement. Also, several keys may be mapped to the same game code. The game code can be translated back to a key code using the getKeyCode() method. This also offers a way to get the name of the key assigned to a game action. For example, the help screen of an application may display
"press "+getKeyName (getKeyCode (GAME_A))

instead of "press GAME_A". The following canvas implementation shows the usage of the key event methods. For each key pressed, repeated, or released, it shows the event type, character and code, key name, and game action. The first part of the implementation stores the event type and code in two variables and schedules a repaint whenever a key event occurs:
import javax.microedition.lcdui.*;

class KeyDemoCanvas extends Canvas { String eventType = "- Press any!"; int keyCode; public void keyPressed (int keyCode) { eventType = "pressed"; this.keyCode = keyCode; repaint (); } public void keyReleased (int keyCode) { eventType = "released"; this.keyCode = keyCode; repaint (); } public void keyRepeated (int keyCode) { eventType = "repeated"; this.keyCode = keyCode; repaint (); }

The second part prints all event properties available to the device screen. For this purpose, you first implement an additional write() method that helps the paint() method to identify the current y position on the screen. This is necessary because drawText() does not advance to a new line automatically. The write() method draws the string at the given y position and returns the y position plus the line height of the current font, so paint() knows where to draw the next line:
public int write (Graphics g, int y, String s) { g.drawString (s, 0, y, Graphics.LEFT|Graphics.TOP); return y + g.getFont ().getHeight (); }

The paint() method analyzes the keyCode and prints the result by calling the write() method defined previously, as shown in Figure 3.18:
public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); int y = 0; y = write (g, y, "Key "+ eventType); if (keyCode == 0) return; y = write (g, y, "Char/Code: "+ ((keyCode < 0) ? "N/A" : "" +(char) keyCode) + "/" + keyCode); y = write (g, y, "Name: "+getKeyName (keyCode)); String gameAction; switch (getGameAction (keyCode)) {

case LEFT: gameAction = "LEFT"; break; case RIGHT: gameAction = "RIGHT"; break; case UP: gameAction = "UP"; break; case DOWN: gameAction = "DOWN"; break; case FIRE: gameAction = "FIRE"; break; case GAME_A: gameAction = "GAME_A"; break; case GAME_B: gameAction = "GAME_B"; break; case GAME_C: gameAction = "GAME_C"; break; case GAME_D: gameAction = "GAME_D"; break; default: gameAction = "N/A"; } write (g, y, "Action: "+gameAction); } }

Figure 3.18 Output of the KeyDemo example when the "Fire" key was released.

Pointer Events
For devices supporting a pointer device such as a stylus, touch screen, or trackball, the Canvas class provides three notification methods: pointerPressed(), pointerDragged(), and pointerReleased(). These methods work similarly to the key event methods, except that they provide two integer parameters, denoting the x and y position of the pointer when the corresponding event occurs. (Please note that pointer support is optional in MIDP, so the application should not rely on the presence of a pointer. Such devices are uncommon for devices such as mobile phones.) The following sample program demonstrates the usage of the three methods:
import javax.microedition.lcdui.*; class PointerDemoCanvas extends Canvas { String eventType = "Press Pointer!"; int x; int y; public void pointerPressed (int x, int y) { eventType = "Pointer Pressed"; this.x = x; this.y = y; repaint (); }

public void pointerReleased (int x, int y) { eventType = "Pointer Released"; this.x = x; this.y = y; repaint (); } public void pointerDragged (int x, int y) { eventType = "Pointer Repeated"; this.x = x; this.y = y; repaint (); } public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); g.drawString (eventType + " " +x +"/"+y, 0, 0, Graphics.TOP|Graphics.LEFT); g.drawLine (x-4, y, x+4, y); g.drawLine (x, y-4, x, y+4); } }

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

High-Level API
Now that you know the basics of the MIDlet's life cycle and general display model, we can start to look deeper into the lcdui package. We will start with another subclass of Screen: Alert. Then we will discuss some simple Items like StringItem and ImageItem. We will explain the use of more advanced Items such as TextField and ChoiceGroup by creating a simple TeleTransfer example application. As we introduce new MIDP high-level UI capabilities like other Screen subclasses, we will extend the TeleTransfer sample step by step.

Alerts
You already know the Form class from the first example. The simplest subclass of Screen is Alert. Alert provides a mechanism to show a dialog for a limited period of time. It consists of a label, text, and an optional Image. Furthermore, it is possible to set a

period of time the Alert will be displayed before another Screen is shown. Alternatively, an Alert can be shown until the user confirms it. If the Alert does not fit on the screen and scrolling is necessary to view it entire contents, the time limit is disabled automatically. The following code snippet creates an Alert with the title "HelloAlert" and displays it until it is confirmed by the user:
Alert alert = new Alert ("HelloAlert"); alert.setTimeout (Alert.FOREVER); display.setCurrent (alert);

Forms and Items


Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit o StumbleUpon o Facebook o FriendFeed o Furl o Newsvine o Google o LinkedIn o MySpace o Technorati o Twitter o Windows Live o YahooBuzz

The most important subclass of Screen is the class Form. A Form can hold any number of Items such as StringItems, TextFields, and ChoiceGroups. Items can be added to the Form using the append() method. The Item class itself is an abstract base class that cannot be instantiated. It provides a label that is a common property of all subclasses. The label can be set and queried using the setLabel()and getLabel() methods, respectively. The label is optional, and a null value indicates that the item does not have a label. However, several widgets switch to separate screens for user interaction, where the label is used as the title of the screen. In

order to allow the user to keep track of the program state, it is recommended that you provide a label at least for interactive items. neither be placed freely nor can their size be set explicitly. Unfortunately, it is not possible to implement Item subclasses with a custom appearance. The Form handles layout and scrolling automatically. Table 3.1 provides an overview of all Items available in MIDP.
Items can

Table 3.1 All Subclasses of Item


Item
ChoiceGroup DateField Gauge ImageItem StringItem TextField

Description Enables the selection of elements in group. Used for editing date and time information. Displays a bar graph for integer values. Used to control the layout of an Image. Used for read-only text elements. Holds a single-line input field.

StringItem
StringItems are simple read-only text elements that are initialized with the label and a text String parameter only. The following code snippet shows the creation of a simple HelloMidp

version label. After creation, the label is added to the main form in the constructor of the application:

public HelloMidp () { mainForm = new Form ("HelloMidp"); StringItem versionItem = new StringItem ("Version: ", "1.0"); mainForm.append (versionItem); }

The label of the StringItem can be accessed using the setLabel() and getLabel() methods inherited from Item. To access the text, you can use the methods setText() and getText().

ImageItem
Similar to the StringItem, the ImageItem is a plain non-interactive Item. In addition to the label, the ImageItem constructor takes an Image object, a layout parameter, and an alternative text string that is displayed when the device is not able to display the image. The image given to the constructor must be non-mutable. All images loaded from the

MIDlet

suite's JAR file are not mutable. (Details about adding resources to a JAR file are explained in Chapter 2, "The Connected Limited Device Configuration.")

The difference between mutable and non-mutable Images is described in more detail in the section about Images in the "Low Level API" section of this chapter. For now, we will treat the Image class as a "black box" that has a string constructor that denotes the location of the image in the JAR file. Please note that Image construction from a JAR file throws an IOException if the image cannot be loaded for some reason. The layout parameter is one of the integer constants listed in Table 3.2, where the newline constants can be combined with the horizontal alignment constants.

Table 3.2 ImageItem Layout Constants


Constant
LAYOUT_CENTER LAYOUT_DEFAULT

Value The image is centered horizontally. A device-dependent default formatting is applied to the image. The image is left-aligned. A new line will be started after the image is drawn. A new line will be started before the image is drawn. The image is aligned to the right.

LAYOUT_LEFT LAYOUT_NEWLINE_AFTER LAYOUT_NEWLINE_BEFORE LAYOUT_RIGHT

The following code snippet shows how a center aligned ImageItem is added to the HelloMidp sample MIDlet:
public HelloMidp () { display = Display.getDisplay (this); mainForm = new Form ("HelloMidp"); try { ImageItem logo = new ImageItem ("Copyright: ", Image.createImage ("/mcp.png"), ImageItem.LAYOUT_CENTER | ImageItem.LAYOUT_NEWLINE_BEFORE | ImageItem.LAYOUT_NEWLINE_AFTER, "Macmillian USA"); mainForm.append (logo); } catch (IOException e) { mainForm.append (new StringItem ("Copyright", "Sams Publishing; Image not available:" + e)); } }

By forcing a new line before and after the image, you ensure that the image is centered in its own line. Figure 3.3 shows the corresponding display on the device. If the image

cannot be loaded and an exception is thrown, a simple StringItem is appended to the form instead of the image.

Figure 3.3 The HelloMidp application showing an ImageItem.

Handling Textual Input in TextFields


As shown in Table 3.1, textual input is handled by the class TextField. The constructor of TextField takes four values: a label, initial text, a maximum text size, and constraints that indicate the type of input allowed. In addition to avoiding input of illegal characters, the constraints may also influence the keyboard mode. Several MIDP devices have a numeric keyboard only, and the constraints allow the application manager to switch the key assignments accordingly. The constants listed in Table 3.3, declared in the class TextField, are valid constraint values.

Table 3.3 TextField


Constant
ANY EMAILADDR NUMERIC PASSWORD PHONENUMBER URL

Constraint

Constant Values

Value Allows any text to be added. Adds a valid e-mail address, for instance myemail@mydomain.com. Allows integer values. Lets the user enter a password, where the entered text is masked. Lets the user enter a phone number. Allows a valid URL.

We will now show the usage of TextFields by creating a simple example Form for bank transfers. A bank transfer form contains at least the amount of money to be transferred and the name of the receiver. To start the implementation of the TeleTransfer MIDlet, you first need to import the two packages containing the midlet and lcdui classes:

import javax.microedition.midlet.*; import javax.microedition.lcdui.*;

Every MID application is derived from MIDlet, so you need to extend the MIDlet class, too:
public class TeleTransfer extends MIDlet {

Because you want to create a Form that contains Items for entering the transfer information, you need a corresponding member variable mainForm. You can already initialize the variable at its declaration because it has no dependencies from constructor parameters:
Form mainForm = new Form ("TeleTransfer");

In order to let the user enter the transfer information, add TextFields for the name of the receiver for entering the amount to be transferred. Because of the lack of floating-point values in the CLDC, the numeric TextFields in MIDP can hold integer values only. So you need to split the amount into separate fields for dollars and cents. An alternative would be to use an alphanumeric field and parse the string into two separate values. However, this may result in switching the keyboard to alpha mode on cell phones, making numeric input unnecessarily complicated. In this case, you'll limit the size of possible values to five digits for the whole dollar part and two digits for the fractional cent part. Again, you initialize the variables where they are declared:
TextField receiverName = new TextField ("Receiver Name", "", 20, TextField.ANY); TextField receiverAccount = new TextField ("Receiver Account#", "", 12, TextField.NUMERIC); TextField amountWhole = new TextField ("Dollar", "", 6, TextField.NUMERIC); TextField amountFraction = new TextField ("Cent", "", 2, TextField.NUMERIC);

Finally, you add a variable storing the Display instance for your application:
Display display = Display.getDisplay (this);

Now you can add the constructor to your application where you added the previous TextFields to the main form:
public TeleTransfer () { mainForm.append (receiverName); mainForm.append (receiverAccount); mainForm.append (amountWhole); mainForm.append (amountFraction); }

When the application is started, you request the display to show your money transfer form by calling setCurrent(). As explained in the "MIDlets" section, the application manager notifies you about the application start by calling the startApp() method. So you implement this method accordingly:
public void startApp () { display.setCurrent (mainForm); }

Please note that startApp() is called also when the MIDlet resumes from the paused state, so you cannot move the initialization code from the constructor to this method. Both pauseApp() and destroyApp() are declared as abstract in the MIDlet class, so you need to implement these methods in your application, even if you do not have real content for them. You just provide empty implementations, like in the HelloMidp example in the first section:
public void pauseApp () { } public void destroyApp (boolean unconditional) { }

Selecting Elements Using ChoiceGroups


In the previous section, you created a simple Form to enter information for transferring money between two accounts. Now you will extend the application to allow the user to select different currencies. For this purpose, you will now add a ChoiceGroup to your application. The ChoiceGroup is an MIDP UI widget enabling the user to choose between different elements in a Form. These elements consist of simple Strings, but can display an optional image per element as well. ChoiceGroups can be of two different types. Corresponding type constants are defined in the Choice interface. These constants are used in the List class as well; the List class allows an additional third type. The three type constants are listed in Table 3.4.

Table 3.4 Choice Type Constants


Constant
EXCLUSIVE

Value Specifies a ChoiceGroup or List having only one element selected at the same time. Valid for Lists only. It lets the List send Commands to indicate state changes. In contrast to EXPLICIT, MULTIPLE allows the selection of multiple elements.

IMPLICIT

MULTIPLE

The ChoiceGroup constructor requires at least a label and a type value. Additionally, a String array and an Image array containing the elements can be passed to the constructor. Elements can also be added dynamically using the append() method. The append() method has two parameters, a String for the label and an Image. In both cases, the image parameter may be null if no images are desired. In order to add a ChoiceGroup to the TeleTransfer application, you introduce a new variable currency of type ChoiceGroup. By setting the type to EXCLUSIVE, you get a ChoiceGroup where only one item can be selected at a time. You directly add elements for the United States (USD), the European Union (EUR), and Japan (JPY) by passing a String array created inline. The ChoiceGroup enables the user to choose between three currencies that are represented textually by the abbreviations specified in the String array. The last parameter of the constructor is set to null because you do not want Images to be displayed at this time:
ChoiceGroup currency = new ChoiceGroup ("Currency", Choice.EXCLUSIVE, new String[] {"USD", "EUR", "JPY"}, null);

You still need to add the currency ChoiceGroup to your main Form. As for the text fields, this is done via the append() method of Form:
mainForm.append (currency);

Figure 3.4 shows the TeleTransfer application extended to choose a currency using a ChoiceGroup.

Figure 3.4 The TeleTransfer MIDlet extended to enable the user to choose a currency.

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

Receiving Changes from Interactive UI Items


If you run the new version of the TeleTransfer MIDlet, you can change the currency using the ChoiceGroup, but the TextField labels for Dollar and Cent are not changed accordingly. You need a way to notify the application if a selection is made in the currency ChoiceGroup. Receiving changes of interactive high-level UI items in MIDP is based on a listener model similar to AWT. Classes implementing the ItemStateListener interface are able to receive notifications for the following events:

State changes of a ChoiceGroup Value adjustments of an interactive Gauge


o o o o

Post a comment Email Article Print Article Share Articles Digg del.icio.us Slashdot DZone Reddit StumbleUpon Facebook FriendFeed Furl Newsvine Google LinkedIn MySpace Technorati Twitter Windows Live YahooBuzz value changes

TextField DateField

changes

The events are sent to the method itemStateChanged() of the ItemStateListener, where the item that has changed is given as a parameter. In order to actually receive these events, the ItemStateChangeListener must be registered using the setItemStateListener() method of the corresponding Form.

Now that you know about item state change events, you can add the desired functionality to your TeleTransfer MIDlet. First, you need to add the ItemStateListener interface to the class declaration: public class TeleTransfer extends MIDlet implements ItemStateListener { You also need to implement a corresponding itemStateChanged() method. Since the itemStateChanged() method is called for changes of all Items in the Form, you need to check the item parameter indicating the event source first. If the source of the event is the currency ChoiceGroup, you set the labels of the amount and fraction TextFields correspondingly:
public void itemStateChanged (Item item) { if (item == currency) { int index = currency.getSelectedIndex (); switch (index) { case 0: amountWhole.setLabel ("Dollar"); amountFraction.setLabel ("Cent"); break; case 1: amountWhole.setLabel ("Euro"); amountFraction.setLabel ("Cent"); break; case 2: amountWhole.setLabel ("Yen"); amountFraction.setLabel ("Sen"); } } }

Just adding the interface and implementing the corresponding methods is not sufficient to enable the MIDlet to receive state changes. Additionally, you need to register your ItemStateListener at the Form containing the currency item. You do so by calling the setItemStateListener() method in the TeleTransfer constructor:
public TeleTransfer () { mainForm.append (senderAccount); ... mainForm.append (currency); mainForm.setItemStateListener (this); }

Figure 3.5 shows the new version of the TeleTransfer example, where the labels are changed depending on the state of the currency ChoiceGroup.

Figure 3.5 The TeleTransfer MIDlet extended to change the labels depending on the state of the currency ChoiceGroup.

Using Commands for User Interaction


Now you can enter all the information required for a telegraphic transfer, but you have no means to initiate the actual transfer. In contrast to desktop computers, which have plenty of screen space for displaying buttons or menus, a different approach is necessary for mobile devices. Some devices provide so-called soft buttons, which are buttons without fixed functionality that are assigned dynamically depending on the application context. The number of soft buttons may vary if they are available. Other mobile devices do not even have space for soft buttons, but provide scrolling menus. MIDP needs to abstract from the concrete device and to provide a mechanism that is suitable for all devices, independent of the availability and number of soft buttons. Thus, the lcdui package does not provide buttons or menus, but an abstraction called Command.
Commands can be added to all classes derived from the Displayable class. These classes are Screen and its subclasses such as Form, List, and TextBox for the high-level API and Canvas for the low-level API.

No positioning or layout information is passed to the Commandthe Displayable class itself is completely responsible for arranging the visible components corresponding to Commands on a concrete device. The only layout and display information that can be assigned to a Command except from the command label is semantic information. The semantic information consists of a type and a priority. The priority allows the device to decide which commands are displayed as soft buttons if the number of commands exceeds the number of soft buttons available. For additional commands not displayed as soft buttons, a separate menu is created automatically. The type information is an additional hint for the device about how to display the command. For example, if the Exit command is always assigned to the leftmost soft button in native applications of a certain device type, the MIDP implementation is able to make the same assignment. Thus, a consistent look and feel can be accomplished for a device. The available command type constants are listed in Table 3.5.

Table 3.5 Command Type Constants


Constant
Command.BACK

Value Used for navigation commands that are used to return the user to the previous Screen. Needed to notify the screen that a negative answer occurred. Used to specify a Command for exiting the application.

Command.CANCEL Command.EXIT

Command.HELP Command.ITEM

Passed when the application requests a help screen. A command type to tell the application that it is appended to an explicit item on the screen. Needed to notify the screen that a positive answer occurred. A type that specifies a screen-specific Command of the application. Interrupts a procedure that is currently running.

Command.OK Command.SCREEN Command.STOP

The Command constructor takes the label, the command type and the priority as input. The Command class provides read() methods for all these fields, but it is not possible to change the parameters after creation. Using the addCommand() method, commands can be added to a Form or any other subclass of Displayable. As in the case of receiving state changes of UI widgets, the MIDP uses a listener model for detecting command actions. For this purpose, the lcdui package contains the interface CommandListener. A CommandListener can be registered to any Displayable class using the setCommandListener method. After registration, the method commandAction() of the Commandlistener is invoked whenever the user issues a Command. In contrast to AWT, only one listener is allowed for each Displayable class. The commandAction() callback method provides the Displayable class where the command was issued and the corresponding Command object as parameters. With this information, you can extend your TeleTransfer application with the desired Commands. But before going into actual command implementation, you need to add some corresponding functionality. You'll add three commands: a Send command, a Clear command, and an Exit command. For Clear, you just add a method setting the content of the fields of your form to empty strings:
public void clear () { receiverName.setString (""); receiverAccount.setString (""); amountWhole.setString (""); amountFraction.setString (""); }

The Send command is a bit more difficult since you do not yet have the background to really submit information over the network. (Network connections will be handled in Chapter 6, "Networking: The Generic Connection Framework.") So you just display the content to be transmitted in an alert screen as a temporary replacement:
public void send () { Alert alert = new Alert ("Send"); alert.setString ("transfer " + amountWhole.getString () + "." + amountFraction.getString () + " " + amountWhole.getLabel () + "\nto Acc#" + receiverAccount.getString ()

+ "\nof " + receiverName.getString ()); alert.setTimeout (2000); display.setCurrent (alert); clear (); }

For leaving the application, the MIDlet already provides the notifyDestroyed() method, so you do not need to add anything here. Now that you have implemented the corresponding functionality, the next step is to add the actual Command objects to your application class:
static final Command sendCommand = new Command ("Send", Command.SCREEN, 1); static final Command clearCommand = new Command ("Clear", Command.SCREEN, 2); static final Command exitCommand = new Command ("Exit", Command.EXIT, 2);

In order to enable the MIDlet to receive command actions, you need to implement the CommandListener interface, and the corresponding commandAction() method. Depending on the command received, you call send(), clear(), or notifyDestroyed():
public class TeleTransfer extends MIDlet implements ItemStateListener, CommandListener { public void commandAction (Command c, Displayable d) { if (c == exitCommand) { notifyDestroyed(); } else if (c == sendCommand) { send (); } else if (c == clearCommand) { clear (); } }

With these modifications, your TeleTransfer MIDlet is able to handle the desired commands. You still need to add the Commands to the Form, and register the TeleTransfer MIDlet as a CommandListener in order to actually receive the commands:
public TeleTransfer () { ... mainForm.addCommand (sendCommand); mainForm.addCommand (clearCommand); mainForm.addCommand (exitCommand); mainForm.setCommandListener (this); }

Figure 3.6 shows the Send Alert of the new version of your TeleTransfer application.

Figure 3.6 The TeleTransfer MIDlet showing an alert that displays the transfer information as a summary before sending.

Further Item Classes: Gauge and DateField


Now you have used all the Item subclasses except Gauge and DateField. Both classes are specialized input elements, where the Gauge may also make sense as a pure read-only information item. The Gauge item visualizes an integer value by displaying a horizontal bar. It is initialized with a label, a flag indicating whether it is interactive, and a maximum and an initial value. If a Gauge is interactive, the user is allowed to change the value using a devicedependent input method. Changes to the gauge value will cause ItemEvents if a corresponding listener is registered to the form. The following code snippet shows the construction of a non-interactive Gauge labeled Progress that is initialized with a value of 0 and a maximum of 100:
Gauge gauge = new Gauge ("Progress", false, 0, 100);

If a Gauge is used to display progress of a process that takes a longer amount of time, you should also add a corresponding Stop command to the form to abort the progress. The current value of the Gauge can be set using the method setValue() and read using the method getValue(). Analogous setMaxValue() and getMaxValue() methods let you access the maximum value of the Gauge. The DateField is a specialized widget for entering date and time information in a simple way. It can be used to enter a date, a time, or both types of information at once. The appearance of the DateField is specified using three possible input mode constants in the constructor. Possible DateField mode constants are listed in Table 3.6.

Table 3.6 DateField Mode Constants


Constant Value

DATE DATE_TIME TIME

Passed if the DateField should be used for entering a date only. Used for creating a DateField to enter both date and time information. Used to enter time information only.

The DateField has two constructors in which a label and the mode can be specified. Using the second constructor, an additional TimeZone can be passed. The following code snippet shows how a DateField for entering the date of birth can be initialized:
DateField dateOfBirth = new DateField ("Date of birth:", DateField.DATE);

After you enter the date into the DateField, it can be accessed using the getDate() method. The DateField offers some additional methods for getting information about the input mode and methods for setting the date and the input mode as well. The concrete usage of the DateField is shown in Chapter 9 in the Blood Sugar Logger application.

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

Further Screen Classes: List and TextBox


The current version of the TeleTransfer MIDlet shows how to use the Form and the corresponding items available in the lcdui package. The application consists of one main form that holds all application widgets. However, your main form is rather long now, so the question arises how to improve the usability of the application. This section shows how to structure the user interface by using multiple screens and introduces the List and TextBox classes.

The List Class


One possibility to clean up the user interface is to move the currency selection to a separate screen. It takes a lot of space and may need even more room if additional options are added. Also, you can assume that the currency is not changed very often. You could create a new Form and just move the ChoiceGroup there. However, lcdui provides a special List class inherited from Screen for this purpose. The advantage of the List class is that it provides the IMPLICIT mode that was already mentioned in the

section "Selecting Elements Using ChoiceGroups." Using the IMPLICIT mode, the application gets immediate notification when an item is selected. Whenever an element in the List is selected, a Command of the type List.SELECT_COMMAND is issued. As in the ChoiceGroup, the elements consist of Strings and optional Images. For initializing the List, the lcdui packages offers constructors. The constructors work like the ChoiceGroup constructors. The first one creates an empty List with a given title and type only. The second one takes the title, the type, an array of Strings as initial amount of List elements, and an optional array of Images for each List element. In the implementation of the TeleTransfer application, you implement a new class CurrencyList extending List that will be used as your new currency selector. Since you will use the IMPLICIT mode, you need to implement a command listener, so you can already add the corresponding declaration: public class CurrencyList extends List implements CommandListener {

Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit o StumbleUpon o Facebook o FriendFeed o Furl o Newsvine o Google o LinkedIn o MySpace o Technorati o Twitter o Windows Live o YahooBuzz

To set the labels of the main form TextFields according to the index of the selected element in the CurrencyList, you create two String arrays, CURRENCY_NAMES and CURRENCY_FRACTIONS:
static final String [] CURRENCY_NAMES = {"Dollar", "Euro", "Yen"}; static final String [] CURRENCY_FRACTIONS = {"Cent", "Cent", "Sen"};

In order to set the labels of the main forms TextFields for the whole and the fractional amount according to the selected currency in the CurrencyList, you need a reference back to the main TeleTransfer MIDlet. For this reason, you store the TeleTransfer reference in a variable called teleTransfer. The reference is set in the constructor of your CurrencyList:
TeleTransfer teleTransfer;

In the constructor, you also add currency symbol images to the list. You need to load them, but the call to the super constructor must be the first statement in a constructor. So you call the constructor of the super class by specifying the title and type only. Then you create the Images needed for each list element, which are stored in the MIDlet suite's JAR file. You also call setCommandListener() to register the currency list for handling commands that are issued:
public CurrencyList (TeleTransfer teletransfer) { super ("Select Currency", Choice.IMPLICIT); this.teleTransfer = teletransfer; try { append ("USD", Image.createImage ("/Dollar.png")); append ("EUR", Image.createImage ("/Euro.png")); append ("JPY", Image.createImage ("/Yen.png")); } catch (java.io.IOException x) { throw new RuntimeException ("Images not found"); } setCommandListener (this); }

The final step in creating the CurrencyList is to implement the commandAction() method of the CommandListener interface. As you already know, a List of IMPLICIT type issues a List.SELECT_COMMAND to the registered CommandListener whenever a new element is selected to indicate the selection change. In case of a selection change, you modify the labels of the main form TextFields. The actual labels are obtained from the String arrays CURRENCY_NAMES and CURRENCY_FRACTIONS. Using the teleTransfer reference, you can access the TextFields. Finally, you call the new method teleTransfer.back(), which sets the screen back to the main form (the back() method will be given at the end of this section):
public void commandAction (Command c, Displayable d) { if (c == List.SELECT_COMMAND) { teleTransfer.amountWhole.setLabel (CURRENCY_NAMES [getSelectedIndex ()]); teleTransfer.amountFraction.setLabel (CURRENCY_FRACTIONS [getSelectedIndex ()]); teleTransfer.back (); } } }

Figure 3.7 shows currency Images and abbreviations in the CurrencyList.

Figure 3.7 The new CurrencyList.

The TextBox Class


Beneath Alert, List, and Form, there is only one further subclass of Screen: the TextBox. The TextBox allows the user to enter multi-line text on a separate screen. The constructor parameters and the constraint constants are identical to those of TextField. As for the currency list, you can also add a new screen enabling the user to enter a transfer reason if desired. Similar to the CurrencyList, you implement a new class handling the commands related to the new screen. However, this time it is derived from the TextBox. Again, you implement the CommandListener interface: public class TransferReason extends TextBox implements CommandListener { In the TextBox, you provide two commands, okCommand clearCommand for clearing the text: for applying the entered text and

static final Command okCommand = new Command ("OK", Command.BACK, 1); static final Command clearCommand = new Command ("Clear", Command.SCREEN, 2);

Again, you store a reference back to the TeleTransfer MIDlet in the TransferReason TextBox: TeleTransfer teleTransfer; The constructor gets the reference back to TeleTransfer MIDlet and stores it in the variable declared previously. You also add the commands to the TextBox, and register it as CommandListener:
public TransferReason (TeleTransfer teleTransfer) { super ("Transfer Reason", "", 50, TextField.ANY); this.teleTransfer = teleTransfer; addCommand (okCommand); addCommand (clearCommand);

setCommandListener (this); }

Your commandAction() implementation clears the text or returns to the main screen, depending on the Command selected:
public void commandAction (Command c, Displayable d) { if (c == clearCommand) { setString (""); } else if (c == okCommand) { teleTransfer.back (); } } }

Figure 3.8 shows the TransferReason TextBox.

Figure 3.8 The TransferReason TextBox showing a sample transfer reason text.

TeleTransfer with Multiple Screens


Now you have created two additional screens, but you still need to integrate them in your main application. To do so, you need to change the TeleTransfer implementation somewhat. Since the TeleTransfer's ChoiceGroup for selecting the currency is replaced by the CurrencyList, you do not need the ItemStateListener for detecting item changes any more. So you remove the listener and also the corresponding callback method itemStateChanged(). To display the two new Screens CurrencyList and TransferReason, you implement the two commands currencyCommand and reasonCommand. The new commands are added to the MIDlet in the constructor using the addCommand() method. In the clear() method, the new TextBox is also cleared by calling the corresponding setString() method. Finally you add the back() method to the TeleTransfer application; this method is called from the new Screens to return to the main form. The commandAction() method is extended to handle the new commands, displaying the new Screens. Listing 3.1 shows the complete source code of the final version of the TeleTransfer application.

Listing 3.1 TeleTransfer.javaThe Complete TeleTransfer Sample Source Code


import javax.microedition.midlet.*; import javax.microedition.lcdui.*; class CurrencyList extends List implements CommandListener { TeleTransfer teleTransfer; static final String [] CURRENCY_NAMES = {"Dollar", "Euro", "Yen"}; static final String [] CURRENCY_FRACTIONS = {"Cent", "Cent", "Sen"}; public CurrencyList (TeleTransfer teletransfer) { super ("Select Currency", Choice.IMPLICIT); this.teleTransfer = teletransfer; try { append ("USD", Image.createImage ("/Dollar.png")); append ("EUR", Image.createImage ("/Euro.png")); append ("JPY", Image.createImage ("/Yen.png")); } catch (java.io.IOException x) { throw new RuntimeException ("Images not found"); } setCommandListener (this); }

public void commandAction (Command c, Displayable d) { if (c == List.SELECT_COMMAND) { teleTransfer.amountWhole.setLabel (CURRENCY_NAMES [getSelectedIndex ()]); teleTransfer.amountFraction.setLabel (CURRENCY_FRACTIONS [getSelectedIndex ()]); teleTransfer.back (); } } }

class TransferReason extends TextBox implements CommandListener { static final Command okCommand = new Command ("OK", Command.BACK, 1); static final Command clearCommand = new Command ("Clear", Command.SCREEN, 2); TeleTransfer teleTransfer; public TransferReason (TeleTransfer teleTransfer) { super ("Transfer Reason", "", 50, TextField.ANY); this.teleTransfer = teleTransfer; addCommand (okCommand); addCommand (clearCommand); setCommandListener (this); }

public void commandAction (Command c, Displayable d) { if (c == clearCommand) { setString (""); } else if (c == okCommand) { teleTransfer.back (); } } } public class TeleTransfer extends MIDlet implements CommandListener { static final Command sendCommand = new Command ("Send", Command.SCREEN, 2); static final Command clearCommand = new Command ("Clear", Command.SCREEN, 2); static final Command exitCommand = new Command ("Exit", Command.SCREEN, 1); static final Command currencyCommand = new Command ("Currency", Command.SCREEN, 2); static final Command reasonCommand = new Command ("Reason", Command.SCREEN, 2); Form mainForm = new Form ("TeleTransfer"); TextField receiverName = new TextField ("Receiver Name", "", 20, TextField.ANY); TextField receiverAccount = new TextField ("Receiver Account#", "", 8, TextField.NUMERIC); TextField amountWhole = new TextField ("Dollar", "", 6, TextField.NUMERIC); TextField amountFraction = new TextField ("Cent", "", 2, TextField.NUMERIC); CurrencyList currencyList = new CurrencyList (this); TransferReason transferReason = new TransferReason (this); Display display; public TeleTransfer () { mainForm.append (receiverName); mainForm.append (receiverAccount); mainForm.append (amountWhole); mainForm.append (amountFraction); mainForm.addCommand (currencyCommand); mainForm.addCommand (reasonCommand); mainForm.addCommand (sendCommand); mainForm.addCommand (exitCommand); mainForm.setCommandListener (this); } public void startApp () { display = Display.getDisplay (this); display.setCurrent (mainForm); }

public void clear () { receiverName.setString (""); receiverAccount.setString (""); amountWhole.setString (""); amountFraction.setString (""); transferReason.setString (""); } public void send () { Alert alert = new Alert ("Send"); alert.setString ("transfer " + amountWhole.getString () + "." + amountFraction.getString () + " " + amountWhole.getLabel () + "\nto Acc#" + receiverAccount.getString () + "\nof " + receiverName.getString ()); alert.setTimeout (2000); display.setCurrent (alert); clear (); } public void pauseApp () { } public void destroyApp (boolean unconditional) { } public void back () { display.setCurrent (mainForm); } public void commandAction (Command c, Displayable d) { if (c == exitCommand) { notifyDestroyed(); } else if (c == sendCommand) { sendTransferInformation (); } else if (c == clearCommand) { resetTransferInformation (); } else if (c == currencyCommand) { display.setCurrent (currencyList); } else if (c == reasonCommand) { display.setCurrent (transferReason); } } }

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing

Bio Send Email More Articles

Low-Level API
In contrast to the high-level API, the low-level API allows full control of the MID display at pixel level. For this purpose, the lcdui package contains a special kind of screen called Canvas. The Canvas itself does not provide any drawing methods, but it does provide a paint() callback method similar to the paint() method in AWT components. Whenever the program manager determines that it is necessary to draw the content of the screen, the paint() callback method of Canvas is called. The only parameter of the paint() method is a Graphics object. In contrast to the lcdui high-level classes, there are many parallels to AWT in the low-level API. The Graphics object provides all the methods required for actually drawing the content of the screen, such as drawLine() for drawing lines, fillRect() for drawing a filled rectangular area or drawstring() for drawing text strings. In contrast to AWT, lcdui does not let you mix high-level and low-level graphics. It is not possible to display high-level and low-level components on the screen simultaneously. The program manager knows that it must call the paint() method of Canvas when the instance of Canvas is shown on the screen. However, a repaint can also be triggered by the application at any time. By calling the repaint() method of Canvas, the system is notified that a repaint is necessary, and it will call the paint() method. The call of the paint() method is not performed immediately; it may be delayed until the control flow returns from the current event handling method. The system may also collect several repaint requests before paint() is actually called. This delay normally is not a problem, but when you're doing animation, the safest way to trigger repaints is to use Display.callSerially() or to request the repaint from a separate Thread or TimerTask. Alternatively, the application can force an immediate repaint by calling serviceRepaints(). (For more information, see the section "Animation" at the end of this chapter.)

Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit

o o o o o o o o o o o o

StumbleUpon Facebook FriendFeed Furl Newsvine Google LinkedIn MySpace Technorati Twitter Windows Live YahooBuzz

The Canvas class also provides some input callback methods that are called when the user presses or releases a key or touches the screen with the stylus (if one is supported by the device).

Basic Drawing
Before we go into the details of user input or animation, we will start with a small drawing example showing the concrete usage of the Canvas and Graphics classes. The example clears the screen by setting the color to white and filling a rectangle the size of the screen, determined by calling getWidth() and getHeight(). Then it draws a line from coordinates (0,0) to (100,200). Finally, it draws a rectangle starting at (20,30), 30 pixels wide and 20 pixels high:
import javax.microedition.lcdui.*; class DrawingDemoCanvas extends Canvas { public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); g.drawLine (0, 0, 100, 200); g.fillRect (20, 30, 30, 20); } }

As you can see in the example code, you create a custom class DrawingDemoCanvas in order to fill the paint() method. Actually, it is not possible to draw custom graphics without creating a new class and implementing the paint() method. In order to really see your Canvas implementation running, you still need a corresponding MIDlet. Here's the missing code:

import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class DrawingDemo extends MIDlet { public void startApp () { Display.getDisplay (this).setCurrent (new DrawingDemoCanvas ()); } public void pauseApp () {} public void destroyApp (boolean forced) {} }

Now you can start your DrawingDemo MIDlet. Depending on the screen size of the device, it will create output similar to Figure 3.9. In most subsequent examples, you will omit the MIDlet since it is basically the same as this one, except that the name of your Canvas class will be different.

Figure 3.9 Output of the DrawingDemo MIDlet. In the example, the screen is cleared before drawing because the system relies on the paint() method to fill every pixel of the draw region with a valid value. You don't erase the previous content of the screen automatically because doing so may cause flickering of animations. The application cannot make any assumptions about the content of the Screen before paint() is called. The screen may be filled with the content drawn at the last call of paint(), but it may also be filled with an alert box remaining from an incoming phone call, for example.

Drawing Style and Color


In the DrawingDemoCanvas implementation, you can find two calls to setGrayScale(). The setGrayScale() method sets the gray scale value for the following drawing operations. Valid grayscale values range from 0 to 255, where 0 means black and 255 means white. Not all possible values may actually render to different gray values on the screen. If the device provides fewer than 256 shades of gray, the best fitting value supported by the device is chosen. In the example, the value is first set to white, and the screen is cleared by the following call to drawRect(). Then, the color is set to black for the subsequent drawing operations. The setGrayScale() method is not the only way to influence the color of subsequent drawing. MIDP also provides a setColor() method. The setColor() method has three parameters holding the red, green, and blue components of the desired color. Again, the values range from 0 to 255, where 255 means brightest and 0 means darkest. If all three

parameters are set to the same value, the call is equivalent to a corresponding call of setGrayScale(). If the device is not able to display the desired color, it chooses the best fitting color or grayscale supported by the device automatically. Some examples are listed in Table 3.7.

Table 3.7 Example Color Parameter Settings


Parameter Settings
setColor setColor setColor setColor setColor setColor setColor setColor

Resulting Color Red Green Blue Dark red Yellow Black

(255, 0, 0) (0, 255, 0) (0, 0, 255) (128, 0, 0) (255, 255, 0) (0, 0, 0)

(255, 255, 255) White (128, 128, 128) 50% gray

The only other method that influences the current style of drawing is the setStrokeStyle() method. The setStrokeStyle() command sets the drawing style of lines to dotted or solid. You determine the style by setting the parameter to one of the constants DOTTED or SOLID, defined in the Graphics class. When the paint() method is entered, the initial drawing color is always set to black and the line style is SOLID.

Simple Drawing Methods


In the example, you have already seen fillRect() and drawLine(). Table 3.8 shows all drawing primitives contained in the Graphics class. All operations where the method names begin with draw, except drawstring() and drawImage(), are influenced by the current color and line style. They draw the outline of a figure, whereas the fill methods fill the corresponding area with the current color and do not depend on the line style.

Table 3.8 Drawing Methods of the Graphics Class


Method
drawImage (Image image, drawString (String text,

Purpose Draws an Image. Explained in detail in the int x, int y, int align) "Images" section. Draws a text string at the given position in the int x, int y, int align) current color; see "Text and Fonts."

drawRect (int x, int y,

Draws an empty rectangle with the upper-left int w, int h) corner at the given (x,y)coordinate, with the given width and a height. The next section explains why the rectangle is one pixel larger than you might expect. Like drawRect(), except that an additional radius int w, int h, int r) is given for rounded corners of the rectangle. Draws a line from (x0,y0) to (x1,y1). int x1, int y1) covering the specified rectangle, using the current int startAng, int arcArc) color and stroke style. The resulting arc begins at startAng and extends for arcAng degrees. Angles are interpreted such that 0 degrees is at the 3 o'clock position. A positive value indicates a counterclockwise rotation while a negative value indicates a clockwise rotation. Similar to drawRect(), but fills the given area int w, int h) with the current color. Related to fillRect() as drawRoundRect() is int w, int h, related to drawRect(). int startAng, int endAng); Like drawArc(), but fills the corresponding region. int w,
int h, int startAng, int endAng);

drawRoundRect (int x, int y, drawLine (int x0, int y0, drawArc (int x, int y, Draws the outline of a

circular or elliptical arc


int w, int h,

fillRect (int x, int y, fillRoundRect (int x, int y, fillArc (int x, int y,

Coordinate System and Clipping


In the drawing example, we already have used screen coordinates without explaining what they actually mean. You might know that the device display consists of little picture elements (pixels). Each of these pixels is addressed by its position on the screen, measured from the upper-left corner of the device, which is the origin of the coordinate system. Figure 3.10 shows the lcdui coordinate system. Actually, in Java the coordinates do not address the pixel itself, but the space between two pixels, where the "drawing pen" hangs to the lower right. For drawing lines, this does not make any difference, but for rectangles and filled rectangles this results in a difference of one pixel in width and height: In contrast to filled rectangles, rectangles become one pixel wider and higher than you might expect. While this may be confusing at first glance, it respects the mathematical notation that lines are infinitely thin and avoids problems when extending the coordinate system to real distance measures, as in the J2SE class Graphics2D.

Figure 3.10 The lcdui coordinate system. In all drawing methods, the first coordinate (x) denotes the horizontal distance from the origin and the second coordinate (y) denotes the vertical distance. Positive coordinates mean a movement down and to the right. Many drawing methods require additional width and height parameters. An exception is the drawLine() method, which requires the absolute coordinates of the destination point. The origin of the coordinate system can be changed using the translate() method. The given coordinates are added to all subsequent drawing operations automatically. This may make sense if addressing coordinates relative to the middle of the display is more convenient for some applications, as shown in the section "Scaling and Fitting," later in the chapter. The actual size of the accessible display area can be queried using the getWidth() and methods, as performed in the first example that cleared the screen before drawing. The region of the screen where drawing takes effect can be further limited to a rectangular area by the clipRect() method. Drawing outside the clip area will have no effect.
getHeight()

The following example demonstrates the effects of the clipRect() method. First, a dotted line is drawn diagonally over the display. Then a clipping region is set. Finally, the same line as before is drawn using the SOLID style:

import javax.microedition.lcdui.*; class ClipDemoCanvas extends Canvas { public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); int m = Math.min (getWidth (), getHeight ()); g.setGrayScale (0); g.setStrokeStyle (Graphics.DOTTED); g.drawLine (0, 0, m, m); g.setClip (m / 4, m / 4, m / 2, m / 2); g.setStrokeStyle (Graphics.SOLID); g.drawLine (0, 0, m, m); } }

Figure 3.11 shows the resulting image. Although both lines have identical start and end points, only the part covered by the clipping area is replaced by a solid line.

Figure 3.11 Output of the clipRect() example: Only the part covered by the clipping area is redrawn solid, although the line coordinates are identical. When the paint() method is called from the system, a clip area may already be set. This may be the case if the application just requested repainting of a limited area using the parameterized repaint call, or if the device just invalidated a limited area of the display,

for example if a pop-up dialog indicating an incoming call was displayed but did not cover the whole display area. Actually, clipRect() does not set a new clipping area, but instead shrinks the current clip area to the intersection with the given rectangle. In order to enlarge the clip area, use the setClip() method. The current clip area can be queried using the getClipX(), getClipY(), and getClipHeight() methods. When drawing is computationally expensive, this information can be taken into account in order to redraw only the areas of the screen that need an update.

getClipWidth(),

Text and Fonts


For drawing text, lcdui provides the method drawstring(). In addition to the basic drawstring() method, several variants let you draw partial strings or single characters. (Details about the additional methods can be found in the lcdui API documentation.) The simple drawstring() method takes four parameters: The character string to be displayed, the x and y coordinates, and an integer determining the horizontal and vertical alignment of the text. The alignment parameter lets you position the text relative to any of the four corners of its invisible surrounding box. Additionally, the text can be aligned to the text baseline and the horizontal center. The sum or logical or (|) of a constant for horizontal alignment (LEFT, RIGHT, and HCENTER) and constants for vertical alignment (TOP, BOTTOM, and BASELINE) determine the actual alignment. Figure 3.12 shows the anchor points for the valid constant combinations.

Figure 3.12 Valid combinations of the alignment constants and the corresponding anchor points. The following example illustrates the usage of the drawstring() method. By choosing the anchor point correspondingly, the text is displayed relative to the upper-left and lower-right corner of the screen without overlapping the screen border:
import javax.microedition.lcdui.*;

class TextDemoCanvas extends Canvas { public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); g.drawString ("Top/Left", 0, 0, Graphics.TOP | Graphics.LEFT); g.drawString ("Baseline/Center", getWidth () / 2, getHeight () / 2, Graphics.HCENTER | Graphics.BASELINE); g.drawString ("Bottom/Right", getWidth (), getHeight (), Graphics.BOTTOM | Graphics.RIGHT); } }

Figure 3.13 shows the output of the TextDemo example.

Figure 3.13 Output of the TextDemo example. In addition to the current drawing color, the result of the drawstring() method is influenced by the current font. MIDP provides support for three different fonts in three different sizes and with the three different attributes: bold, italic, and underlined. A font is not selected directly, but the setFont() method takes a separate Font object, describing the desired font, as a parameter. The explicit Font class provides additional information about the font, such as its width and height in pixels, baseline position, ascent and descent, and so on. Figure 3.14 illustrates the meaning of the corresponding values. This information is important for operations such as drawing boxes around text strings. In addition, word-wrapping algorithms rely on the actual pixel width of character strings when rendered to the screen.

Figure 3.14 Font properties and the corresponding query methods.

A Font object is created by calling the static method createFont() of the class Font in the lcdui package. The createFont() method takes three parameters: the font type, style, and size of the font. Similar to the text alignment, there are predefined constants for setting the corresponding value; these constants are listed in Table 3.9.

Table 3.9 createFont() Property Constants


Property Constants Size Style Face
SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE STYLE_PLAIN, STYLE_ITALICS, STYLE_BOLD, STYLE_UNDERLINED FACE_SYSTEM, FACE_MONOSPACE, FACE_PROPORTIONAL

The style constants can be combinedfor example, STYLE_ITALICS | STYLE_BOLD will result in a bold italics font style. The following example shows a list of all fonts available, as far as the list fits on the screen of the device:
import javax.microedition.lcdui.*; class FontDemoCanvas extends Canvas { static final int [] styles = {Font.STYLE_PLAIN, Font.STYLE_BOLD, Font.STYLE_ITALIC}; static final int [] sizes = {Font.SIZE_SMALL, Font.SIZE_MEDIUM, Font.SIZE_LARGE}; static final int [] faces = {Font.FACE_SYSTEM, Font.FACE_MONOSPACE, Font.FACE_PROPORTIONAL}; public void paint (Graphics g) { Font font = null; int y = 0; g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); for (int size = 0; size < sizes.length; size++) { for (int face = 0; face < faces.length; face++) { int x = 0; for (int style = 0; style < styles.length; style++) { font = Font.getFont (faces [face], styles [style], sizes [size]); g.setFont (font); g.drawString ("Test", x+1, y+1, Graphics.TOP | Graphics.LEFT); g.drawRect (x, y, font.stringWidth ("Test")+1,

font.getHeight () + 1); x += font.stringWidth ("Test")+1; } y += font.getHeight () + 1; } } } }

Figure 3.15 shows the output of the FontDemo example.

Figure 3.15 Output of the FontDemo example.

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

Images
The Graphics class also provides a method for drawing images. As shown in the final version of TeleTransfer application, Images can be predefined and contained in the JAR file of the MIDlet. The only file format that is mandatory for MIDP is the Portable Network Graphics (PNG) file format. The PNG format has several advantages over other graphics formats; for example, it is license free and supports true color images, including a full transparency (alpha) channel. PNG images are always compressed with a loss-less algorithm. The algorithm is identical to the algorithm used for JAR files, so the MIDP implementation can save space by using the same algorithm for both purposes. An image can be loaded from the JAR file using the static method Image.create (String name). The name parameter denotes the filename of the image in the JAR file. Please note that this create() method may throw an IOException. The drawImage() method in Graphics requires an Image object, the coordinates, and an integer denoting the alignment as parameters. The alignment parameter is similar the

alignment of drawString(), except that the BASELINE constant is not supported. An additional alignment constant available for images only is VCENTER, which forces the image to be vertically centered relative to the given coordinates. Figure 3.16 shows the valid constant combinations and the corresponding anchor points.

Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit o StumbleUpon o Facebook o FriendFeed o Furl o Newsvine o Google o LinkedIn o MySpace o Technorati o Twitter

o o

Windows Live YahooBuzz

Figure 3.16 Alignment constant combinations valid for images and the corresponding anchor points. The following example first loads the image logo.png from the MIDlet JAR file in the constructor, and then displays the image three times. One image is drawn in the upper-left corner, one in the lower-right corner, and one in the center of the display, as shown in Figure 3.17:
import java.io.*; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; class ImageDemoCanvas extends Canvas { Image image; public ImageDemoCanvas () { try { image = Image.createImage ("/logo.png"); } catch (IOException e) { throw new RuntimeException ("Unable to load Image: "+e); } } public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.drawImage (image, 0, 0, Graphics.TOP | Graphics.LEFT); g.drawImage (image, getWidth () / 2, getHeight () / 2, Graphics.HCENTER | Graphics.VCENTER); g.drawImage (image, getWidth (), getHeight (), Graphics.BOTTOM | Graphics.RIGHT); } }

Figure 3.17 Output of the ImageDemo example.

Images can also be created at runtime from scratch. The static method Image.create (int width, int height) creates a new dynamic image of the given size. In contrast to images loaded from a JAR file, these images are mutable. Mutable images can be modified by calling getGraphics (). The Graphics object returned can be used for modifying the image with all the methods provided by the Graphics class. Please note that images loaded from a JAR file cannot be modified. However, it is possible to create a mutable image, and then draw any other image in the mutable image. By modifying the constructor of the previous example canvas as follows, the image drawn in the paint() method is created and filled at runtime instead of loading an image from the JAR file:
public ImageDemoCanvas () { image = Image.createImage (10,10); image.getGraphics ().fillArc (0,0,10,10,0, 360); }

The disadvantage of mutable images is that they cannot be used in high-level GUI elements since it is possible to modify them at any time, possibly leading to inconsistent display of widgets. For that reason, another static create method, createImage(Image image), is provided that creates an immutable image from another image.

Interaction
Because the Canvas class is a subclass of Displayable, it provides the same support for commands as the high-level screen classes. Here, you will concentrate on the additional interaction possibilities the Canvas class offers: direct key input and pointer support. Please note that all input events and command notifications and the paint() method are called serially. That means that the application manager will call none of the methods until the previous event handling method has returned. So all these methods should return quickly, or the user will be unable to interact with the application. For longer tasks, a separate thread can be started.

Key Input
For key input, the Canvas class provides three callback methods: keyPressed(), keyReleased(), and keyRepeated(). As the names suggest, keyPressed() is called when a key is pressed, keyRepeated() is called when the user holds down the key for a longer period of time, and keyReleased() is called when the user releases the key. All three callback methods provide an integer parameter, denoting the Unicode character code assigned to the corresponding key. If a key has no Unicode correspondence, the given integer is negative. MIDP defines the following constant for the keys of a standard ITU-T keypad: KEY_NUM0, KEY_NUM1, KEY_NUM2, KEY_NUM3, KEY_NUM4, KEY_NUM5, KEY_NUM6, KEY_NUM7, KEY_NUM8, KEY_NUM9, KEY_POUND, and KEY_STAR. Applications

should not rely on the presence of any additional key codes. In particular, upper- and lowercase or characters generated by pressing a key multiple times are not supported by low-level key events. A "name" assigned to the key can be queried using the getKeyName() method. Some keys may have an additional meaning in games. For this purpose, MIDP provides the constants UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C, and GAME_D. The "game" meaning of a keypress can be determined by calling the getGameAction() method. The mapping from key codes to game actions is device dependent, so different keys may map to the same game action on different devices. For example, some devices may have separate cursor keys; others may map the number pad to four-way movement. Also, several keys may be mapped to the same game code. The game code can be translated back to a key code using the getKeyCode() method. This also offers a way to get the name of the key assigned to a game action. For example, the help screen of an application may display
"press "+getKeyName (getKeyCode (GAME_A))

instead of "press GAME_A". The following canvas implementation shows the usage of the key event methods. For each key pressed, repeated, or released, it shows the event type, character and code, key name, and game action. The first part of the implementation stores the event type and code in two variables and schedules a repaint whenever a key event occurs:
import javax.microedition.lcdui.*; class KeyDemoCanvas extends Canvas { String eventType = "- Press any!"; int keyCode; public void keyPressed (int keyCode) { eventType = "pressed"; this.keyCode = keyCode; repaint (); } public void keyReleased (int keyCode) { eventType = "released"; this.keyCode = keyCode; repaint (); } public void keyRepeated (int keyCode) { eventType = "repeated"; this.keyCode = keyCode; repaint (); }

The second part prints all event properties available to the device screen. For this purpose, you first implement an additional write() method that helps the paint() method to identify the current y position on the screen. This is necessary because drawText() does not advance to a new line automatically. The write() method draws the string at the given y position and returns the y position plus the line height of the current font, so paint() knows where to draw the next line:
public int write (Graphics g, int y, String s) { g.drawString (s, 0, y, Graphics.LEFT|Graphics.TOP); return y + g.getFont ().getHeight (); }

The paint() method analyzes the keyCode and prints the result by calling the write() method defined previously, as shown in Figure 3.18:
public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); int y = 0; y = write (g, y, "Key "+ eventType); if (keyCode == 0) return; y = write (g, y, "Char/Code: "+ ((keyCode < 0) ? "N/A" : "" +(char) keyCode) + "/" + keyCode); y = write (g, y, "Name: "+getKeyName (keyCode)); String gameAction; switch (getGameAction (keyCode)) { case LEFT: gameAction = "LEFT"; break; case RIGHT: gameAction = "RIGHT"; break; case UP: gameAction = "UP"; break; case DOWN: gameAction = "DOWN"; break; case FIRE: gameAction = "FIRE"; break; case GAME_A: gameAction = "GAME_A"; break; case GAME_B: gameAction = "GAME_B"; break; case GAME_C: gameAction = "GAME_C"; break; case GAME_D: gameAction = "GAME_D"; break; default: gameAction = "N/A"; } write (g, y, "Action: "+gameAction); } }

Figure 3.18 Output of the KeyDemo example when the "Fire" key was released.

Pointer Events
For devices supporting a pointer device such as a stylus, touch screen, or trackball, the Canvas class provides three notification methods: pointerPressed(), pointerDragged(), and pointerReleased(). These methods work similarly to the key event methods, except that they provide two integer parameters, denoting the x and y position of the pointer when the corresponding event occurs. (Please note that pointer support is optional in MIDP, so the application should not rely on the presence of a pointer. Such devices are uncommon for devices such as mobile phones.) The following sample program demonstrates the usage of the three methods:
import javax.microedition.lcdui.*; class PointerDemoCanvas extends Canvas { String eventType = "Press Pointer!"; int x; int y; public void pointerPressed (int x, int y) { eventType = "Pointer Pressed"; this.x = x; this.y = y; repaint (); } public void pointerReleased (int x, int y) { eventType = "Pointer Released"; this.x = x; this.y = y; repaint (); } public void pointerDragged (int x, int y) { eventType = "Pointer Repeated"; this.x = x; this.y = y; repaint (); } public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); g.setGrayScale (0); g.drawString (eventType + " " +x +"/"+y, 0, 0, Graphics.TOP|Graphics.LEFT); g.drawLine (x-4, y, x+4, y); g.drawLine (x, y-4, x, y+4); } }

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

Foreground and Background Notifications


For several reasons, the Canvas may move into the backgroundfor example, if the display is set to another displayable object or if the device displays a system dialog. In these cases, the Canvas is notified by the hideNotify() method. When the Canvas becomes visible (again), the corresponding counterpart, showNotify(), is called.

Javagochi Example
Now that you are familiar with the Canvas object and the basic drawing methods of the Graphics class, you are ready to develop a small interactive application, the Javagochi. As you can see in the following code, the MIDlet implementation of Javagochi is already finished, but the Face class is missing:
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class Javagochi extends MIDlet { static final int IDEAL_WEIGHT = 100; Display display = Display.getDisplay (this); Face face = new Face (this); int weight = IDEAL_WEIGHT; Timer consumption; int score;

Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit o StumbleUpon o Facebook

o o o o o o o o o o

FriendFeed Furl Newsvine Google LinkedIn MySpace Technorati Twitter Windows Live YahooBuzz

Before you begin development, let us first say a few words about the Javagochi itself. A Javagochi has a weight that is initialized with its IDEAL_WEIGHT. It also owns an instance of Display, Face, and Consumption, which will be explained later. Finally, it stores a score value for the care the owner spends on the Javagochi. The happiness of the Javagochi is determined by the deviation of its current weight from the ideal weight, ranging from 10 to 0:
public int getHappiness () { return 20 - (weight > IDEAL_WEIGHT ? 10 * weight / IDEAL_WEIGHT : 10 * IDEAL_WEIGHT / weight); if (happiness < 0) happiness = 0; if (happiness > 10) happiness = 10; }

This formula also demonstrates how to circumvent problems with the absence of floating point arithmetic. In order to avoid loss of significant fractions, the values are scaled up before division. Like all other known life forms, the Javagochi can die. Javagochies only die from sadness when their happiness level reaches zero:
public boolean isDead () { return getHappiness <= 0; }

The only other action a Javagochi can perform besides dying is to transform energy to matter and back. Since a weight change may change the Javagochi's look, a repaint is requested in the transform() method:
public void transform (int amount) { if (!isDead ()) { weight += amount; face.repaint (); } }

When the Javagochi MIDlet is started, it displays itself and starts a consumption Timer that keeps track of the power the Javagochi needs for living:
public void startApp () { display.setCurrent (face); consumption = new Consumption (this).start (); }

When the MIDlet is paused, the Javagochi goes to sleep by telling the consumption thread to terminate itself. The destroyApp() method does nothing because the life cycle will enter sleep anyway, and no further cleanup is needed:
public void pauseApp () { consumption.leave = true; } public void destroyApp (boolean forced) { } }

The consumption Thread is a separate class that monitors the power the Javagochi needs for living. In the run() method, every 0.5 seconds the score is updated depending on the Javagochi's happiness and the small amount of body mass that is transformed back to life energy:
public class Consumption extends Thread { Javagochi javagochi; boolean leave = false; public Consumption (Javagochi javagochi) { this.javagochi = javagochi; } public void run () { while (!leave) { try { sleep (500); } catch (InterruptedException e) {break;} javagochi.score += 10 - javagochi.deviation; javagochi.transform (-5); } } }

Now that you know how a Javagochi works, it is your job to give the Javagochi an appropriate appearance by implementing the missing Face class.

Scaling and Fitting


In many cases, it is a good idea to scale displayed graphics depending on the actual screen size. Otherwise, the display will look nice on one particular device type, but won't fit the screen on devices with a lower screen resolution or become unnecessarily small on devices with higher screen resolutions. We will now show how scaling works for the Javagochi example. A picture of a Javagochi is shown in Figure 3.19. You will start by drawing the shape of the face, a simple ellipse. In this case, the ellipse will reflect the Javagochi's weight. If the Javagochi is at its ideal weight, the ellipse becomes a circle.

Figure 3.19 A happy Javagochi at its ideal weight. In order to leave some space for the Javagochi to grow, the diameter of the ideal circle is half the minimum of the screen width and height. Thus, the height of the Javagochi is calculated using the following formula:
int height = Math.min (getHeight (), getWidth ()) / 2;

Based on the current weight, the ideal weight, and the calculated height, which is also the diameter of the "ideal" Javagochi, you can now calculate the width of the Javagochi:
int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;

Other applications may of course have other dependencies from the actual screen size, but this example should be sufficient to show the general idea. The Javagochi's skin color is dependent on its happiness. If the Javagochi feels well, its skin has a bright yellow color. With decreasing happiness, the Javagochi becomes pale. This is reflected by the following setColor() command:
setColor (255, 255, 28 * javagochi.happiness);

Using the given width and height, you can now implement your first version of the Javagochi's Face class:
import javax.microedition.lcdui.*; class Face extends Canvas implements CommandListener { Javagochi javagochi; Face (Javagochi javagochi) { this.javagochi = javagochi; } public void paint (Graphics g) { g.setColor (255, 255, 255); g.fillRect (0, 0, getWidth (), getHeight ()); int height = Math.min (getHeight (), getWidth ()) / 2; int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT; g.translate (getWidth () / 2, getHeight () / 2); g.setColor (255, 255, 255 - javagochi.getHappiness () * 25); g.fillArc (- width / 2, - height / 2, width, height, 0, 360); g.setColor (0, 0, 0); g.drawArc (- width / 2, - height / 2, width, height, 0, 360); } }

In order to simplify the centered display of the Javagochi, you set the origin of the coordinate system to the center of the screen using the translate() method. The outline of the Javagochi's face is then drawn using the drawArc() method. Unfortunately, the outline of the Javagochi looks a bit boring, so you will add a simple face now. In order to avoid duplicated code, you put the drawing of the eyes in a separate method. The drawEye() method takes the Graphics object, the coordinates of the eye, and a size parameter:
void drawEye (Graphics g, int x, int y, int size) { if (javagochi.isDead ()) { graphics.drawLine (x - size/2, y, x + size/2, y); graphics.drawLine (x, y - size/2, x, y + size/2); } else graphics.drawArc (x-size/2, y-size/2, size, size, 0, 360); }

Now you can insert the rest of the drawing code into the paint() method, just after drawArc(). You will start with the eyes by calling the drawEye() method defined previously. By using fractions of the current width and height of the Javagochi, the eyes are positioned and sized correctly:
drawEye (g, - width / 6, - height / 5, height / 15 + 1); drawEye (g, width / 6, - height / 5, height / 15 + 1);

Now you draw the mouth, depending on the current happiness of the Javagochi. Again, you use fractions of the Javagochi size for positioning and sizing:
switch (javagochi.getHappiness () / 3) { case 0: case 1: g.drawArc (-width/6, height/7, width/3, height/6, 0, 180); break; case 2: g.drawLine (-width/6, height/7, width/6, height/7); break; default: g.drawArc (-width/6, height/7, width/3, height/6, 0, -180); }

Simple Interaction
When you run the first version of the Javagochi application, the Javagochi starts out happy, but dies quickly from starvation. Obviously, you need a way to transfer energy from the device's battery to the Javagochi. One possibility would be to add a corresponding command. However, in the "High-Level API" section you learned that commands may be delegated to a sub-menu. When the Javagochi urgently needs feeding, you would like to be able to react quickly.

So you just use the key event corresponding to the game action FIRE for feeding the Javagochi:
public void keyPressed (int keyCode) { if (getGameAction (keyCode) == FIRE) javagochi.transform (10); }

Now you can save the Javagochi from starvation using the FIRE game key.

Canvas and Text Input


As mentioned in the introduction to interaction, it is not possible to receive composed key events using the low-level API. But what can you do if you need this kind of input, such as for a text input trainer? Let's just assume simple feeding is not enough for your Javagochi. Depending on its current state, it needs special vitamins, denoted by letters ranging from A to Z. On phones providing keys 0 through 9 only, this is a problem. The only solution is to emulate the key input mechanism in software. On cellular phones, there are also three to four letters printed on the number keys. In text input mode, pressing a number makes the first letter appear. If the same number is pressed again in a limited period of time, the second letter appears instead of the first one. This way you can cycle through all the letters on a number key. When no key is pressed for about three quarters of a second, or another key is pressed, the letter currently displayed is confirmed as input key. For emulation of this mechanism, you define the letters on the keys 2 through 9 in a array inside the Face class:

String

public static final String[] keys = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};

You also need a timer to measure the time until confirmation of the current key. The timer is stored in keyTimer. The variables keyMajor and keyMinor contain the index in the keys array and the index inside the corresponding string. The variable needed stores the vitamin currently needed by the Javagochi:
Timer keyTimer; int keyMajor = -1; int keyMinor; char needed = 'a';

What do you do if a numeric key is pressed? If you already have a timer running, you cancel it since a key was pressed. Then, you subtract the code of the 2 key from the current key code in order to calculate the index in your key array. If the given event does not represent a numeric key between 2 and 9, you set keyMajor to the special value 1, denoting that no valid character is being entered. Otherwise, you check whether the key is identical to the last key. If so, keyMinor is incremented in order to cycle through the

letters assigned to a single numeric key. If another key is pressed, keyMajor is changed accordingly and keyMinor is set back to 0. A new timer is scheduled for half a second later:
public synchronized void keyPressed (int keyCode) { if (keyTimer != null) keyTimer.cancel (); int index = keyCode - KEY_NUM2; if (index < 0 || index > keys.length) keyMajor = -1; else { if (index != keyMajor) { keyMinor = 0; keyMajor = index; } else { keyMinor++; if (keyMinor >= keys [keyMajor].length ()) keyMinor = 0; } keyTimer = new Timer (); keyTimer.schedule (new KeyConfirmer (this), 500); } repaint (); }

Now you need to implement a timer task that confirms the letter if no other key is pressed for half a second. In that case, the KeyConfirmer class just calls keyConfirmed() in the original Face class:
import java.util.*; public class KeyConfirmer extends TimerTask { Face face; public KeyConfirmer (Face face) { this.face = face; } public void run () { face.keyConfirmed (); } }

Back in the Face class, you can now implement the functionality performed when the letter is finally confirmed. You just compare the letter to the vitamin needed by the Javagochi. If the right vitamin is fed, the weight of the Javagochi is increased 10 units by calling transform():

synchronized void keyConfirmed () { if (keyMajor != -1) { if (keys [keyMajor].charAt (keyMinor) == needed) { javagochi.score += javagochi.getHappiness (); if (!javagochi.isDead ()) needed = (char) ('a' + ((System.currentTimeMillis () / 10) % 26)); javagochi.transform (10); } keyMajor = -1; repaint (); } } }

Finally, you add some status information about the current score and selected key to the Face.paint() method. Just insert the following code at the end of the previous implementation of paint():
String keySelect = ""; if (keyMajor != -1) { String all = keys [keyMajor]; keySelect = all.substring (0, keyMinor) + "[" + all.charAt (keyMinor) + "]" + all.substring (keyMinor+1); } g.drawString ("Feed: " + needed + " " + keySelect, 0, getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER); g.drawString ("Score: "+javagochi.score, 0, -getHeight ()/2, Graphics.TOP|Graphics.HCENTER);

Figure 3.20 shows the Javagochi being fed with vitamins. The complete source code is contained in Listing 3.2.

Figure 3.20 A Javagochi being fed with vitamins.

Listing 3.2 Javagochi.javaThe Complete Javagochi Sample Source Code


import java.util.*;

import javax.microedition.midlet.*; import javax.microedition.lcdui.*;

class Consumption extends TimerTask { Javagochi javagochi; public Consumption (Javagochi javagochi) { this.javagochi = javagochi; }

public void run () { javagochi.transform (-1 - javagochi.score/100 ); } }

class KeyConfirmer extends TimerTask { Face face; public KeyConfirmer (Face face) { this.face = face; } public void run () { face.keyConfirmed (); } }

class Face extends Canvas { public static final String[] keys = {"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; Javagochi javagochi; Timer keyTimer; int keyMajor = -1; int keyMinor; char needed = 'a'; Face (Javagochi javagochi) { this.javagochi = javagochi; } public void paint (Graphics g) { g.setColor (255, 255, 255); g.fillRect (0, 0, getWidth (), getHeight ()); int height = Math.min (getHeight (), getWidth ()) / 2; int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;

g.translate (getWidth () / 2, getHeight () / 2); g.setColor (255, 255, 255 - javagochi.getHappiness () * 25); g.fillArc (- width / 2, - height / 2, width, height, 0, 360); g.setColor (0, 0, 0); g.drawArc (- width / 2, - height / 2, width, height, 0, 360); g.drawString ("Score: "+javagochi.score, 0, -getHeight ()/2, Graphics.TOP|Graphics.HCENTER); String keySelect = ""; if (keyMajor != -1) { String all = keys [keyMajor]; keySelect = all.substring (0, keyMinor) + "[" + all.charAt (keyMinor) + "]" + all.substring (keyMinor+1); } g.drawString ("Feed: " + needed + " " + keySelect, 0, getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER); drawEye (g, - width / 6, - height / 5, height / 15 + 1); drawEye (g, width / 6, - height / 5, height / 15 + 1); switch (javagochi.getHappiness () / 3) { case 0: case 1: g.drawArc (-width/6, height/7, width/3, height/6, 0, 180); break; case 2: g.drawLine (-width/6, height/7, width/6, height/7); break; default: g.drawArc (-width/6, height/7, width/3, height/6, 0, -180); } } void drawEye (Graphics graphics, int x0, int y0, int w) { if (javagochi.isDead ()) { graphics.drawLine (x0 - w/2, y0, x0 + w/2, y0); graphics.drawLine (x0, y0 - w/2, x0, y0 + w/2); } else graphics.fillArc (x0-w/2, y0-w/2, w, w, 0, 360); } public synchronized void keyPressed (int keyCode) { int index = keyCode - KEY_NUM2; if (keyTimer != null) keyTimer.cancel (); if (index < 0 || index > keys.length) keyMajor = -1; else { if (index != keyMajor) {

keyMinor = 0; keyMajor = index; } else { keyMinor++; if (keyMinor >= keys [keyMajor].length ()) keyMinor = 0; } keyTimer = new Timer (); keyTimer.schedule (new KeyConfirmer (this), 500); } repaint (); } synchronized void keyConfirmed () { if (keyMajor != -1) { if (keys [keyMajor].charAt (keyMinor) == needed) { javagochi.score += javagochi.getHappiness (); if (!javagochi.isDead ()) needed = (char) ('a' + ((System.currentTimeMillis () / 10) % 26)); javagochi.transform (10); } keyMajor = -1; repaint (); } } }

public class Javagochi extends MIDlet { static final int IDEAL_WEIGHT = 100; Display display; Face face = new Face (this); int weight = IDEAL_WEIGHT; Timer consumption; int score; public int getHappiness () { int happiness = 20 - (weight > IDEAL_WEIGHT ? 10 * weight / IDEAL_WEIGHT : 10 * IDEAL_WEIGHT / weight); if (happiness < 0) happiness = 0; else if (happiness > 10) happiness = 10; return happiness; } public boolean isDead () { return getHappiness () == 0; }

public void transform (int amount) { if (!isDead ()) { weight += amount; face.repaint (); } } public void startApp () { display = Display.getDisplay (this); display.setCurrent (face); consumption = new Timer (); consumption.scheduleAtFixedRate (new Consumption (this), 500, 500); } public void pauseApp () { consumption.cancel (); } public void destroyApp (boolean forced) { } }

MIDP Programming with J2ME


December 26, 2002 By Sams Publishing Bio Send Email More Articles

Animation
With animation, there are normally two main problems: Display flickering and synchronization of painting with calculation of new frames. We will first address how to get the actual painting and application logic in sync, and then solve possible flickering.

Synchronization of Frame Calculation and Drawing


When you perform animations, you can first calculate the display content and then call repaint() in order to paint the new frame. But how do you know that the call to paint() has finished? One possibility would be to call serviceRepaints(), which blocks until all pending display updates are finished. The problem with serviceRepaints() is that paint() may be called from another thread. If the thread calling serviceRepaints() holds any locks that are required in paint(), a deadlock may occur. Also, calling serviceRepaints() makes sense only from a thread other than

the event handling thread. Otherwise, key events may be blocked until the animation is over. An alternative to serviceRepaints() is calling callSerially() at the end of the paint() method. The callSerially() method lets you put Runnable objects in the event queue. The run() method of the Runnable object is then executed serially like any other event handling method. In the run() method, the next frame can be set up, and a new repaint can be requested there. To demonstrate this execution model, you will build a simple stopwatch that counts down a given number of seconds by showing a corresponding pie slice using the fillArc() method, as shown in Figure 3.21.

Post a comment Email Article Print Article Share Articles o Digg o del.icio.us o Slashdot o DZone o Reddit o StumbleUpon o Facebook o FriendFeed o Furl o Newsvine o Google o LinkedIn o MySpace o Technorati o Twitter o Windows Live o YahooBuzz

Figure 3.21 A very simple stopwatch.

The Canvas implementation stores the current slice in degree, the start time, the total amount of seconds and the MIDlet display in local variables. In order to make use of callSerially(), your Canvas implements the Runnable interface:
class StopWatchCanvas extends Canvas implements Runnable { int degree = 360; long startTime; int seconds; Display display;

When the StopWatchCanvas is created, you store the given display and seconds. Then, the current time is determined and stored, too:
StopWatchCanvas (Display display, int seconds) { this.display = display; this.seconds = seconds; startTime = System.currentTimeMillis (); }

In the paint() method, you clear the display. If you need to draw more than 0 degrees, you fill a corresponding arc with red color and request recalculation of the pie slice using callSerially(). Finally, you draw the outline of the stopwatch by setting the color to black and calling drawArc():
public void paint (Graphics g) { g.setGrayScale (255); g.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g.setColor (255, 0, 0); g.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g.setGrayScale (0); g.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360); }

This method is invoked by the event handling thread as a result of the previous display.callSerially(this) statement. In this case, it just calculates a new pie slice and requests a repaint():
public void run () { int permille = (int) ((System.currentTimeMillis () - startTime) / seconds); degree = 360 - (permille * 360) / 1000; repaint (); } }

As always, you need a MIDlet to actually display your StopWatchCanvas implementation. The following code creates a stopwatch set to 10 seconds when the application is started:
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class StopWatch extends MIDlet { public void startApp () { Display display = Display.getDisplay (this); display.setCurrent (new StopWatchCanvas (display, 10)); } public void pauseApp () { } public void destroyApp (boolean forced) { } }

Avoiding Flickering
On some devices, the stopwatch implementation will flicker. This is due to the fact that the display is cleared completely before a new stopwatch is drawn. However, on some other devices, the stopwatch will not flicker because those devices provide automated double buffering. Before the screen is updated, all drawing methods are performed in a hidden buffer area. Then, when the paint() method is finished, the complete display is updated from the offscreen buffer at once. The method isDoubleBuffered() in the Canvas class is able to determine whether the device screen is double buffered. In order to avoid flickering of your animation in all cases, you can add your own offscreen image, which is allocated only if the system does not provide double buffering:
Image offscreen = isDoubleBuffered () ? null : Image.createImage (getWidth (), getHeight ());

In the paint() method, you just check if the offscreen image is not null, and if so, you delegate all drawing to your offscreen buffer. The offscreen buffer is then drawn immediately at the end of the paint() method, without first clearing the screen. Clearing the screen is not necessary in that case since the offscreen buffer was cleared before drawing and it fills every pixel of the display:
public void paint (Graphics g) { Graphics g2 = offscreen == null ? g : offscreen.getGraphics (); g2.setGrayScale (255); g2.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g2.setColor (255, 0, 0);

g2.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g2.setGrayScale (0); g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360); if (offscreen != null) g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT); }

Listing 3.3 gives the complete source code for the buffered stopwatch.

Listing 3.3 BufferedStopWatch.java The Complete Source Code of the Buffered Stopwatch
import javax.microedition.midlet.*; import javax.microedition.lcdui.*; class BufferedStopWatchCanvas extends Canvas implements Runnable { int degree = 360; long startTime; int seconds; Display display; Image offscreen; BufferedStopWatchCanvas (Display display, int seconds) { this.display = display; this.seconds = seconds; if (!isDoubleBuffered () && false) offscreen = Image.createImage (getWidth (), getHeight ()); startTime = System.currentTimeMillis (); } public void paint (Graphics g) { Graphics g2 = offscreen == null ? g : offscreen.getGraphics (); g2.setGrayScale (255); g2.fillRect (0, 0, getWidth (), getHeight ()); if (degree > 0) { g2.setColor (255, 0, 0); g2.fillArc (0,0, getWidth (), getHeight (), 90, degree); display.callSerially (this); } g2.setGrayScale (0); g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360);

if (offscreen != null) g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT); } public void run () { int permille = (int) ((System.currentTimeMillis () - startTime) / seconds); degree = 360 - (permille * 360) / 1000; repaint (); } }

public class BufferedStopWatch extends MIDlet { public void startApp () { Display display = Display.getDisplay (this); display.setCurrent (new BufferedStopWatchCanvas (display, 10)); } public void pauseApp () { } public void destroyApp (boolean forced) { } }

Summary
In this chapter, you learned the general life cycle of MIDP applications. You know how to build a user interface using the high-level lcdui widgets, and how to interact using the listener mechanism. You have learned to perform custom graphics using the low-level API, including device-independent flicker-free animation and coordination of graphics calculation and drawing. The next chapter gives a corresponding overview of the PDAP life cycle and user interface. The PDAP introduction focuses on the differences between the J2SE AWT classes and the subset included in PDAP, but still gives a basic introduction to AWT programming.

About the Authors


Michael Kroll and Stefan Haustein are the creators of the kAWT-Project, an abstract window toolkit designed to allow graphical J2ME programming on devices using the KVM. Since the first release of KVM, both have been members of the Expert group specifying the J2ME PDA Profile. Michael Kroll's experience in professional J2ME programming includes an application for viewing biosignals like ECGs and EEGs on a Palm organizer. Michael is working for Visus Technology Transfer in Germany developing a Java based radiology imaging system called JiveX.

Stefan Haustein studied Computer Science at the University of Dortmund, and is working on his Ph.D in the AI-Unit. He wrote his diploma thesis in the Neuros-Project at the "Institut fur Neuroinformatik" at the University of Bochum about graph-based robot navigation.

Preface
This is Part 1 of a two-part lesson in a series of tutorial lessons designed to teach you how to write programs using the Sun Java Wireless Toolkit for CLDC. The first lesson was titled Getting Started with MIDlets and the Sun Java Wireless Toolkit for CLDC. The previous lesson was titled Programming MIDlets for Interactive Behavior (see Resources). What you will learn In this lesson, you will learn how to use several methods of the Canvas class (including paint and repaint), along with several methods of the Graphics class to draw text and lines on the cell phone screen. You will learn how to use anchor point coordinates to control the position of text that is drawn on the screen. You will also learn how to handle keyPressed events fired by a Canvas object. I recommend that you open another copy of this document in a separate browser window and use the following links to easily find and view the figures and listings while you are reading about them.

Figures

Figure 1. Partial class hierarchy for MIDP 2.0. Figure 2. Output from MIDlet named Canvas01 in Sun emulator. Figure 3. Sun cell phone emulator output for the MIDlet named Canvas02.

Listings

Listing 1. The main class for the MIDlet named Canvas01. Listing 2. Beginning of the member class named MyCanvas. Listing 3. Draw eight lines and eight text strings. Listing 4. Draw two more lines. Listing 5. Beginning of the class for the MIDlet named Canvas02. Listing 6. Remainder of the main class for the MIDlet named Canvas02. Listing 7. Beginning of the member class named MyCanvas. Listing 8. Beginning of the overridden paint method. Listing 9. Remainder of the overridden paint method. Listing 10. Overridden keyPressed method. Listing 11. Source code for the MIDlet named Canvas01. Listing 12. Source code for the MIDlet named Canvas02.

Supplementary material
I recommend that you also study the other lessons in my extensive collection of online Java tutorials. You will find a consolidated index at www.DickBaldwin.com.

General background information


If you are familiar with graphics programming in J2SE without the benefit of Java 2D and Java 3D, you will probably be reasonably comfortable with the material in this lesson. J2ME graphics is very similar to graphics programming in the early days of Java, but with some interesting new wrinkles. Partial class hierarchy for MIDP 2.0 A partial class hierarchy for MIDP 2.0 is shown in Figure 1. Figure 1 includes all of the classes that are included in the package named javax.microedition.lcdui. In addition, Figure 1 includes three classes that are in other packages. Figure 1. Partial class hierarchy for MIDP 2.0.

Object
o

Displayable Screen TextBox Alert List Form Canvas (abstract) GameCanvas (abstract) Display Ticker AlertType Image Item Gauge ChoiceGroup CustomItem DateField ImageItem Spacer StringItem TextField Timer TimerTask Command Graphics

o o o o o

o o o o

Font

Classes to be illustrated I have discussed and illustrated the classes shown in boldface Italics (plus the Choice interface, the CommandListener interface, and the ItemCommandListener interface) in earlier lessons (see Resources). I will discuss and illustrate the following classes in this lesson:

Canvas class Graphics class Font class

I will discuss and illustrate the GameCanvas class in a future lesson. I will leave CustomItem as an exercise for the student.

The Canvas class


Here is part of what Sun has to say about the Canvas class: "The Canvas class is a base class for writing applications that need to handle low-level events and to issue graphics calls for drawing to the display. Game applications will likely make heavy use of the Canvas class. From an application development perspective, the Canvas class is interchangeable with standard Screen classes, so an application may mix and match Canvas with high-level screens as needed. For example, a List screen may be used to select the track for a racing game, and a Canvas subclass would implement the actual game." The use of the Canvas class and the GameCanvas class for programming games will be the topic of a future lesson. In this lesson, I will concentrate on using the Canvas class for presenting graphic information on the cell phone screen, and handling key events that are fired by a Canvas object. Yes, I really did mean key events Unlike the commands that we studied in an earlier lesson, the Canvas class provides the ability to detect and handle key events in a manner that is very similar, but not identical to the JavaBeans or Delegation event model in J2SE (see Resources). Pointer events The Canvas class also provides the ability to detect and handle pointer (mouse like) events on hardware implementations that support them. However, in these lessons, I am constrained to the use of the Sun cell phone emulator, which does not appear to support

pointer events. (If it does support pointer events, I haven't figured out how to make it do so.) Therefore, I will have very little, if anything, to say about pointer events. Canvas also supports commands Just like other Displayable objects, Canvas objects also support commands such as EXIT, BACK, OK, etc. (see Programming MIDlets for Interactive Behavior in Resources). Therefore, you can mix command programming and real event-driven programming for Canvas objects. Canvas is an abstract class The Canvas class is abstract, so you must extend it in order to use it. The reason for this is that the primary use of the Canvas class is to serve as a surface on which to draw. In order to draw, you must override the paint method of the Canvas class. The only way to override a method is to extend the class to which the method belongs. Comparison with J2SE When programming in J2SE (using the JavaBeans source-listener event model), if you want to detect and handle key events on an object that is the source of events, you must:

Define a class that implements the KeyListener interface. Instantiate an object of that class. Register the listener object on the source object.

Event handler methods, such as keyPressed are declared in the KeyListener interface and must be defined in your new class. That is not the case with MIDP 2.0. The Canvas class already defines the following empty event-handler methods:

keyPressed keyReleased keyRepeated pointerDragged pointerPressed pointerReleased

Handling an event To handle one of the events in the above list, you must override the corresponding method in your subclass of the Canvas class, writing the desired behavior into your overridden method. If you don't want to handle any of the events in the above list, just forget about the ones that you don't want to handle. Unlike the JavaBeans event model, it is not necessary to define empty methods for the events that you don't want to handle. More information from Sun

According to Sun, "Applications receive keystroke events in which the individual keys are named within a space of key codes. Every key for which events are reported to MIDP applications is assigned a key code. The key code values are unique for each hardware key unless two keys are obvious synonyms for each other." Sun goes on to tell us, "MIDP defines the following key codes: KEY_NUM0, KEY_NUM1, KEY_NUM2, KEY_NUM3, KEY_NUM4, KEY_NUM5, KEY_NUM6, KEY_NUM7, KEY_NUM8, KEY_NUM9, KEY_STAR, and KEY_POUND. (These key codes correspond to keys on a ITU-T standard telephone keypad.)" Public static final variables The Canvas class provides a public static final variable (constant) for each of the key codes listed above. Finally, Sun tells us, "Other keys may be present on the keyboard, and they will generally have key codes distinct from those list above. In order to guarantee portability, applications should use only the standard key codes." Did not adhere to the rule As you will see later, I did not adhere to this rule in one of the sample MIDlets. I wasn't interested in achieving portability. Rather, I was mainly interested in clarity and I found it easier to be clear using the following key names (which may be peculiar to the Sun emulator) in the MIDlet:

LEFT RIGHT UP DOWN SELECT

These names apply to the arrow keys and the large key in the middle of the arrow keys on the Sun cell phone emulator keypad (see Figure 2).; As mentioned above, they may not apply to similar keys in other cell phone emulators or in real cell phones for that matter. (There is a better way to deal with the arrow keys, which you will learn about in a future lesson on the GameCanvas class.) Normal versus full-screen mode Here is some of what Sun has to say on this topic:

"A Canvas can be in normal mode or in full-screen mode. In normal mode, space on the display may be occupied by command labels, a title, and a ticker. By setting a Canvas into full-screen mode, the application is requesting that the Canvas occupy as much of the display space as is possible. In full-screen mode, the title and ticker are not displayed even if they are present on the Canvas, and Commands may be presented using some alternative means (such as through a pop-up menu)." A Canvas object is in normal mode by default. You will see an example of normal and full-screen modes in one of the sample MIDlets in this lesson. Numerous methods are defined in the Canvas class. I will illustrate the use of many of those methods in the sample MIDlets in this lesson.

The Graphics class


Sun describes this class as follows: "Provides simple 2D geometric rendering capability." While true, this statement hardly does justice to the importance of the Graphics class. No public constructor The Graphics class doesn't have a public constructor, so you can't directly instantiate an object of the Graphics class. I am aware of at least two ways to get access to an object of the class. The overridden paint method The paint method that I mentioned earlier is a callback method. Whenever your Canvas object is visible on the screen and the system needs to display graphics information on that canvas, it will call your overridden paint method. When your overridden paint method is called, it will receive a reference to an object of Graphics class. You can think of that object as a drawing surface that represents the screen. When you call the various methods of the Graphics class to draw pictures on the Graphics object, those pictures will appear on the screen (assuming that your MIDlet currently has access to the screen). The createImage and getGraphics methods As you will see in one the sample MIDlets in this lesson, you can use the createImage and getGraphics methods together to get access to an off-screen drawing area that is represented by a Graphics object. When you use the various methods of the Graphics class to draw on that Graphics object, the results are not immediately visible. Rather,

you are simply drawing in memory when you do that and only you know what is happening. This makes it possible for you to compose a complex drawing in memory and then transfer it to the Canvas object (the screen) very quickly by calling the drawImage method on the Canvas passing the off-screen image's reference as a parameter. This is often a more visually pleasing approach than composing a complex drawing on the screen in full view of the user, particularly if the time required to compose the drawing is significant. Drawing primitives The methods of the Graphics class provide drawing primitives for text, images, lines, rectangles, and arcs. Rectangles and arcs may be filled with a solid color. Rectangles may also be specified with rounded corners. The coordinate system The origin of the default coordinate system is at the upper left-hand corner of the screen or off-screen drawing area. The X-axis direction is positive towards the right, and the Yaxis direction is positive downwards. You can translate the origin to some other location if you want to by calling the translate method. For example, by using the translate method and the appropriate arithmetic operations, you could cause the origin to be relocated to the center of the screen with the positive Yaxis direction going up the screen instead of down. You can assume that horizontal and vertical distances in the coordinate system represent equal distances on the actual device display. In other words, if you draw a square, it won't be turned into a rectangle and if you draw a circle, it won't be turned into an ellipse. All coordinates are specified as integers. Drawing text When a character is painted, the pixels forming the character's shape are filled with the Graphics object's current color. The pixels that are not part of the character's shape are left untouched. Several methods are available to draw text. As far as I know, you can only draw text horizontally. Line stroke styles What about circles? Circles are constructed from 360degree circular arcs. Note that for the case of the drawArc method, angles are specified in degrees instead of radians.

Lines, arcs, rectangles, and rounded rectangles may be drawn with either a SOLID or a DOTTED stroke style. The default is SOLID. You can change the style by calling the setStrokeStyle method, which doesn't affect fill, text, and image operations. Lines, arcs, rectangles, and rounded rectangles are drawn with a stroke that is one pixel wide. If you need additional width, you must draw two or more lines in parallel. Clipping Sun tells us, "The clip is the set of pixels in the destination of the Graphics object that may be modified by graphics rendering operations." To make a long story short, there is a single clip per Graphics object, and it is a rectangle that you can set with a call to the setClip method. The only pixels modified by graphics operations are those that lie within the clip rectangle. Pixels outside the clip rectangle are not modified by any graphics operations. Anchor points One of the more complex aspects of the MIDP 2.0 graphics drawing system is the concept of anchor points. Anchor points apply when you are drawing text, images, regions, and areas. (They do not apply when drawing lines, arcs, rectangles, and rounded rectangles.) Later, I will present and explain a sample MIDlet named Canvas01 that illustrates what is meant by anchor points in conjunction with the drawing of text. (The interpretation is generally the same when drawing images, etc.) The output from the MIDlet, when run in the Sun cell phone emulator, is shown in Figure 2. I will explain the output from the MIDlet here, and explain the code that produced that output later. Figure 2. Output from MIDlet named Canvas01 in Sun emulator.

Drawing parameters When you call the drawString method to draw text on the screen, you must provide the following four parameters:

No anchor point in J2SE graphics The anchor parameter is not used in the argument list of the drawString method of J2SE version 1.6.

String string - the text string to be drawn. int x - a horizontal coordinate value. int y - a vertical coordinate value. int anchor - an anchor point value.

The big question... The parameter values x and y clearly specify a point in the Cartesian coordinate system. The big question is what does the location of that point in the coordinate system have to do with the location of the text (or the image, etc.)? Allowable anchor-point values The allowable anchor-point values are given below in terms of public static final variables (constants) of the Graphics class: Horizontal Anchor-Point Values

LEFT HCENTER VCENTER RIGHT

Vertical Anchor-Point Values


TOP BASELINE BOTTOM

(Note that VCENTER can be used with images, but cannot be used with text. BASELINE can be used with text but cannot be used with images.) Must construct a bitwise inclusive OR value The value that is passed as the anchor parameter to the drawString method must be constructed as the bit-wise inclusive OR of one of the horizontal anchor-point values (excluding VCENTER for text) and one of the vertical anchor-point values (excluding BASELINE for images). The value zero may also be passed as the anchor parameter and is interpreted the same as the value that would be constructed from TOP | LEFT.

Eight calls to the drawString method Now turn your attention to Figure 2. When we examine the code later, you will see that this MIDlet called the drawString method eight times in succession. Each time the method is called the string that is passed as the first parameter is the text displayed on a single line on the screen in Figure 2. The value that is passed as anchor is constructed according to that text (exclusive of the part of the text that reads "y-," more on this later). For example, the first call to the drawString method constructs the anchor value as TOP|RIGHT and the text in Figure 2 reads y-TOP|RIGHT. The coordinate values for the first call to drawString The x and y coordinate values passed to the method specify the point identified by the intersection of two lines immediately above the text at the right end of the text. In other words, the specified value for anchor instructs the system to display the text string below and to the left of the point at the specified coordinate values. Compare intersections of lines with the text If you follow the lines down the screen and compare the intersection of each pair of two lines with the construction of the anchor value given by the text, you should be able to see the correlation between the location of the specified point in the coordinate system and the location of the text string relative to that point. TOP Note that in all cases that use TOP for the construction of anchor, there is some space between the actual top of the upper case characters and the vertical location of the point in the coordinate system. BOTTOM Note also that in all cases that use BOTTOM for the construction of anchor there is some space between the bottom of the characters and the vertical location of the point. This is true even when the text includes a lower case y with a descender. BASELINE However, when BASELINE is used for the construction of anchor, there is no space between the bottom of the characters and the vertical location of the point. In other words, when BASELINE is used for the vertical placement, that is interpreted to mean that the bottom of the characters should coincide with that vertical coordinate with no blank space in between. When there are no descenders, the bottoms of the characters without descenders match the BASELINE value. When there are descenders, the bottom of the lowest descender matches the BASELINE value.

On the other hand, both BOTTOM and TOP allow for some blank space at the bottom and top of the characters. RIGHT, LEFT, and HCENTER RIGHT and LEFT mean what they say. As you can see, no blank space margin is allowed. HCENTER means that the horizontal center of the text should occur at the specified horizontal coordinate. Interpretation for images As mentioned earlier, the same concept applies to the placement of images with the added benefit of being able to use VCENTER to specify that the image should be centered vertically on the specified vertical coordinate value. (I will illustrate this in one of the sample MIDlets in this lesson.) On the other hand, BASELINE cannot be used with images.

Preview
I will present and explain two sample MIDlets named Canvas01 and Canvas02 in Part 1 of this lesson, and will present and explain a third sample MIDlet named Canvas03 in Part 2 of this lesson. Canvas01 The purpose of the MIDlet named Canvas01 is to illustrate the use of aCanvas object and a Graphics object in a very simple and understandable way. Canvas02 The purpose of the MIDlet named Canvas02 is to illustrate the detection and handling of key events on a Canvas object. The MIDlet also demonstrates certain aspects of the fullscreen mode versus the normal mode. Canvas03 The purpose of this MIDlet named Canvas03 is to illustrate the following major programming concepts involving the use of the Canvas class and the Graphics class:

Creating an off-screen image. Importing Image files and drawing them in specified locations on the off-screen image. Drawing a variety of shapes in different colors on the off-screen image, including the following: o Rectangle o Filled triangle

Full circular arc Rounded rectangle Drawing the off-screen image onto the Canvas in a specified location. Handling keyPressed events on the Canvas to cause the off-screen image to be drawn at different locations on the Canvas as a result of the user having pressed the following keys (simple animation): o RIGHT o LEFT o UP o DOWN o SELECT
o o

In addition, the MIDlet named Canvas03 implements an EXIT command that can be used to terminate the MIDlet and cause it to enter the destroyed state.

Discussion and sample code


The MIDlet named Canvas01
The purpose of this MIDlet is to illustrate the use of a Canvas object and a Graphics object in a very simple and understandable way. Anchor point constants The MIDlet further illustrates the use of anchor point constants to establish the alignment of drawn text. The output from the MIDlet running in the Sun cell phone emulator is shown in Figure 2. Clear the screen The MIDlet begins by painting the entire screen white. Otherwise, the new drawing would appear on top of what was previously on the screen. Then it sets the drawing color to black. Draw the text and the lines Following that, the MIDlet draws eight pairs of graphic objects. Each pair consists of:

MIDlet testing All of the MIDlets in this lesson were tested using a Java SE 6 compiler, targeted at a V1.4 virtual machine, and WTK 2.5.2 running under Windows XP.

A line from 0,0 to a specific location on the canvas. A string of text with the anchor point coordinates set to match the end of the line.

The text contents of each string give the combination of anchor point constants used to position that particular string. Some text strings also begin with a lower case y- to show the effect of descenders on the vertical alignment of the text string.

The anchor point constants The eight pairs of graphic objects illustrate the effect of different combinations of the following anchor point constants as well as the impact of lower-case descenders on the alignment produced by the constants:

TOP BASELINE BOTTOM LEFT HCENTER RIGHT

Combining anchor point constants An anchor point must be specified for each text string that is drawn. The anchor point must be produced by the bitwise inclusive OR of one of the top three (vertical) anchor constants in the above list and one of the bottom three (horizontal) anchor constants. For example, the following would be a valid anchor point: value to pass to the drawString method: TOP|RIGHT Determining the position and the alignment of the text The actual position and the alignment of the text string are determined by the combination of a pair of specified coordinate values and the bitwise inclusive OR of two anchor constants. The coordinates specify the position that will be assumed by a particular point in the text string. The bitwise inclusive OR of two anchor constants determines which point in the text string will be located at the specified coordinates. The special anchor point value of 0 is interpreted to mean TOP|LEFT. Two more straight lines Another pair of straight lines is used to connect the ends of the sloping lines that originate in the upper left corner of the screen. This makes it easier to visually determine the location of the anchor point in each text string. Methods that are used This MIDlet illustrates the use of the following methods from the Graphics class:

setColor fillRect drawLine

drawString

The MIDlet also illustrates the use of the following methods from the Canvas class:

paint repaint getWidth getHeight

Display for twenty seconds The MIDlet displays the canvas for 20 seconds to give you a chance to view it and then enters the destroyed state. The main class for the MIDlet named Canvas01 The MIDlet class is shown in its entirety in Listing 1. Listing 1. The main class for the MIDlet named Canvas01.
public class Canvas01 extends MIDlet{ Canvas myCanvas; public Canvas01(){ System.out.println("Construct MIDlet"); myCanvas = this.new MyCanvas(); //Guarentee that the screen gets painted. myCanvas.repaint(); }//end constructor public void startApp(){ //Make the Canvas the current display. Display.getDisplay(this).setCurrent(myCanvas); //Sleep for 20 seconds. try{Thread.currentThread().sleep(20000); } catch(Exception e){} //Enter the destroyed state. this.destroyApp(true); }//end startApp public void pauseApp(){ }//end pauseApp public void destroyApp(boolean unconditional){ System.out.println("Destroy MIDlet"); notifyDestroyed(); }//end destroyApp

If you have studied the previous lessons in this series, the only new material in Listing 1 should be:

The instantiation of an object of a member class named MyClass. The call to the repaint method belonging to that object.

Java graphics 101 As you will see later, the new class overrides the paint method inherited from the Canvas class. The code in the paint method produces the graphics on the screen. However, the paint method is a callback method and MIDlet code normally doesn't call it directly (although there may be exceptions to this rule of thumb). Rather, the MIDlet code normally calls the repaint method. This sends a request to the virtual machine to execute the paint method. This allows the virtual machine and the operating system to act as a traffic cop forcing the different applications needing access to the screen to play nice and take turns. The call to the repaint method in Listing 1 is probably superfluous. The screen would probably be painted by executing the paint method anyway with or without this call to repaint. The call to repaint was included in Listing 1 simply to guarantee that the paint method does get called when the MIDlet becomes active. (Another sample MIDlet in this lesson illustrates the requirement to call the repaint method to cause the screen to be repainted with new information.) Beginning of the member class named MyCanvas Listing 2 shows the beginning of the member class named MyCanvas. This class extends the Canvas class and overrides the paint method. No requirement for a member class The class named MyCanvas was defined as a member class for programming convenience only. There was no technical requirement for making it a member class.

Listing 2. Beginning of the member class named MyCanvas.


class MyCanvas extends Canvas{ public void paint(Graphics g){ //Paint the screen white g.setColor(0xffffff); g.fillRect(0,0,getWidth(),getHeight()); //Set the drawing color to black. g.setColor(0x000000);

Clear the screen Listing 2 begins by painting the entire screen white. Otherwise, the new drawing would simply appear over top of what was previously on the screen. Even though that was not

desired in this MIDlet, drawing on top of the existing material on the screen can sometimes be useful. For example, that would make it possible to combine a TextBox object and a Canvas object on the same screen. Set the drawing color After painting the screen white, Listing 2 sets the drawing color to black. This color will be used for all subsequent drawing until the drawing color is changed to some other color. Draw eight graphic objects Listing 3 draws the eight lines that originate in the upper left corner along with the eight text strings shown in Figure 2. Listing 3. Draw eight lines and eight text strings.
g.drawLine(0,0,200,20); g.drawString("y-TOP|RIGHT",200,20, Graphics.TOP|Graphics.RIGHT); g.drawLine(0,0,150,40); g.drawString("y-TOP|HCENTER",150,40, Graphics.TOP|Graphics.HCENTER); g.drawLine(0,0,100,60); g.drawString("y-TOP|LEFT",100,60, Graphics.TOP|Graphics.LEFT); g.drawLine(0,0,100,90); g.drawString("y-BOTTOM|LEFT",100,90, Graphics.BOTTOM|Graphics.LEFT); g.drawLine(0,0,100,110); g.drawString("BOTTOM|LEFT",100,110, Graphics.BOTTOM|Graphics.LEFT); g.drawLine(0,0,100,130); g.drawString("y-BASELINE|LEFT",100,130, Graphics.BASELINE|Graphics.LEFT); g.drawLine(0,0,100,150); g.drawString("BASELINE|LEFT",100,150, Graphics.BASELINE|Graphics.LEFT); g.drawLine(0,0,100,170); g.drawString("Special Case of Zero",100,170,0);

No Color class available Unlike J2SE, MIDP 2.0 does not provide a Color class containing constants for use in setting color values. Therefore, you must set the color using a mixture of red, green, and blue values as shown in Listing 2.

Drawing on a Graphics object

Note that the overridden paint method receives an incoming parameter of type Graphics known locally simply as g. This is a reference to an object of the class Graphics and you can think of the Graphics object as representing the screen. Whatever you draw on the Graphics object will be rendered on the screen. The drawLine method of the Graphics class is called eight times to draw the eight lines that originate at the origin and end at a particular location on the screen. The drawString method is also called eight times to draw the eight text strings with the anchor point coordinate values matching the ends of the corresponding lines. Each time the drawString method is called, two anchor point constants are combined using a bitwise inclusive OR to provide the value that is passed as the fourth parameter. As explained earlier, this value is used to determine how the text string will be positioned relative to the point defined by the specified coordinate values. Draw two more lines Listing 4 draws two additional lines that connect the ends of the sloping lines that originate in the upper left corner of the screen. Listing 4. Draw two more lines.
g.drawLine(200,20,100,60); g.drawLine(100,60,100,170); }//end overridden paint method }//end member class MyCanvas }//end class Canvas01

Listing 4 also signals the end of the overridden paint method and the end of the class named Canvas01.

The MIDlet named Canvas02


The purpose of this MIDlet is to illustrate the detection and handling of key events on a Canvas object. The MIDlet also demonstrates certain aspects of the full-screen mode versus the normal mode. A Canvas object is instantiated and set to full-screen mode. The inherited keyPressed method is overridden to:

Capture each user keystroke, Convert the key code to a key name. Add the key name to a Vector object. Cause the screen to be repainted each time a keyPressed event occurs.

The paint method is overridden to:


Clear the canvas. Extract each of the key names from the Vector. Display the key names in two columns on the screen by using the drawString method to draw the key names on the Canvas object.

Sun cell phone emulator output for the MIDlet named Canvas02 Figure 3 shows an output from the MIDlet when run in the Sun cell phone emulator for a particular sequence of keystrokes on the emulator keypad. Figure 3. Sun cell phone emulator output for the MIDlet named Canvas02.

Keys that don't fire events You can terminate the MIDlet that is running in the emulator by clicking the button on the right that has a picture of an old-fashioned telephone receiver in a horizontal position. (When you point to this button, the picture of the telephone receiver should turn red.) This button doesn't fire a keyPressed event. (At least it doesn't fire one that you can handle). The key on the left with the picture of the house also doesn't fire a keyPressed event.

The key pressed order Figure 3 shows the result of starting at the top left soft key and pressing keys going from left to right, top to bottom, excluding the two keys mentioned above. The names of most of the keys in the Sun emulator are pretty easy to correlate with the label on the key. Two that may not be clear are SELECT and CLEAR. The SELECT key is the large square key in the middle of the arrow keys. The CLEAR key is the key on the right with the picture of the audio speaker. Comparing full-screen and normal mode You can see one of the differences between full-screen mode and normal mode by comparing Figure 3, (which is in full-screen mode) and Figure 2, (which is in normal mode). Since neither of these MIDlets displays a title and a ticker, the main difference between Figure 2 and Figure 3 is the disappearance of the gray bar at the bottom of the screen in Figure 3 where soft key commands normally appear. (I will have more to say about this later.) Beginning of the class for the MIDlet named Canvas02 Listing 5 shows the beginning of the class for the MIDlet named Canvas02 including the constructor. Listing 5. Beginning of the class for the MIDlet named Canvas02.
public class Canvas02 extends MIDlet{ Canvas myCanvas; public Canvas02(){//Constructor System.out.println("Construct MIDlet"); myCanvas = this.new MyCanvas(); //Guarentee that the screen gets painted. myCanvas.repaint(); //Add three commands to the Canvas myCanvas.addCommand(new Command("EXIT", Command.EXIT,2)); myCanvas.addCommand(new Command("BACK", Command.BACK,2)); myCanvas.addCommand(new Command("OK",Command.OK,2)); }//end constructor

Three commands are added Note that the constructor in Listing 5 adds three commands to the Canvas object. However, even if visible, the commands could not be activated because there is no CommandListener registered on the Canvas object. The commands were added to the Canvas solely to demonstrate the impact of full-screen mode versus normal mode. As

mentioned earlier, when the Canvas object is set to full-screen mode, as is the case for this MIDlet, the commands are not visible. Accessing the commands If the Canvas is switched to normal mode, the commands are displayed on the gray bar that normally appears at the bottom of the screen and also in a command menu accessible from the gray bar. You can demonstrate this behavior by modifying the code to pass true or false to the setFullScreenMode method in the MyCanvas constructor that I will discuss later. Alternative command presentation The Sun documentation indicates that in full-screen mode, "Commands may be presented using some alternative means (such as through a pop-up menu)." If the Sun cell phone emulator provides such an alternative presentation, I haven't figured out what it is.

Remainder of the main class for the MIDlet named Canvas02 Listing 6 shows the remainder of the main class for the MIDlet named Canvas02. Listing 6. Remainder of the main class for the MIDlet named Canvas02.
public void startApp(){ //Make the Canvas the current display. Display.getDisplay(this).setCurrent(myCanvas); }//end startApp public void pauseApp(){ }//end pauseApp public void destroyApp(boolean unconditional){ System.out.println("Destroy MIDlet"); notifyDestroyed(); }//end destroyApp

There is nothing new in this code so further explanation should not be necessary. Beginning of the member class named MyCanvas Listing 7 shows the beginning of the member class named MyCanvas, including the constructor. Listing 7. Beginning of the member class named MyCanvas.
class MyCanvas extends Canvas{ Vector vect = new Vector(); MyCanvas(){//constructor this.setFullScreenMode(true);

}//end constructor

The constructor calls the method named setFullScreenMode to cause the Canvas to appear on the screen in full-screen mode. If you change the parameter from true to false, the Canvas will revert to normal mode. If what you need is normal mode, you don't need to call this method because the Canvas is in normal mode by default. Beginning of the overridden paint method Listing 8 shows the beginning of the overridden paint method. The method begins by painting the screen white and then setting the drawing color to black. Following this, the method declares some working variables and initializes some of them. Listing 8. Beginning of the overridden paint method.
public void paint(Graphics g){ //Paint the entire screen white. g.setColor(0xffffff); g.fillRect(0,0,getWidth(),getHeight()); g.setColor(0x000000);//Set drawing color to black //Declare and initialize some working variables. String keyData = null; int fontHeight = g.getFont().getHeight(); //Number of lines of text that fit on screen. int cntLimit = this.getHeight()/fontHeight; int yOffset = 0; int xOffset = 0;

A two-column display Most of the complexity of this method resides in the requirement to create two columns for drawing the text strings. This requires knowledge of the number of strings that will fit from top to bottom on the screen, which in turn requires knowledge of the vertical space occupied by each character. The code in Listing 8 gets the font height and performs that calculation. Remainder of the overridden paint method Listing 9 displays the data captured from the user keyboard input in two columns. When the length of the first column exceeds the height of the screen, the display wraps and starts a new column as shown in Figure 3. If the length of the second column exceeds the height of the screen, the data will simply be lost off the bottom of the screen. Listing 9. Remainder of the overridden paint method.
for(int cnt = 0;cnt < vect.size();cnt++){

keyData = (String)vect.elementAt(cnt);//note cast if(cnt * fontHeight < this.getHeight() fontHeight){ //Display in first column. yOffset = cnt * fontHeight; xOffset = 5; }else{ //Display in second column yOffset = (cnt - cntLimit) * fontHeight; xOffset = this.getWidth()/2; }//end else //Draw the string on the Canvas. g.drawString(keyData,xOffset,yOffset,0); }//end for loop }//end overridden paint method

Drawing from top to bottom of the screen Note that a value of 0 is passed as the fourth parameter to the drawString method. This causes the anchor point location to default to TOP|LEFT. Since the direction for increasing values of the Y-coordinate is down the screen, the code in Listing 9 extracts the text data from the Vector and draws the text on the screen moving from top to bottom. Overridden keyPressed method The keyReleased method When the MyCanvas object is on the screen and the user releases one of the keys, the keyReleased method is called. In this MIDlet, the empty keyReleased method is not overridden. Calling the empty method is of no consequence. It simply returns quietly.

The MyCanvas class inherits an empty method named keyPressed from the Canvas class and overrides that method. Whenever the MyCanvas object is being displayed on the screen and the used presses one of the keys shown in Figure 3, the Canvas object "fires" a keyPressed event, causing the overridden keyPressed method to be called. The overridden keyPressed method is shown in Listing 10. Listing 10. Overridden keyPressed method.
public void keyPressed(int keyCode){ vect.addElement(getKeyName(keyCode)); this.repaint(); }//end keyPressed }//end member class MyCanvas }//end class Canvas02

Behavior of the code in Listing 10 The code in Listing 10:


Gets the name of the key that was pressed (which may be peculiar to the Sun cell phone emulator). Adds that name to the data already in the Vector object. Calls the repaint method on the Canvas object.

The call to the repaint method causes the overridden paint method to be executed. As described earlier, the call to the overridden paint method:

Clears the canvas by painting it white. Extracts the text data from the Vector (including the new data). Draws the text on the screen in two columns as shown in Figure 3.

Therefore, each time the user presses a key, the screen is re-drawn, causing the contents of the Vector object to be displayed. In this case, the call to the repaint method is not superfluous as it probably was earlier. When is the paint method called? As mentioned above, the paint method is called whenever the program code calls the repaint method. However, that is not the only time the paint method is called. It is called at any time that the system needs to cause the Canvas to be repainted by the MIDlet for any reason. For example, if a system screen covers the Canvas temporarily, the paint method will be called to redraw the screen when the system screen goes away.

Run the programs


I encourage you to copy the MIDlet code from Listing 11 and Listing 12. Run the MIDlets in the updated MIDlet development framework named WTKFramework03 that I provided in the lesson titled Using Alerts, Images, Timers, and Gauges in MIDlets (see Resources). Experiment with the MIDlet code, making changes and running your modified MIDlets in the framework program. See if you can explain the results produced by your changes. Don't forget that you will need to download and install the latest version of the Sun Java Wireless Toolkit for CLDC (see Resources). As of the date this lesson is being written, the latest version of the toolkit is WTK2.5.2.

Summary
In this lesson, you learned how to use several methods of the Canvas class (including paint and repaint), along with several methods of the Graphics class to draw text and lines on the cell phone screen. You learned how to use anchor point coordinates to

control the position of text that is drawn on the screen. You also learned how to handle keyPressed events fired by a Canvas object.

What's next?
In Part 2 of this lesson, you will learn how to mix image file data and drawn graphics on a cell phone screen. You will learn how to draw various shapes such as rectangles, circles, arcs, filled triangles, and rounded rectangles. You will learn how to use an offscreen image, and how to use the event handling capability of the Canvas class for simple animation. You will also learn how to make you MIDlet animations efficient by minimizing the screen area that must be repainted.

Resources

Download Sun Java Wireless Toolkit 2.5 for CLDC Release MIDlet: From Wikipedia, the free encyclopedia MIDlet: According to SCMAD Certification Center J2ME Tutorial, Part 1: Creating MIDlets by Vikram Goyal Pre-verifying MIDlet code according to Vikram Goyal Deploying a MIDlet according to Vikram Goyal The Java ME Device Table Retrieving MIDlet Attributes by Richard Marejka Learning Path: MIDlet Life Cycle When Runtime.exec() won't By Michael C. Daconta Connected Limited Device Configuration (CLDC) from Wikipedia Mobile Information Device Profile (MIDP) from Wikipedia Online API documentation forCLDC 1.0 Online API documentation forCLDC 1.1 Online API documentation for MIDP 1.0 Online API documentation for MIDP 2.0 Free Online Image Converter 62 Event Handling in JDK 1.0.2, The Event Class 80 Event Handling in JDK 1.1, A First Look, Delegation Event Model 102 Handling Events in Extended Components without Listener Objects 1640 The Essence of OOP using Java, Anonymous Classes 2570 Getting Started with MIDlets and the Sun Java Wireless Toolkit for CLDC 2572 Capturing Output Produced by Programs Running in a Child Process 2574 Back to Basics with MIDlets and the Sun Java Wireless Toolkit for CLDC 2576 Introduction to the MIDlet User Interface, A First Look 2578 Handling Life-Cycle Issues with the MIDlet User Interface 2580 Using Alerts, Images, Timers, and Gauges in MIDlets 2582 Using Lists in MIDlets 2584 Using Forms and Items in MIDlets 2586 Programming MIDlets for Interactive Behavior

Complete program listings


Complete listings of the programs discussed in this lesson are shown in Listing 11 and Listing 12 below. Listing 11. Source code for the MIDlet named Canvas01.
/*Canvas01.java Copyright 2007, R.G.Baldwin The purpose of this program is to illustrate a simple MIDlet using a Canvas object and a Graphics object. It further illustrates the use of anchor point constants to establish the alignment of drawn text. The MIDlet begins by painting the entire screen white. Otherwise, the new drawing would appear on top of what was previously on the screen. Then it sets the drawing color to black. Then it draws eight pairs of graphic objects. consists of: Each pair

A line from 0,0 to a specific coordinate value on the canvas. A string of text with the anchor point set to match the end of the line. The text contents of each string give the combination of anchor point constants used to position that particular string. Some text strings also begin with a lower case y- to show the effect of descenders on the alignment of the text string. The eight pairs of graphic objects illustrate the effect of different combinations of the following anchor point constants as well as the impact of lower-case descenders on the alignment produced by the constants: TOP BASELINE BOTTOM LEFT HCENTER RIGHT An anchor point must be specified for each text string that is drawn. The anchor point must be produced by the bitwise inclusive OR of one of the top three vertical anchor constants and one of the bottom three horizontal anchor constants. For example, the following would be a valid anchor point: TOP|RIGHT

The actual position and the alignment of the text string are determined by the combination of a pair of specified coordinate values and the bitwise inclusive OR of two anchor constants. The coordinates specify the position that will be assumed by a particular point in the text string. The bitwise inclusive OR of two anchor constants determines which point in the text string will be located at the specified coordinates. The special anchor point value of zero is interpreted to mean TOP|LEFT. A pair of straight lines is used to connect the ends of two groups of sloping lines. This makes it easier to visually determine the location of the anchor point in each text string. This MIDlet illustrates the use of the following methods from the Graphics class: setColor fillRect drawLine drawString It also illustrates the use of the following method from the Canvas class: paint repaint getWidth getHeight The MIDlet displays the canvas for 20 seconds and then enters the destroyed state. Tested using a Java SE 6 compiler, targeted at a V1.4 virtual machine, and WTK 2.5.2 running under Windows XP. *********************************************************/ package Canvas01; import import import import javax.microedition.lcdui.Display; javax.microedition.midlet.MIDlet; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Graphics;

public class Canvas01 extends MIDlet{ Canvas myCanvas; public Canvas01(){ System.out.println("Construct MIDlet"); myCanvas = this.new MyCanvas(); //Guarentee that the screen gets painted. myCanvas.repaint(); }//end constructor

public void startApp(){ //Make the Canvas the current display. Display.getDisplay(this).setCurrent(myCanvas); //Sleep for 20 seconds. try{Thread.currentThread().sleep(20000); } catch(Exception e){} //Enter the destroyed state. this.destroyApp(true); }//end startApp public void pauseApp(){ }//end pauseApp public void destroyApp(boolean unconditional){ System.out.println("Destroy MIDlet"); notifyDestroyed(); }//end destroyApp //----------------------------------------------------// //member class class MyCanvas extends Canvas{ public void paint(Graphics g){ //Paint the entire screen white. Otherwise, the new // drawing will simply appear over top of what was // previously on the screen. g.setColor(0xffffff); g.fillRect(0,0,getWidth(),getHeight()); //Set the drawing color to black. g.setColor(0x000000); //Draw text at eight different anchor points. The // text shows the vertical and horizontal constants // required to set the respective anchor points. // In addition, a line is drawn from 0,0 to the // anchor point to visually identify the anchor // point. A lower-case y is used to show the effect // of descenders on the alignment of the text. g.drawLine(0,0,200,20); g.drawString("y-TOP|RIGHT",200,20, Graphics.TOP|Graphics.RIGHT); g.drawLine(0,0,150,40); g.drawString("y-TOP|HCENTER",150,40, Graphics.TOP|Graphics.HCENTER); g.drawLine(0,0,100,60); g.drawString("y-TOP|LEFT",100,60, Graphics.TOP|Graphics.LEFT); g.drawLine(0,0,100,90); g.drawString("y-BOTTOM|LEFT",100,90,

Graphics.BOTTOM|Graphics.LEFT); g.drawLine(0,0,100,110); g.drawString("BOTTOM|LEFT",100,110, Graphics.BOTTOM|Graphics.LEFT); g.drawLine(0,0,100,130); g.drawString("y-BASELINE|LEFT",100,130, Graphics.BASELINE|Graphics.LEFT); g.drawLine(0,0,100,150); g.drawString("BASELINE|LEFT",100,150, Graphics.BASELINE|Graphics.LEFT); g.drawLine(0,0,100,170); g.drawString("Special Case of Zero",100,170,0); //Draw straight lines that connect the ends of the // sloping lines. g.drawLine(200,20,100,60); g.drawLine(100,60,100,170); }//end overridden paint method }//end member class MyCanvas }//end class Canvas01

Listing 12. Source code for the MIDlet named Canvas02.


/*Canvas02.java Copyright 2007, R.G.Baldwin The purpose of this program is to illustrate the detection and handling of key events on a Canvas object. The MIDlet also demonstrates certain aspects of the FullScreenMode versus the normal mode. A Canvas object is instantiated and set to FullScreenMode. The inherited keyPressed method is overridden to capture a user keystroke, convert the key code to a key name, add the key name to a Vector object, and cause the screen to be repainted each time a keyPressed event occurs. The paint method is overridden to clear the canvas, extract all of the key names from the Vector, and display the key names in two columns by using the drawString method to draw the key names on the Canvas object. Note, on the Sun cell phone emulator, you can terminate the MIDlet by clicking the button on the right that has a picture of a horizontal telephone receiver. When you

point to this button, the picture of the telephone receiver should turn red. I did not register an EXIT command listener because I did not want to cause the left soft key to be occupied with a command when in normal mode. Tested using a Java SE 6 compiler, targeted at a V1.4 virtual machine, and WTK 2.5.2 running under Windows XP. *********************************************************/ package Canvas02; import import import import import import import javax.microedition.lcdui.Display; javax.microedition.midlet.MIDlet; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Command; javax.microedition.lcdui.Displayable; java.util.Vector;

public class Canvas02 extends MIDlet{ Canvas myCanvas; public Canvas02(){ System.out.println("Construct MIDlet"); myCanvas = this.new MyCanvas(); //Guarentee that the screen gets painted. myCanvas.repaint(); //Note: The following commands cannot be activated // because there is no CommandListener registered on // the Canvas object. They are here solely to // demonstrate the impact of FullScreenMode versus // normal mode. When the Canvas object is set to // FullScreenMode, as is the case in this MIDlet, // these commands are not displayed. Therefore, even // if a CommandListener were registered on the Canvas, // it would not be possible for the user to activate // the commands. In normal mode, the commands are // displayed. If there was a CommandListener // registered on the Canvas, the user could activate // the commands. You can demonstrate this behavior by // modifying the code to pass true or false to the // setFullScreenMode method in the MyCanvas // constructor. myCanvas.addCommand(new Command("EXIT", Command.EXIT,2)); myCanvas.addCommand(new Command("BACK", Command.BACK,2)); myCanvas.addCommand(new Command("OK",Command.OK,2)); }//end constructor public void startApp(){ //Make the Canvas the current display. Display.getDisplay(this).setCurrent(myCanvas); }//end startApp

public void pauseApp(){ }//end pauseApp public void destroyApp(boolean unconditional){ System.out.println("Destroy MIDlet"); notifyDestroyed(); }//end destroyApp //----------------------------------------------------// //Member class class MyCanvas extends Canvas{ Vector vect = new Vector(); MyCanvas(){//constructor //Note: In normal mode, the gray bar where the soft // key commands normally appear is visible at the // bottom of the screen. However, in // FullScreenMode, that gray bar disappears. This // seems it make it impossible for the user to // activate commands on the Canvas when in // FullScreenMode. You can demonstrate this behavior // by either removing the following statement or // modifying the code to pass false to the // setFullScreenMode method. Passing false to the // method will switch the Canvas to normal mode. this.setFullScreenMode(true); }//end constructor public void paint(Graphics g){ //Paint the entire screen white. g.setColor(0xffffff); g.fillRect(0,0,getWidth(),getHeight()); g.setColor(0x000000);//Set drawing color to black //Declare and initialize some working variables. String keyData = null; int fontHeight = g.getFont().getHeight(); //Number of lines of text that fit on screen. int cntLimit = this.getHeight()/fontHeight; int yOffset = 0; int xOffset = 0; //Display the data captured from the user keyboard // input in two columns using graphic display // capabilities. If the length of the second // column exceeds the height of the screen, the // data will simply fall off the bottom of the // screen. for(int cnt = 0;cnt < vect.size();cnt++){ keyData = (String)vect.elementAt(cnt);//note cast if(cnt * fontHeight < this.getHeight() fontHeight){ //Display in first column.

yOffset = xOffset = }else{ //Display yOffset = xOffset = }//end else

cnt * fontHeight; 5; in second column (cnt - cntLimit) * fontHeight; this.getWidth()/2;

//Draw the string on the Canvas. g.drawString(keyData,xOffset,yOffset,0); }//end for loop }//end overridden paint method //--------------------------------------------------// //Override the keyPressed method to: // capture keystrokes // save them in a Vector object // repaint the canvas. public void keyPressed(int keyCode){ vect.addElement(getKeyName(keyCode)); this.repaint(); }//end keyPressed }//end member class MyCanvas }//end class Canvas02

Copyright
Copyright 2008, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.

About the author


Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web. Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which have gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.

In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor's degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics. Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.

(1) Use m3g package


import import import import import import import import import import import javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.game.GameCanvas; javax.microedition.m3g.Camera; javax.microedition.m3g.Graphics3D; javax.microedition.m3g.Loader; javax.microedition.m3g.World; javax.microedition.midlet.MIDlet;

public class RMMIDlet extends MIDlet implements CommandListener { private Display myDisplay = null; private RetainedCanvas myCanvas = null; private Command exitCommand = new Command("Exit", Command.ITEM, 1); public RMMIDlet() { super(); myDisplay = Display.getDisplay(this); myCanvas = new RetainedCanvas(); myCanvas.setCommandListener(this); myCanvas.addCommand(exitCommand);

} public void startApp() { myCanvas.init(); myDisplay.setCurrent(myCanvas); myCanvas.start(); } public void pauseApp() { } public void destroyApp(boolean unconditional) { myCanvas.stop(); } public void commandAction(Command cmd, Displayable disp) { if (cmd == exitCommand) { try { destroyApp(false); notifyDestroyed(); } catch (Exception e) { e.printStackTrace(); } } } } class RetainedCanvas extends GameCanvas implements Runnable { private boolean mRunning = false; private Thread mPaintThrd = null; private Graphics3D mGraphics3D = Graphics3D.getInstance(); private World mWorld = null; private Camera mCam = null; private long mWorldStartTime = 0; public RetainedCanvas() { super(true); } public void init() { try { mWorld = (World) Loader.load("/p.m3g")[0]; mCam = mWorld.getActiveCamera(); mCam.translate(0, 0, -1.5f); mCam.setOrientation(180, 0, 1, 0); } catch (Exception e) { e.printStackTrace(); } mWorldStartTime = System.currentTimeMillis(); }

public void start() { mRunning = true; mPaintThrd = new Thread(this); mPaintThrd.start(); } public void stop() { mRunning = false; try { mPaintThrd.join(); } catch (InterruptedException ex) { } } public void run() { Graphics g = getGraphics(); long startTime; while (mRunning) { cameraForward(); startTime = System.currentTimeMillis() - mWorldStartTime; mWorld.animate((int) startTime); mGraphics3D.bindTarget(g); mGraphics3D.render(mWorld); mGraphics3D.releaseTarget(); flushGraphics(); try { Thread.sleep(100); } catch (InterruptedException ie) { } } } private void cameraForward() { mCam.translate(0f, 0f, 0.2f); } }

(2) MutableImage

import import import import import import import import import

javax.microedition.lcdui.Alert; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class J2MEMutableImageExample extends MIDlet { private Display display; private MyCanvas canvas; public J2MEMutableImageExample() { display = Display.getDisplay(this); canvas = new MyCanvas(this); } protected void startApp() { display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void exitMIDlet() { destroyApp(true); notifyDestroyed(); } class MyCanvas extends Canvas implements CommandListener { private Command exit; private J2MEMutableImageExample mutableImageExample; private Image image = null; public MyCanvas(J2MEMutableImageExample mutableImageExample) { this.mutableImageExample = mutableImageExample; exit = new Command("Exit", Command.EXIT, 1); addCommand(exit); setCommandListener(this); try { image = Image.createImage(70, 70); Graphics graphics = image.getGraphics(); graphics.setColor(255, 0, 0); graphics.fillArc(10, 10, 60, 50, 180, 180); } catch (Exception error) { Alert alert = new Alert("Failure", "Creating Image", null, null); alert.setTimeout(Alert.FOREVER); display.setCurrent(alert); } } protected void paint(Graphics graphics) { if (image != null) { graphics.setColor(255, 255, 255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.drawImage(image, 30, 30, Graphics.VCENTER | Graphics.HCENTER); } }

public void commandAction(Command command, Displayable display) { if (command == exit) { mutableImageExample.exitMIDlet(); } } } }

(3) Draw image with rectangle


import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class DrawImageMIDlet extends MIDlet implements CommandListener { private Command exitCommand; Display display; Image image = null; public void startApp() { Display display = Display.getDisplay(this); try { image = Image.createImage("/0.png"); } catch (Exception e) { }

Displayable d = new DrawImageCanvas(image); exitCommand = new Command("Exit", Command.EXIT, 1); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class DrawImageCanvas extends Canvas { int width = 0; int height = 0; Image image = null; DrawImageCanvas(Image image) { this.image = image; } public void paint(Graphics g) { width = getWidth(); height = getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawRect(0, 0, width - 1, height - 1); g.drawImage(image, width / 2, height / 2, g.HCENTER | g.VCENTER); } }

(4) Draw image to the center vertically and horizontally


import java.io.IOException; import javax.microedition.lcdui.Canvas;

import import import import

javax.microedition.lcdui.Display; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class ImageVerticalHorizontalGraphicsMIDlet extends MIDlet { private Display display; protected void startApp() { Canvas canvas = new LineCanvas(); display = Display.getDisplay(this); display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } } class LineCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); try { Image image = Image.createImage("/h.png"); g.drawImage(image, width/2, height/2, Graphics.VCENTER | Graphics.HCENTER); } catch (IOException ex) { g.setColor(0xffffff); g.drawString("Failed to load image!", 0, 0, Graphics.TOP | Graphics.LEFT); return; } } }

(5) Use Image as buffer


import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Display; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class ImageBufferGraphicsMIDlet extends MIDlet {

private Display display; protected void startApp() { Canvas canvas = new LineCanvas(); display = Display.getDisplay(this); display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } }

class LineCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); Image image = Image.createImage(width, height); Graphics imageGraphics = image.getGraphics(); imageGraphics.fillRect(0, 0, width, height); int count = 10; int yIncrement = height/count; int xIncrement = width/count; for (int i = 0, x = xIncrement, y = 0; i < count; i++) { imageGraphics.setColor(0xC0 + ((128 + 10 * i) << 8) + ((128 + 10 * i) << 16) imageGraphics.drawLine(0, y, x, height); y += yIncrement; x += xIncrement; } imageGraphics.setFont(Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_UNDERLINED, Font.SIZE_SMALL)); imageGraphics.setColor(0xffff00); imageGraphics.drawString("Image Graphics", width/2, 0, Graphics.TOP | Graphics.H g.drawImage(image, 0, 0, Graphics.TOP | Graphics.LEFT); } }

(6) OffscreenCanvas
import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image;

import javax.microedition.midlet.MIDlet; class OffscreenCanvas extends Canvas { private Image mImage; public void paint(Graphics g) { if (mImage == null) initialize(); g.drawImage(mImage, 0, 0, Graphics.TOP | Graphics.LEFT); } private void initialize() { int w = getWidth(); int h = getHeight(); mImage = Image.createImage(w, h); Graphics g = mImage.getGraphics(); g.drawRect(0, 0, w - 1, h - 1); g.drawLine(w - 1, 0, 0, h - 1); } } public class OffscreenMIDlet extends MIDlet { public void startApp() { Displayable d = new OffscreenCanvas(); d.addCommand(new Command("Exit", Command.EXIT, 0)); d.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { notifyDestroyed(); } }); Display.getDisplay(this).setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } }

(7) Fill a rectangle, and a round rectangle


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class J2MEFilledRectangleExample extends MIDlet { private Display display; private MyCanvas canvas; public J2MEFilledRectangleExample() { display = Display.getDisplay(this); canvas = new MyCanvas(this); } protected void startApp() { display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void exitMIDlet() { destroyApp(true); notifyDestroyed(); } } class MyCanvas extends Canvas implements CommandListener { private Command exit; private J2MEFilledRectangleExample filledRectangleExample; public MyCanvas(J2MEFilledRectangleExample filledRectangleExample) { this.filledRectangleExample = filledRectangleExample; exit = new Command("Exit", Command.EXIT, 1); addCommand(exit); setCommandListener(this); } protected void paint(Graphics graphics) { graphics.setColor(255, 255, 255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.setColor(0, 0, 255); graphics.fillRect(2, 2, 20, 20); graphics.fillRoundRect(20, 20, 60, 60, 15, 45); } public void commandAction(Command command, Displayable displayable) { if (command == exit) { filledRectangleExample.exitMIDlet(); } } }

(8) Clipping region


import import import import import import import import import javax.microedition.lcdui.Alert; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class J2MEClippingRegion extends MIDlet { private Display display; private MyCanvas canvas; public J2MEClippingRegion() { display = Display.getDisplay(this); canvas = new MyCanvas(this); } protected void startApp() { display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void exitMIDlet() { destroyApp(true); notifyDestroyed(); } class MyCanvas extends Canvas implements CommandListener { private Command exit; private J2MEClippingRegion clippingRegion; private Image image = null; public MyCanvas(J2MEClippingRegion clippingRegion) { this.clippingRegion = clippingRegion; exit = new Command("Exit", Command.EXIT, 1); addCommand(exit); setCommandListener(this); try { image = Image.createImage(70, 70); Graphics graphics = image.getGraphics(); graphics.setColor(255, 0, 0); graphics.fillArc(10, 10, 60, 50, 180, 180); } catch (Exception error) { Alert alert = new Alert("Failure", "Creating Image", null, null

); alert.setTimeout(Alert.FOREVER); display.setCurrent(alert); } } protected void paint(Graphics graphics) { if (image != null) { graphics.setColor(255, 255, 255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.setClip(35, 35, 40, 40); graphics.drawImage(image, 30, 30, Graphics.VCENTER | Graphics.H CENTER); } } public void commandAction(Command command, Displayable display) { if (command == exit) { clippingRegion.exitMIDlet(); } } } }

(9) Coordinates translation


import import import import import import import import import javax.microedition.lcdui.Alert; javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class J2METranslateCoordinates extends MIDlet { private Display display; private MyCanvas canvas; public J2METranslateCoordinates() { display = Display.getDisplay(this); canvas = new MyCanvas(this); } protected void startApp() { display.setCurrent(canvas); }

protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void exitMIDlet() { destroyApp(true); notifyDestroyed(); } class MyCanvas extends Canvas implements CommandListener { private Command exit; private J2METranslateCoordinates translateCoordinates; private Image image = null; public MyCanvas(J2METranslateCoordinates translateCoordinates) { this.translateCoordinates = translateCoordinates; exit = new Command("Exit", Command.EXIT, 1); addCommand(exit); setCommandListener(this); try { image = Image.createImage(70, 70); Graphics graphics = image.getGraphics(); graphics.setColor(255, 0, 0); graphics.fillArc(10, 10, 60, 50, 180, 180); } catch (Exception error) { Alert alert = new Alert("Failure", "Creating Image", null, null ); alert.setTimeout(Alert.FOREVER); display.setCurrent(alert); } } protected void paint(Graphics graphics) { if (image != null) { graphics.setColor(255, 255, 255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.translate(45, 45); graphics.drawImage(image, 0, 0, Graphics.VCENTER | Graphics.HCE NTER); } } public void commandAction(Command command, Displayable display) { if (command == exit) { translateCoordinates.exitMIDlet(); } } } }

(10) Get width and height


import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class FontDimensionMIDlet extends MIDlet implements CommandListener { private Command exitCommand= new Command("Exit", Command.EXIT, 1); Display display; public void startApp() { Display display = Display.getDisplay(this); Displayable d = new FontDimensionCanvas(); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class FontDimensionCanvas extends Canvas { int width; int height; private Font aFont; public FontDimensionCanvas() { aFont = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_PLAIN, Font.SIZE_LARGE); } public void paint(Graphics g) { width = getWidth(); height = getHeight(); String s = "TEXT";

int stringWidth = aFont.stringWidth(s); int stringHeight = aFont.getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawRect(0, 0, width - 1, height - 1); g.setFont(aFont); g.drawString(s, 10, 10, Graphics.TOP | Graphics.LEFT); g.drawRect(10, 10, stringWidth, stringHeight); } }

(11) Underlined Style Font


import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Display; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class UnderlineFontMidlet extends MIDlet { public UnderlineFontMidlet() { // constructor } public void startApp() { Canvas canvas = new FontCanvas(); Display display = Display.getDisplay(this); display.setCurrent(canvas); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } class FontCanvas extends Canvas { public void paint(Graphics g) { g.setColor(0xffffff); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(0x000000); g.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_UNDERLINED, Fon t.SIZE_MEDIUM)); g.drawString("Underlined Style", 0, 60, g.LEFT | g.TOP); } }

(12) System Font

import import import import import

javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Display; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class SystemFontMidlet extends MIDlet { public SystemFontMidlet() { // constructor } public void startApp() { Canvas canvas = new FontCanvas(); Display display = Display.getDisplay(this); display.setCurrent(canvas); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } class FontCanvas extends Canvas { public void paint(Graphics g) { g.setColor(0xffffff); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(0x000000); g.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZ E_LARGE)); g.drawString("System Font", 0, 0, g.LEFT | g.TOP); } }

(13) Font style


import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class FontMIDlet extends MIDlet implements CommandListener { private FontCanvas mFontCanvas;

private Command mBoldCommand, mItalicCommand, mUnderlineCommand; public FontMIDlet() { mFontCanvas = new FontCanvas(); mBoldCommand = new Command("Bold", Command.SCREEN, 0); mItalicCommand = new Command("Italic", Command.SCREEN, 0); mUnderlineCommand = new Command("Underline", Command.SCREEN, 0); Command exitCommand = new Command("Exit", Command.EXIT, 0); mFontCanvas.addCommand(mBoldCommand); mFontCanvas.addCommand(mItalicCommand); mFontCanvas.addCommand(mUnderlineCommand); mFontCanvas.addCommand(exitCommand); mFontCanvas.setCommandListener(this); } public void startApp() { Display.getDisplay(this).setCurrent(mFontCanvas); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { if (c.getCommandType() == Command.EXIT) { notifyDestroyed(); return; } boolean isBold = mFontCanvas.isBold() ^ (c == mBoldCommand); boolean isItalic = mFontCanvas.isItalic() ^ (c == mItalicCommand); boolean isUnderline = mFontCanvas.isUnderline() ^ (c == mUnderlineC ommand); int style = (isBold ? Font.STYLE_BOLD : 0) | (isItalic ? Font.STYLE _ITALIC : 0) | (isUnderline ? Font.STYLE_UNDERLINED : 0); mFontCanvas.setStyle(style); mFontCanvas.repaint(); } } class FontCanvas extends Canvas { private Font mSystemFont, mMonospaceFont, mProportionalFont; public FontCanvas() { this(Font.STYLE_PLAIN); } public FontCanvas(int style) { setStyle(style); }

public void setStyle(int style) { mSystemFont = Font.getFont(Font.FACE_SYSTEM, style, Font.SIZE_MEDIU M); mMonospaceFont = Font.getFont(Font.FACE_MONOSPACE, style, Font.SIZE _MEDIUM); mProportionalFont = Font.getFont(Font.FACE_PROPORTIONAL, style, Fon t.SIZE_MEDIUM); } public boolean isBold() { return mSystemFont.isBold(); } public boolean isItalic() { return mSystemFont.isItalic(); } public boolean isUnderline() { return mSystemFont.isUnderlined(); } public void paint(Graphics g) { int w = getWidth(); int h = getHeight(); int x = w / 2; int y = 20; y += showFont(g, "System", x, y, mSystemFont); y += showFont(g, "Monospace", x, y, mMonospaceFont); y += showFont(g, "Proportional", x, y, mProportionalFont); } private int showFont(Graphics g, String s, int x, int y, Font f) { g.setFont(f); g.drawString(s, x, y, Graphics.TOP | Graphics.HCENTER); return f.getHeight(); } }

(14) Plot 2D
import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class Plot2DMIDlet extends MIDlet implements CommandListener { private Command exitCommand;

Display display; int[] line1 = { 80, 20, 60, 70, 50, 20 }; int[] line2 = { 20, 10, 50, 60, 90, 60 }; public void startApp() { int maxX = 10; int maxY = 100; Display display = Display.getDisplay(this); Displayable d = new Plot2DCanvas(line1, line2, maxX, maxY); exitCommand = new Command("Exit", Command.EXIT, 1); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class Plot2DCanvas extends Canvas { int line1[]; int line2[]; int maxX, maxY; Plot2DCanvas(int[] line1, int[] line2, int maxX, int maxY) { this.line1 = line1; this.line2 = line2; this.maxX = maxX; this.maxY = maxY; } public void paint(Graphics g) { int width = this.getWidth(); int height = this.getHeight(); g.setGrayScale(0); g.drawLine(0, height, 0, 0); g.drawLine(0, height - 1, width, height - 1); int deltaX = width / maxX; int deltaY = height / maxY;

g.setStrokeStyle(Graphics.DOTTED); for (int i = 0; i < line1.length - 1; i++) { g.drawLine(deltaX * i, height deltaY * line1[i], deltaX * (i + 1), height - deltaY * line1[i + 1]); } } }

(15) J2MEArcExample
import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class J2MEArcExample extends MIDlet { private Display display; private MyCanvas canvas; public J2MEArcExample() { display = Display.getDisplay(this); canvas = new MyCanvas(this); } protected void startApp() { display.setCurrent(canvas); } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void exitMIDlet() { destroyApp(true); notifyDestroyed(); } } class MyCanvas extends Canvas implements CommandListener { private Command exit; private J2MEArcExample arcExample;

public MyCanvas(J2MEArcExample arcExample) { this.arcExample = arcExample; exit = new Command("Exit", Command.EXIT, 1); addCommand(exit); setCommandListener(this); } protected void paint(Graphics graphics) { graphics.setColor(255, 255, 255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.setColor(255, 0, 0); graphics.drawArc(0, 0, getWidth(), getHeight(), 180, 180); } public void commandAction(Command command, Displayable displayable) { if (command == exit) { arcExample.exitMIDlet(); } } }

(16) Pie chart


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class PieChartMIDlet extends MIDlet implements CommandListener { private Command exitCommand; public void startApp() { Display display = Display.getDisplay(this); int[] data = { 10, 20, 30, 40, 20 }; Displayable d = new PieChartCanvas(data); exitCommand = new Command("exit", Command.EXIT, 1); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { }

public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class PieChartCanvas extends Canvas { int[] data; int colors[] = { 0xFF0000, 0xA9E969, 0x00FFFF, 0xC675EC, 0x008800, 0x 00C400 }; public PieChartCanvas(int[] data) { this.data = data; } public void paint(Graphics g) { int width = this.getWidth(); int height = this.getHeight(); g.setColor(255, 255, 255); g.fillRect(0, 0, width, height); int sum = 0; for (int i = 0; i < data.length; i++) { sum += data[i]; } int deltaAngle = 360 * 100 / sum / 100; int x = 4; int y = 4; int diameter; if (width > height) diameter = height - y * 2; else diameter = width - x * 2; int startAngle = 0; for (int i = 0; i < data.length; i++) { g.setColor(colors[i]); g.fillArc(x, y, diameter, diameter, startAngle, deltaAngle * data [i]); startAngle += deltaAngle * data[i]; } } }

(17) Translate coordinate

import import import import import import import

javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class OrigTransMIDlet extends MIDlet implements CommandListener { private Command exitCommand; Display display; public void startApp() { Display display = Display.getDisplay(this); Displayable d = new OrigTransCanvas(); exitCommand = new Command("exit", Command.EXIT, 1); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class OrigTransCanvas extends Canvas { int width = 0; int height = 0; public void paint(Graphics g) { width = getWidth(); height = getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawRect(0, 0, width - 1, height - 1); int counts = 5; int step = width / 5; for (int i = 1; i <= 5; i++) { g.fillRect(0, 0, step, step);

g.translate(step, step); } } }

(18) Canvas key event and navigate through arrow keys


import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class Navigate2MIDlet extends MIDlet implements CommandListener { private Command exitCommand; Displayable nd; Image image = null; public Navigate2MIDlet() { try { image = Image.createImage("/J.png"); } catch (Exception e) { } Display display = Display.getDisplay(this); nd = new Navigate2Canvas(image); exitCommand = new Command("exit", Command.EXIT, 1);

nd.addCommand(exitCommand); nd.setCommandListener(this); display.setCurrent(nd); } public void startApp() { } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class Navigate2Canvas extends Canvas { private Image image; private int newX = 0; private int newY = 0; private int stepX = 0; private int stepY = 0; public Navigate2Canvas(Image image) { this.image = image; newX = 0; newY = 0; stepX = getWidth() / 4; stepY = getHeight() / 4; } public void steppingXY(int x, int y) { newX += x; newY += y; } public void paint(Graphics g) { int width = this.getWidth(); int height = this.getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawRect(0, 0, width - 1, height - 1); g.translate(newX, newY); g.drawImage(image, 0, 0, g.TOP | g.LEFT); }

protected void keyPressed(int keyCode) { int gameaction = getGameAction(keyCode); switch (gameaction) { case UP: steppingXY(0, stepY); break; case DOWN: steppingXY(0, -stepY); break; case LEFT: steppingXY(stepX, 0); break; case RIGHT: steppingXY(-stepX, 0); break; } repaint(); } }

(19) Navigatable Canvas


import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image; javax.microedition.midlet.MIDlet;

public class NavigateMIDlet extends MIDlet implements CommandListener { int stepX; int stepY; private Command leftCommand; private Command rightCommand; private Display display; Displayable nd; Image image = null; public NavigateMIDlet() { try {

image = Image.createImage("/M.png"); } catch (Exception e) { } Display display = Display.getDisplay(this); nd = new NavigateCanvas(image); stepX = ((NavigateCanvas) nd).getWidth() / 4; stepY = ((NavigateCanvas) nd).getHeight() / 4; leftCommand = new Command("<Left", Command.SCREEN, 1); rightCommand = new Command("Right>", Command.SCREEN, 1); nd.addCommand(rightCommand); nd.addCommand(leftCommand); nd.setCommandListener(this); display.setCurrent(nd); } public void startApp() { } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable d) { if (d == nd && c == leftCommand) { ((NavigateCanvas) nd).steppingXY(stepX, 0); ((NavigateCanvas) nd).repaint(); } else if (d == nd && c == rightCommand) { ((NavigateCanvas) nd).steppingXY(-stepX, 0); ((NavigateCanvas) nd).repaint(); } } } class NavigateCanvas extends Canvas { private Image image; private int newX = 0; private int newY = 0; public NavigateCanvas(Image image) { this.image = image; newX = 0; newY = 0; } public void steppingXY(int x, int y) { newX += x; newY += y; } public void paint(Graphics g) {

int width = this.getWidth(); int height = this.getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawRect(0, 0, width - 1, height - 1); g.translate(newX, newY); g.drawImage(image, 0, 0, g.TOP | g.LEFT); } }

(20) Canvas size


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class CanvasSizeMIDlet extends MIDlet implements CommandListener { private Command exitCommand; Display display; public void startApp() { display = Display.getDisplay(this); exitCommand = new Command("Exit", Command.EXIT, 1); Displayable SizeCanvas = new SizeCanvas(); SizeCanvas.addCommand(exitCommand); SizeCanvas.setCommandListener(this); display.setCurrent(SizeCanvas); } public void pauseApp() {

} public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { if (c == exitCommand) { destroyApp(false); notifyDestroyed(); } } } class SizeCanvas extends Canvas { int width = 0; int height = 0; public void paint(Graphics g) { width = getWidth(); height = getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0); g.drawString("Stroke Style:" + g.getStrokeStyle(), 0, 60, Graphics. TOP | Graphics.LEFT); g.drawRect(0, 0, width - 1, height - 1); g.setStrokeStyle(Graphics.DOTTED); g.drawLine(00, 40, 60, 60); g.drawString(Integer.toBinaryString(width), 0, 0, Graphics.TOP | Gr aphics.LEFT); g.drawString(Integer.toBinaryString(height), 10, 20, Graphics.TOP | Graphics.LEFT); } }

(21) Use Canvas to draw a clock


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class ClockMIDlet extends MIDlet implements CommandListener { private Command exitCommand; Display display; public void startApp() { Display display = Display.getDisplay(this); Displayable d = new ClockCanvas(10, 10, 10); exitCommand = new Command("Exit", Command.EXIT, 1); d.addCommand(exitCommand); d.setCommandListener(this); display.setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } class ClockCanvas extends Canvas { private int hour; private int minute; private int second; protected int xCenter, yCenter; protected int clockRadius; int width = 0; int height = 0; ClockCanvas(int hour, int minute, int second) { this.hour = hour; this.minute = minute; this.second = second; } public void paint(Graphics g) { width = getWidth(); height = getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); g.setGrayScale(0);

g.drawRect(0, 0, width - 1, height - 1); clockRadius = Math.min(width, height) - 20; xCenter = getWidth() / 2; yCenter = getHeight() / 2; g.drawArc(10, 12, clockRadius, clockRadius, 0, 360); g.drawString("12", xCenter, 0, Graphics.TOP | Graphics.HCENTER); g.drawString("9", 1, yCenter, Graphics.BASELINE | Graphics.LEFT); g.drawString("3", width 1, yCenter, Graphics.BASELINE | Graphics.RIGHT); g.drawString("6", xCenter, height, Graphics.BOTTOM | Graphics.RIGHT ); } }

(22) Get Canvas properties


import import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Form; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.StringItem; javax.microedition.midlet.MIDlet;

public class AttributesMIDlet extends MIDlet implements CommandListener { private Display display; protected boolean started; private Command exitCommand; protected void startApp() { if (!started) { display = Display.getDisplay(this); Canvas canvas = new DummyCanvas(); Form form = new Form("Attributes"); exitCommand = new Command("Exit", Command.EXIT, 0);

form.addCommand(exitCommand);

boolean isColor = display.isColor(); form.append(new StringItem(isColor ? "Colors: " : "Grays: ", String.valueOf(display.numColors()))); form.append(new StringItem("Width: ", String.valueOf(canvas.getWidth()))); form.append(new StringItem("Height: ", String.valueOf(canvas.getHeight()))) form.append(new StringItem("Pointer? ", String.valueOf(canvas.hasPointerEve form.append(new StringItem("Motion? ",String.valueOf(canvas.hasPointerMotio form.append(new StringItem("Repeat? ",String.valueOf(canvas.hasRepeatEvents form.append(new StringItem("Buffered? ", String.valueOf(canvas.isDoubleBuff form.setCommandListener(this); display.setCurrent(form); started = true; } } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable d) { if (c == exitCommand) { notifyDestroyed(); } } static class DummyCanvas extends Canvas { protected void paint(Graphics g) { } } }

(23) Paint Canvas in a thread


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class J2MESweep extends MIDlet { public void startApp() { final SweepCanvas sweeper = new SweepCanvas();

sweeper.start(); sweeper.addCommand(new Command("Exit", Command.EXIT, 0)); sweeper.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { sweeper.stop(); notifyDestroyed(); } }); Display.getDisplay(this).setCurrent(sweeper); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } class SweepCanvas extends Canvas implements Runnable { private boolean mTrucking; private int mTheta = 0; private int mBorder = 10; private int mDelay = 50; public void start() { mTrucking = true; Thread t = new Thread(this); t.start(); } public void stop() { mTrucking = false; } public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); int x = mBorder; int y = mBorder; int w = width - mBorder * 2; int h = height - mBorder * 2; for (int i = 0; i < 8; i++) { g.setGrayScale((8 - i) * 32 - 16); g.fillArc(x, y, w, h, mTheta + i * 10, 10); } } public void run() {

while (mTrucking) { mTheta = (mTheta + 1) % 360; repaint(); try { Thread.sleep(mDelay); } catch (InterruptedException ie) { } } } }

(24) Canvas Key Events


import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.midlet.MIDlet;

public class EventsMIDlet extends MIDlet implements CommandListener { private Display display; protected boolean started; private Command exitCommand = new Command("Exit", Command.EXIT, 0); protected void startApp() { if (!started) { display = Display.getDisplay(this); Canvas canvas = new EventsCanvas(); canvas.addCommand(exitCommand); canvas.setCommandListener(this); display.setCurrent(canvas); started = true; }

} protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable d) { if (c == exitCommand) { notifyDestroyed(); } } } class EventsCanvas extends Canvas { static int[] keyCodes = {KEY_NUM0, KEY_NUM1, KEY_NUM2, KEY_NUM3, KE Y_NUM4, KEY_NUM5, KEY_NUM6, KEY_NUM7, KEY_NUM8, KE Y_NUM9, KEY_POUND, KEY_STAR}; static String[] keyNames = {"KEY_NUM0", "KEY_NUM1", "KEY_NUM2", "KE Y_NUM3", "KEY_NUM4", "KEY_NUM5", "KEY_NUM6", "KEY_NUM7", "KEY_N UM8", "KEY_NUM9", "KEY_POUND", "KEY_STAR"}; static int[] gameActions = { UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C, GAME_D}; static String[] gameNames = { "UP", "DOWN", "LEFT", "RIGHT", "FIRE", "GAME_A", "GAME_B", "GAME_C", "GAME_D" };

int lastKeyCode = 0; int lastX; int lastY; boolean pointer; protected void keyPressed(int keyCode) { lastKeyCode = keyCode; repaint(); } protected void keyRepeated(int keyCode) { lastKeyCode = keyCode; repaint(); } protected void keyReleased(int keyCode) { lastKeyCode = 0; repaint();

} protected void pointerPressed(int x, int y) { lastX = x; lastY = y; pointer = true; repaint(); } protected void pointerDragged(int x, int y) { lastX = x; lastY = y; pointer = true; repaint(); } protected void pointerReleased(int x, int y) { pointer = false; repaint(); } protected void paint(Graphics g) { g.setColor(0xffffff); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(0); if (lastKeyCode != 0) { String keyText = "keyCode " + lastKeyCode; String keyName = null; for (int i = 0; i < keyCodes.length; i++) { if (lastKeyCode == keyCodes[i]) { keyName = keyNames[i]; break; } } if (keyName == null) { for (int i = 0; i < gameActions.length; i++) { if (lastKeyCode == getKeyCode(gameActions[i])) { keyName = gameNames[i]; break; } } } g.drawString(keyText, getWidth()/2, getHeight()/2, Graphics.BASELINE|Graphics.HCENTER); if (keyName != null) { g.drawString(keyName, getWidth()/2, getHeight()/2 + g.g etFont().getHeight(), Graphics.BASELINE|Graphics.HCENTER); } } else if (pointer) { g.drawString("(" + lastX + ", " + lastY + ")", getWidth()/2 , getHeight()/2, Graphics.BASELINE|Graphics.HCENTER); }

} }

(24) Piano

import javax.microedition.media.*; import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class PianoMIDlet extends MIDlet { public void startApp() { Displayable d = new PianoCanvas(); d.addCommand(new Command("Exit", Command.EXIT, 0)); d.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { notifyDestroyed(); } }); Display.getDisplay(this).setCurrent(d); } public void pauseApp() {}

public void destroyApp(boolean unconditional) {} } class PianoCanvas extends Canvas { private static final int[] kNoteX = { 0, 11, 16, 29, 32, 48, 59, 64, 76, 80, 93, 96 }; private static final int[] kNoteWidth = { 16, 8, 16, 8, 16, 16, 8, 16, 8, 16, };

8, 16

private static final int[] kNoteHeight = { 96, 64, 96, 64, 96, 96, 64, 96, 64, 96, 64, 96 }; private static final boolean[] kBlack = { false, true, false, true, false, false, true, false, true, false, true, false }; private int mMiddleCX, mMiddleCY; private int mCurrentNote; public PianoCanvas() { int w = getWidth(); int h = getHeight(); int fullWidth = kNoteWidth[0] * 8; mMiddleCX = (w - fullWidth) / 2; mMiddleCY = (h - kNoteHeight[0]) / 2; mCurrentNote = 60; } public void paint(Graphics g) { int w = getWidth(); int h = getHeight(); g.setColor(0xffffff); g.fillRect(0, 0, w, h); g.setColor(0x000000); for (int i = 60; i <= 72; i++) drawNote(g, i); drawSelection(g, mCurrentNote); } private void drawNote(Graphics g, int note) { int n = note % 12; int octaveOffset = ((note - n) / 12 - 5) * 7 * kNoteWidth[0]; int x = mMiddleCX + octaveOffset + kNoteX[n]; int y = mMiddleCY; int w = kNoteWidth[n]; int h = kNoteHeight[n];

if (isBlack(n)) g.fillRect(x, y, w, h); else g.drawRect(x, y, w, h); } private void drawSelection(Graphics g, int note) { int n = note % 12; int octaveOffset = ((note - n) / 12 - 5) * 7 * kNoteWidth[0]; int x = mMiddleCX + octaveOffset + kNoteX[n]; int y = mMiddleCY; int w = kNoteWidth[n]; int h = kNoteHeight[n]; int sw = 6; int sx = x + (w - sw) / 2; int sy = y + h - 8; g.setColor(0xffffff); g.fillRect(sx, sy, sw, sw); g.setColor(0x000000); g.drawRect(sx, sy, sw, sw); g.drawLine(sx, sy, sx + sw, sy + sw); g.drawLine(sx, sy + sw, sx + sw, sy); } private boolean isBlack(int note) { return kBlack[note]; } public void keyPressed(int keyCode) { int action = getGameAction(keyCode); switch (action) { case LEFT: mCurrentNote--; if (mCurrentNote < 60) mCurrentNote = 60; repaint(); break; case RIGHT: mCurrentNote++; if (mCurrentNote > 72) mCurrentNote = 72; repaint(); break; case FIRE: try { Manager.playTone(mCurrentNote, 1000, 100); } catch (MediaException me) {} break; default: break; } } }

(25) Pacer
import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class Pacer extends MIDlet{ public void startApp() { Displayable d = new PacerCanvas(); d.addCommand(new Command("Exit", Command.EXIT, 0)); d.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { notifyDestroyed(); } } ); Display.getDisplay(this).setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } class PacerCanvas extends Canvas { public void paint(Graphics g) { int w = getWidth(); int h = getHeight(); g.setColor(0xffffff); g.fillRect(0, 0, w, h); g.setColor(0x000000); for (int x = 0; x < w; x += 10) g.drawLine(0, w - x, x, 0); int z = 50; g.drawRect(z, z, 20, 20); z += 20; g.fillRoundRect(z, z, 20, 20, 5, 5); z += 20; g.drawArc(z, z, 20, 20, 0, 360); } }

(26) GraphicsMIDlet
/* J2ME in a Nutshell By Kim Topley ISBN: 0-596-00253-X */ import import import import import import import import import

javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Font; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.List; javax.microedition.midlet.MIDlet;

public class GraphicsMIDlet extends MIDlet implements CommandListener { // The MIDlet's Display object private Display display; // Flag indicating first call of startApp protected boolean started; // Exit command private Command exitCommand; // Back to examples list command private Command backCommand; // The example selection list private List examplesList; // The Canvases used to demonstrate different Items private Canvas[] canvases; // The example names. Used to populate the list. private String[] examples = { "Lines", "Rectangles", "RectangleFills", "Arcs", "FilledArcs", "Text" };

protected void startApp() { if (!started) { started = true; display = Display.getDisplay(this); // Create the common commands createCommands(); // Create the canvases createCanvases(); // Create the list of examples createList(); // Start with the List display.setCurrent(examplesList); } } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable d) { if (d == examplesList) { // New example selected int index = examplesList.getSelectedIndex(); display.setCurrent(canvases[index]); } else if (c == exitCommand) { // Exit. No need to call destroyApp // because it is empty. notifyDestroyed(); } else if (c == backCommand) { // Go back to main selection list display.setCurrent(examplesList); } } private void createCommands() { exitCommand = new Command("Exit", Command.EXIT, 0); backCommand = new Command("Back", Command.BACK, 1); } private void createList() { examplesList = new List("Select Example", List.IMPLICIT); for (int i = 0; i < examples.length; i++) { examplesList.append(examples[i], null); } examplesList.setCommandListener(this); } private void createCanvases() { canvases = new Canvas[examples.length]; canvases[0] = createLinesCanvas();

canvases[1] canvases[2] canvases[3] canvases[4] canvases[5] }

= = = = =

createRectanglesCanvas(); createRectangleFillsCanvas(); createArcsCanvas(); createFilledArcsCanvas(); createTextCanvas();

private void addCommands(Displayable d) { d.addCommand(exitCommand); d.addCommand(backCommand); d.setCommandListener(this); } // Create the Canvas for the line drawing example private Canvas createLinesCanvas() { Canvas canvas = new LineCanvas(); addCommands(canvas); return canvas; } // Create the Canvas for the rectangles example private Canvas createRectanglesCanvas() { Canvas canvas = new RectanglesCanvas(); addCommands(canvas); return canvas; } // Create the Canvas for the filled rectangles example private Canvas createRectangleFillsCanvas() { Canvas canvas = new RectangleFillsCanvas(); addCommands(canvas); return canvas; } // Create the Canvas for the arcs example private Canvas createArcsCanvas() { Canvas canvas = new ArcsCanvas(); addCommands(canvas); return canvas; } // Create the Canvas for the filled arcs example private Canvas createFilledArcsCanvas() { Canvas canvas = new FilledArcsCanvas(); addCommands(canvas); return canvas; } // Create the Canvas for the text example private Canvas createTextCanvas() { Canvas canvas = new TextCanvas(); addCommands(canvas); return canvas; } }

// A canvas that illustrates line drawing class LineCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); // Fill the background using black g.setColor(0); g.fillRect(0, 0, width, height); // White horizontal line g.setColor(0xFFFFFF); g.drawLine(0, height/2, width - 1, height/2); // Yellow dotted horizontal line g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xFFFF00); g.drawLine(0, height/4, width - 1, height/4); // Solid diagonal line in brightest gray g.setGrayScale(255); g.setStrokeStyle(Graphics.SOLID); g.drawLine(0, 0, width - 1, height - 1); } } // A canvas that illustrates rectangles class RectanglesCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); // Create a white background g.setColor(0xffffff); g.fillRect(0, 0, width, height); // Draw a solid rectangle g.setColor(0); g.drawRect(width/4, 0, width/2, height/4); // Draw a dotted rectangle inside // the solid rectangle. g.setStrokeStyle(Graphics.DOTTED); g.drawRect(width/4 + 4, 4, width/2 - 8, height/4 - 8); // Draw a rounded rectangle g.setStrokeStyle(Graphics.SOLID); g.drawRoundRect(width/4, height/2, width/2, height/4, 16, 8); } } // A canvas that illustrates filled rectangles class RectangleFillsCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight();

// Create a black background g.setColor(0); g.fillRect(0, 0, width, height); // Draw a filled rectangle with // a dotted rectangle around it g.setStrokeStyle(Graphics.DOTTED); g.setColor(0x00ff00); g.fillRect(width/4, height/4, width/2, height/2); g.setColor(0xffff00); g.drawRect(width/4, height/4, width/2, height/2); } } // A canvas that illustrates arcs class ArcsCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); // Create a black background g.setColor(0); g.fillRect(0, 0, width, height); // A quarter circle, clockwise 90 degrees // from the 3 o'clock position. Show the // bounding rectangle as well. g.setColor(0xffffff); g.drawArc(0, 0, width/2, height/2, 0, 90); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(0, 0, width/2, height/2); // A quarter circle, anticlockwise 90 degrees // from the 3 o'clock position. g.setStrokeStyle(Graphics.SOLID); g.setColor(0xffffff); g.drawArc(width/2, 0, width/2, height/2, 0, -90); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(width/2, 0, width/2, height/2); // An elliptical arc from the six o'clock // position, counterclockwise 180 degrees g.setStrokeStyle(Graphics.SOLID); g.setColor(0xffffff); g.drawArc(0, height/2, width, height/2, -90, -180); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(0, height/2, width, height/2); } } // A canvas that illustrates filled arcs class FilledArcsCanvas extends Canvas { public void paint(Graphics g) {

int width = getWidth(); int height = getHeight(); // Create a black background g.setColor(0); g.fillRect(0, 0, width, height); // A quarter circle, clockwise 90 degrees // from the 3 o'clock position. Show the // bounding rectangle as well. g.setColor(0xffffff); g.fillArc(0, 0, width/2, height/2, 0, 90); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(0, 0, width/2, height/2); // A quarter circle, anticlockwise 90 degrees // from the 3 o'clock position. g.setStrokeStyle(Graphics.SOLID); g.setColor(0xffffff); g.fillArc(width/2, 0, width/2, height/2, 0, -90); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(width/2, 0, width/2, height/2); // An elliptical arc from the six o'clock // position, counterclockwise 180 degrees g.setStrokeStyle(Graphics.SOLID); g.setColor(0xffffff); g.fillArc(0, height/2, width, height/2, -90, -180); g.setStrokeStyle(Graphics.DOTTED); g.setColor(0xffff00); g.drawRect(0, height/2, width, height/2); } } // A canvas that illustrates text rendering class TextCanvas extends Canvas { public void paint(Graphics g) { int width = getWidth(); int height = getHeight(); // Create a black background g.setColor(0); g.fillRect(0, 0, width, height); // Top-left of canvas g.setColor(0xffffff); g.drawString("Top left", 0, 0, Graphics.TOP | Graphics.LEFT); // Draw another string one line below Font font = g.getFont(); g.drawString("Below top left", 0, font.getHeight(), Graphics.TO P | Graphics.LEFT); // Bottom-right of canvas g.drawString("Bottom right", width, height, Graphics.BOTTOM | G

raphics.RIGHT); // Mixed fonts and colors String str = "Multi-font "; font = Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_UNDERLIN ED, Font.SIZE_LARGE); g.setFont(font); g.drawString(str, 0, height/2, Graphics.LEFT | Graphics.BASELIN E); int x = font.stringWidth(str); g.setColor(0x00ff00); g.setFont(Font.getFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD | Font.STYLE_ITALIC, Font.SIZE_MEDIUM)); g.drawString("and multicolor", x, height/2, Graphics.LEFT | Graphics.BASELINE); } }

(27) OffscreenCanvas

import import import import import import import

javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.Image;

import javax.microedition.midlet.MIDlet; public class OffscreenMIDlet extends MIDlet { public void startApp() { Displayable d = new OffscreenCanvas(); d.addCommand(new Command("Exit", Command.EXIT, 0)); d.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { notifyDestroyed(); } }); Display.getDisplay(this).setCurrent(d); } public void pauseApp() { } public void destroyApp(boolean unconditional) { } } class OffscreenCanvas extends Canvas { private Image mImage; public void paint(Graphics g) { if (mImage == null) initialize(); g.drawImage(mImage, 0, 0, Graphics.TOP | Graphics.LEFT); } private void initialize() { int w = getWidth(); int h = getHeight(); mImage = Image.createImage(w, h); Graphics g = mImage.getGraphics(); g.drawRect(0, 0, w - 1, h - 1); g.drawLine(0, 0, w - 1, h - 1); g.drawLine(w - 1, 0, 0, h - 1); } }

(28) Quatsch MIDlet

/* Wireless Java 2nd edition Jonathan Knudsen Publisher: Apress ISBN: 1590590775 */ import java.io.IOException; import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; import javax.microedition.midlet.MIDlet; public class QuatschMIDlet extends MIDlet implements CommandListener { private Display mDisplay; private QuatschCanvas mQuatschCanvas; private Form mShowForm; private Command mExitCommand, mShowCommand, mOkCommand; public void startApp() { if (mQuatschCanvas == null) { try { mQuatschCanvas = new QuatschCanvas("/quatsch.png", "/atmosphere.png", "/background_tiles.png"); mQuatschCanvas.start(); mExitCommand = new Command("Exit", Command.EXIT, 0); mShowCommand = new Command("Show/Hide", Command.SCREEN, 0); mOkCommand = new Command("OK", Command.OK, 0); mQuatschCanvas.addCommand(mExitCommand); mQuatschCanvas.addCommand(mShowCommand); mQuatschCanvas.setCommandListener(this); } catch (IOException ioe) { System.out.println(ioe); } }

mDisplay = Display.getDisplay(this); mDisplay.setCurrent(mQuatschCanvas); } public void pauseApp() {} public void destroyApp(boolean unconditional) { mQuatschCanvas.stop(); } public void commandAction(Command c, Displayable s) { if (c.getCommandType() == Command.EXIT) { destroyApp(true); notifyDestroyed(); } else if (c == mShowCommand) { if (mShowForm == null) { mShowForm = new Form("Show/Hide"); ChoiceGroup cg = new ChoiceGroup("Layers", Choice.MULTIPLE); cg.append("Fog", null); cg.append("Dr. Quatsch", null); cg.append("Background", null); mShowForm.append(cg); mShowForm.addCommand(mOkCommand); mShowForm.setCommandListener(this); } ChoiceGroup cg = (ChoiceGroup)mShowForm.get(0); cg.setSelectedIndex(0, mQuatschCanvas.isVisible(0)); cg.setSelectedIndex(1, mQuatschCanvas.isVisible(1)); cg.setSelectedIndex(2, mQuatschCanvas.isVisible(2)); mDisplay.setCurrent(mShowForm); } else if (c == mOkCommand) { ChoiceGroup cg = (ChoiceGroup)mShowForm.get(0); mQuatschCanvas.setVisible(0, cg.isSelected(0)); mQuatschCanvas.setVisible(1, cg.isSelected(1)); mQuatschCanvas.setVisible(2, cg.isSelected(2)); mDisplay.setCurrent(mQuatschCanvas); } } } class QuatschCanvas extends GameCanvas implements Runnable { private boolean mTrucking; private LayerManager mLayerManager; private TiledLayer mAtmosphere; private TiledLayer mBackground; private int mAnimatedIndex; private Sprite mQuatsch; private int mState, mDirection; private static final int kStanding = 1; private static final int kRunning = 2;

private static final int kLeft = 1; private static final int kRight = 2; private static final int[] kRunningSequence = { 0, 1, 2 }; private static final int[] kStandingSequence = { 3 }; public QuatschCanvas(String quatschImageName, String atmosphereImageName, String backgroundImageName) throws IOException { super(true); // Create a LayerManager. mLayerManager = new LayerManager(); int w = getWidth(); int h = getHeight(); mLayerManager.setViewWindow(96, 0, w, h); createBackground(backgroundImageName); createAtmosphere(atmosphereImageName); createQuatsch(quatschImageName); } private void createBackground(String backgroundImageName) throws IOException { // Create the tiled layer. Image backgroundImage = Image.createImage(backgroundImageName); int[] map = { 1, 2, 0, 0, 0, 0, 0, 0, 3, 3, 2, 0, 0, 0, 5, 0, 3, 3, 3, 2, 4, 1, 3, 2, 6, 6, 6, 6, 6, 6, 6, 6 }; mBackground = new TiledLayer(8, 4, backgroundImage, 48, 48); mBackground.setPosition(12, 0); for (int i = 0; i < map.length; i++) { int column = i % 8; int row = (i - column) / 8; mBackground.setCell(column, row, map[i]); } mAnimatedIndex = mBackground.createAnimatedTile(8); mBackground.setCell(3, 0, mAnimatedIndex); mBackground.setCell(5, 0, mAnimatedIndex); mLayerManager.append(mBackground); } private void createAtmosphere(String atmosphereImageName) throws IOException { // Create the atmosphere layer Image atmosphereImage = Image.createImage(atmosphereImageName); mAtmosphere = new TiledLayer(8, 1, atmosphereImage, atmosphereImage.getWidth(), atmosphereImage.getHeight()); mAtmosphere.fillCells(0, 0, 8, 1, 1); mAtmosphere.setPosition(0, 96); mLayerManager.insert(mAtmosphere, 0); }

private void createQuatsch(String quatschImageName) throws IOException { // Create the sprite. Image quatschImage = Image.createImage(quatschImageName); mQuatsch = new Sprite(quatschImage, 48, 48); mQuatsch.setPosition(96 + (getWidth() - 48) / 2, 96); mQuatsch.defineReferencePixel(24, 24); setDirection(kLeft); setState(kStanding); mLayerManager.insert(mQuatsch, 1); } public void start() { mTrucking = true; Thread t = new Thread(this); t.start(); } public void run() { int w = getWidth(); int h = getHeight(); Graphics g = getGraphics(); int frameCount = 0; int factor = 2; int animatedDelta = 0; while (mTrucking) { if (isShown()) { int keyStates = getKeyStates(); if ((keyStates & LEFT_PRESSED) != 0) { setDirection(kLeft); setState(kRunning); mBackground.move(3, 0); mAtmosphere.move(3, 0); mQuatsch.nextFrame(); } else if ((keyStates & RIGHT_PRESSED) != 0) { setDirection(kRight); setState(kRunning); mBackground.move(-3, 0); mAtmosphere.move(-3, 0); mQuatsch.nextFrame(); } else { setState(kStanding); } frameCount++; if (frameCount % factor == 0) { int delta = 1; if (frameCount / factor < 10) delta = -1; mAtmosphere.move(delta, 0); if (frameCount / factor == 20) frameCount = 0; mBackground.setAnimatedTile(mAnimatedIndex, 8 + animatedDelta++); if (animatedDelta == 3) animatedDelta = 0;

} g.setColor(0x5b1793); g.fillRect(0, 0, w, h); mLayerManager.paint(g, 0, 0); flushGraphics(); } try { Thread.sleep(80); } catch (InterruptedException ie) {} } } public void stop() { mTrucking = false; } public void setVisible(int layerIndex, boolean show) { Layer layer = mLayerManager.getLayerAt(layerIndex); layer.setVisible(show); } public boolean isVisible(int layerIndex) { Layer layer = mLayerManager.getLayerAt(layerIndex); return layer.isVisible(); } private void setDirection(int newDirection) { if (newDirection == mDirection) return; if (mDirection == kLeft) mQuatsch.setTransform(Sprite.TRANS_MIRROR); else if (mDirection == kRight) mQuatsch.setTransform(Sprite.TRANS_NONE); mDirection = newDirection; } private void setState(int newState) { if (newState == mState) return; switch (newState) { case kStanding: mQuatsch.setFrameSequence(kStandingSequence); mQuatsch.setFrame(0); break; case kRunning: mQuatsch.setFrameSequence(kRunningSequence); break; default: break; } mState = newState; } }

(29) Sweep Game

/* Wireless Java 2nd edition Jonathan Knudsen Publisher: Apress ISBN: 1590590775 */ import javax.microedition.lcdui.*; import javax.microedition.lcdui.game.*; import javax.microedition.midlet.*; public class SweepGame extends MIDlet { public void startApp() { final SweepGameCanvas sweeper = new SweepGameCanvas(); sweeper.start(); sweeper.addCommand(new Command("Exit", Command.EXIT, 0)); sweeper.setCommandListener(new CommandListener() { public void commandAction(Command c, Displayable s) { sweeper.stop(); notifyDestroyed(); } }); Display.getDisplay(this).setCurrent(sweeper); } public void pauseApp() {} public void destroyApp(boolean unconditional) {} } class SweepGameCanvas extends GameCanvas implements Runnable {

private private private private

boolean mTrucking; int mTheta; int mBorder; int mDelay;

public SweepGameCanvas() { super(true); mTheta = 0; mBorder = 10; mDelay = 50; } public void start() { mTrucking = true; Thread t = new Thread(this); t.start(); } public void stop() { mTrucking = false; } public void render(Graphics g) { int width = getWidth(); int height = getHeight(); // Clear the Canvas. g.setGrayScale(255); g.fillRect(0, 0, width - 1, height - 1); int x = mBorder; int y = mBorder; int w = width - mBorder * 2; int h = height - mBorder * 2; for (int i = 0; i < 8; i++) { g.setGrayScale((8 - i) * 32 - 16); g.fillArc(x, y, w, h, mTheta + i * 10, 10); g.fillArc(x, y, w, h, (mTheta + 180) % 360 + i * 10, 10); } } public void run() { Graphics g = getGraphics(); while (mTrucking) { mTheta = (mTheta + 1) % 360; render(g); flushGraphics(); try { Thread.sleep(mDelay); } catch (InterruptedException ie) {} } } }

(30) Dungeon
/* Title: J2ME Games With MIDP2 Authors: Carol Hamer Publisher: Apress ISBN: 1590593820 */ import java.io.*; import import import import javax.microedition.lcdui.*; javax.microedition.lcdui.game.*; javax.microedition.midlet.*; javax.microedition.rms.*;

/** * This is the main class of the dungeon game. * * @author Carol Hamer */ public class Dungeon extends MIDlet implements CommandListener { //----------------------------------------------------// game object fields /** * The canvas that the dungeon is drawn on. */ private DungeonCanvas myCanvas; /** * the thread that advances the game clock. */ private GameThread myGameThread; //----------------------------------------------------// command fields /** * The button to exit the game. */ private Command myExitCommand = new Command("Exit", Command.EXIT, 99) ; /** * The command to save the game in progress. */ private Command mySaveCommand = new Command("Save Game", Command.SCRE EN, 2); /** * The command to restore a previously saved game. */ private Command myRestoreCommand = new Command("Restore Game", Command.SCREEN, 2);

/** * the command to start moving when the game is paused. */ private Command myGoCommand = new Command("Go", Command.SCREEN, 1); /** * the command to pause the game. */ private Command myPauseCommand = new Command("Pause", Command.SCREEN, 1); /** * the command to start a new game. */ private Command myNewCommand = new Command("Next Board", Command.SCRE EN, 1); //----------------------------------------------------// initialization and game state changes /** * Initialize the canvas and the commands. */ public Dungeon() { try { // create the canvas and set up the commands: myCanvas = new DungeonCanvas(this); myCanvas.addCommand(myExitCommand); myCanvas.addCommand(mySaveCommand); myCanvas.addCommand(myRestoreCommand); myCanvas.addCommand(myPauseCommand); myCanvas.setCommandListener(this); } catch (Exception e) { // if there's an error during creation, display it as an alert. errorMsg(e); } } /** * Switch the command to the play again command. (removing other comm ands * that are no longer relevant) */ void setNewCommand() { myCanvas.removeCommand(myPauseCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myNewCommand); } /** * Switch the command to the go command. (removing other commands tha t are * no longer relevant) */ void setGoCommand() { myCanvas.removeCommand(myPauseCommand);

myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myGoCommand); } /** * Switch the command to the pause command. (removing other commands that * are no longer relevant) */ void setPauseCommand() { myCanvas.removeCommand(myNewCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); } //---------------------------------------------------------------// implementation of MIDlet // these methods may be called by the application management // software at any time, so we always check fields for null // before calling methods on them. /** * Start the application. */ public void startApp() throws MIDletStateChangeException { if (myCanvas != null) { if (myGameThread == null) { // create the thread and start the game: myGameThread = new GameThread(myCanvas); myCanvas.start(); myGameThread.start(); } else { // in case this gets called again after // the application has been started once: myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } } } /** * Stop the threads and throw out the garbage. */ public void destroyApp(boolean unconditional) throws MIDletStateChangeException { myCanvas = null; if (myGameThread != null) { myGameThread.requestStop(); } myGameThread = null; System.gc(); } /** * Pause the game.

*/ public void pauseApp() { if (myCanvas != null) { setGoCommand(); } if (myGameThread != null) { myGameThread.pause(); } } //---------------------------------------------------------------// implementation of CommandListener /* * Respond to a command issued on the Canvas. (reset, exit, or change size * prefs). */ public void commandAction(Command c, Displayable s) { try { if (c == myGoCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.flushKeys(); myGameThread.resumeGame(); } else if (c == myPauseCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myPauseCommand); myCanvas.addCommand(myGoCommand); myGameThread.pause(); } else if (c == myNewCommand) { myCanvas.setNeedsRepaint(); // go to the next board and restart the game myCanvas.removeCommand(myNewCommand); myCanvas.addCommand(myPauseCommand); myCanvas.reset(); myGameThread.resumeGame(); /*} else if (c == Alert.DISMISS_COMMAND) { // if there was a serious enough error to // cause an alert, then we end the game // when the user is done reading the alert: // (Alert.DISMISS_COMMAND is the default // command that is placed on an Alert // whose timeout is FOREVER) destroyApp(false); notifyDestroyed();*/ } else if (c == mySaveCommand) { myCanvas.setNeedsRepaint(); myCanvas.saveGame(); } else if (c == myRestoreCommand) { myCanvas.setNeedsRepaint(); myCanvas.removeCommand(myNewCommand); myCanvas.removeCommand(myGoCommand); myCanvas.addCommand(myPauseCommand); myCanvas.revertToSaved(); } else if (c == myExitCommand) {

destroyApp(false); notifyDestroyed(); } } catch (Exception e) { errorMsg(e); } } //------------------------------------------------------// error methods /** * Converts an exception to a message and displays the message.. */ void errorMsg(Exception e) { if (e.getMessage() == null) { errorMsg(e.getClass().getName()); } else { errorMsg(e.getClass().getName() + ":" + e.getMessage()); } } /** * Displays an error message alert if something goes wrong. */ void errorMsg(String msg) { Alert errorAlert = new Alert("error", msg, null, AlertType.ERROR); errorAlert.setCommandListener(this); errorAlert.setTimeout(Alert.FOREVER); Display.getDisplay(this).setCurrent(errorAlert); } } /** * This class represents doors and keys. * * @author Carol Hamer */ class DoorKey extends Sprite { //--------------------------------------------------------// fields /** * The image file shared by all doors and keys. */ public static Image myImage; /** * A code int that indicates the door or key's color. */ private int myColor; //--------------------------------------------------------// get/set data

/** * @return the door or key's color. */ public int getColor() { return (myColor); } //--------------------------------------------------------// constructor and initializer static { try { myImage = Image.createImage("/images/keys.png"); } catch (Exception e) { throw (new RuntimeException( "DoorKey.<init>-->failed to load image, caught " + e.getClass() + ": " + e.getMessage())); } } /** * Standard constructor sets the image to the correct frame (accordin g to * whether this is a door or a key and what color it should be) and t hen * puts it in the correct location. */ public DoorKey(int color, boolean isKey, int[] gridCoordinates) { super(myImage, DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_W IDTH); myColor = color; int imageIndex = color * 2; if (isKey) { imageIndex++; } setFrame(imageIndex); setPosition(gridCoordinates[0] * DungeonManager.SQUARE_WIDTH, gridCoordinates[1] * DungeonManager.SQUARE_WIDTH); } } /** * This class is a set of simple utility functions that can be used to convert * standard data types to bytes and back again. It is used especially f or data * storage, but also for sending and receiving data. * * @author Carol Hamer */ class DataConverter { //-------------------------------------------------------// utilities to encode small, compactly-stored small ints.

/** * Encodes a coordinate pair into a byte. * * @param coordPair * a pair of integers to be compacted into a single byte f or * storage. WARNING: each of the two values MUST BE betwee n 0 and * 15 (inclusive). This method does not verify the length of the * array (which must be 2!) nor does it verify that the in ts are * of the right size. */ public static byte encodeCoords(int[] coordPair) { // get the byte value of the first coordinate: byte retVal = (new Integer(coordPair[0])).byteValue(); // move the first coordinate's value up to the top // half of the storage byte: retVal = (new Integer(retVal << 4)).byteValue(); // store the second coordinate in the lower half // of the byte: retVal += (new Integer(coordPair[1])).byteValue(); return (retVal); } /** * Encodes eight ints into a byte. This could be easily modified to e ncode * eight booleans. * * @param eight * an array of at least eight ints. WARNING: all values mu st be 0 * or 1! This method does not verify that the values are i n the * correct range nor does it verify that the array is long * enough. * @param offset * the index in the array eight to start reading data from . * (should usually be 0) */ public static byte encode8(int[] eight, int offset) { // get the byte value of the first int: byte retVal = (new Integer(eight[offset])).byteValue(); // progressively move the data up one bit in the // storage byte and then record the next int in // the lowest spot in the storage byte: for (int i = offset + 1; i < 8 + offset; i++) { retVal = (new Integer(retVal << 1)).byteValue(); retVal += (new Integer(eight[i])).byteValue(); } return (retVal); }

//-------------------------------------------------------// utilities to decode small, compactly-stored small ints. /** * Turns a byte into a pair of coordinates. */ public static int[] decodeCoords(byte coordByte) { int[] retArray = new int[2]; // we perform a bitwise and with the value 15 // in order to just get the bits of the lower // half of the byte: retArray[1] = coordByte & 15; // To get the bits of the upper half of the // byte, we perform a shift to move them down: retArray[0] = coordByte >> 4; // bytes in Java are generally assumed to be // signed, but in this coding algorithm we // would like to treat them as unsigned: if (retArray[0] < 0) { retArray[0] += 16; } return (retArray); } /** * Turns a byte into eight ints. */ public static int[] decode8(byte data) { int[] retArray = new int[8]; // The flag allows us to look at each bit individually // to determine if it is 1 or 0. The number 128 // corresponds to the highest bit of a byte, so we // start with that one. int flag = 128; // We use a loop that checks // the data bit by bit by performing a bitwise // and (&) between the data byte and a flag: for (int i = 0; i < 8; i++) { if ((flag & data) != 0) { retArray[i] = 1; } else { retArray[i] = 0; } // move the flag down one bit so that we can // check the next bit of data on the next pass // through the loop: flag = flag >> 1; } return (retArray); } //-------------------------------------------------------// standard integer interpretation /** * Uses an input stream to convert an array of bytes to an int. */

public static int parseInt(byte[] data) throws IOException { DataInputStream stream = new DataInputStream(new ByteArrayInputStre am( data)); int retVal = stream.readInt(); stream.close(); return (retVal); } /** * Uses an output stream to convert an int to four bytes. */ public static byte[] intToFourBytes(int i) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(4); DataOutputStream dos = new DataOutputStream(baos); dos.writeInt(i); baos.close(); dos.close(); byte[] retArray = baos.toByteArray(); return (retArray); } //-------------------------------------------------------// integer interpretation illustrated /** * Java appears to treat a byte as being signed when returning it as an * int-this function converts from the signed value to the corresponding * unsigned value. This method is used by nostreamParseInt. */ public static int unsign(int signed) { int retVal = signed; if (retVal < 0) { retVal += 256; } return (retVal); } /** * Takes an array of bytes and returns an int. This version will retu rn the * same value as the method parseInt above. This version is included in * order to illustrate how Java encodes int values in terms of bytes. * * @param data * an array of 1, 2, or 4 bytes. */ public static int nostreamParseInt(byte[] data) { // byte 0 is the high byte which is assumed // to be signed. As we add the lower bytes // one by one, we unsign them because because // a single byte alone is interpreted as signed, // but in an int only the top byte should be signed. // (note that the high byte is the first one in the array)

int retVal = data[0]; for (int i = 1; i < data.length; i++) { retVal = retVal << 8; retVal += unsign(data[i]); } return (retVal); } /** * Takes an arbitrary int and returns an array of four bytes. This ve rsion * will return the same byte array as the method intToFourBytes above . This * version is included in order to illustrate how Java encodes int va lues in * terms of bytes. */ public static byte[] nostreamIntToFourBytes(int i) { byte[] fourBytes = new byte[4]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get all four bytes by taking the lowest // byte four times and moving the whole int // down by one byte between each one. // (note that the high byte is the first one in the array) fourBytes[3] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[2] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[0] = (new Integer(i)).byteValue(); return (fourBytes); } /** * Takes an int between 32768 and 32767 and returns an array of two bytes. * This does not verify that the argument is of the right size. If th e * absolute value of i is too high, it will not be encoded correctly. */ public static byte[] nostreamIntToTwoBytes(int i) { byte[] twoBytes = new byte[2]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get the lower two bytes by taking the lowest // byte twice and moving the whole int // down by one byte between each one. twoBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; twoBytes[0] = (new Integer(i)).byteValue(); return (twoBytes); } } /**

* This class contains the data for the map of the dungeon.. * * @author Carol Hamer */ class BoardDecoder { //-------------------------------------------------------// fields /** * The coordinates of where the player starts on the map in terms of the * array indices. */ private int[] myPlayerSquare; /** * The coordinates of the goal (crown). */ private int[] myGoalSquare; /** * The coordinates of the doors. the there should be two in a row of each * color, following the same sequence as the keys. */ private int[][] myDoors; /** * The coordinates of the Keys. the there should be of each color, fo llowing * the same sequence as the doors. */ private int[][] myKeys; /** * The coordinates of the stone walls of the maze, encoded bit by bit . */ private TiledLayer myLayer; /** * The data in bytes that gives the various boards. This was created using * EncodingUtils... This is a twodimensional array: Each of the four main * sections corresponds to one of the four possible boards. */ private static byte[][] myData = { { 0, 0, -108, -100, -24, 65, 21, 58, 53, -54, -116, -58, -56, 84, 115, -118, -1, -1, -128, 1, -103, -15, -128, 25, -97, -127, -128, 79, -14, 1, -126, 121, -122, 1, -113, -49, -116, 1, -100, -3, -124, 5, -25, -27, -128, 1, -1, -1 }, { 0, 1, 122, 90, -62, 34, -43, 72, -59, -29, 56, -55, 98, 126, 79,

61, -1, -1, -125, 1, -128, 17, -26, 29, -31, 57, -72, 1, -128, -51, -100, 65, -124, 57, -2, 1, -126, 13, -113, 1, -97, 25, -127, -99, -8, 1, -1, -1 }, { 0, 2, 108, -24, 18, -26, 102, 30, -58, 46, -28, -88, 34, 98, 97, -41, -1, -1, -96, 1, -126, 57, -9, 97, -127, 69, -119, 73, -127, 1, -109, 59, -126, 1, -26, 103, -127, 65, -103, 115, -127, 65, -25, 73, -128, 1, -1, -1 }, { 0, 3, -114, 18, -34, 27, -39, -60, -76, -50, 118, 90, 82, -88, 34, -74, -1, -1, -66, 1, -128, 121, -26, 125, -128, -123, -103, 29, -112, 1, -109, 49, -112, 1, -116, -31, -128, 5, -122, 5, -32, 13, -127, -51, -125, 1, -1, -1 }, }; //-------------------------------------------------------// initialization /** * Constructor fills data fields by interpreting the data bytes. */ public BoardDecoder(int boardNum) throws Exception { // we start by selecting the two dimensional // array corresponding to the desired board: byte[] data = myData[boardNum]; // The first two bytes give the version number and // the board number, but we ignore them because // they are assumed to be correct. // The third byte of the first array is the first one // we read: it gives the player's starting coordinates: myPlayerSquare = DataConverter.decodeCoords(data[2]); // the next byte gives the coordinates of the crown: myGoalSquare = DataConverter.decodeCoords(data[3]); // the next four bytes give the coordinates of the keys: myKeys = new int[4][]; for (int i = 0; i < myKeys.length; i++) { myKeys[i] = DataConverter.decodeCoords(data[i + 4]); } // the next eight bytes give the coordinates of the doors: myDoors = new int[8][]; for (int i = 0; i < myDoors.length; i++) { myDoors[i] = DataConverter.decodeCoords(data[i + 8]); } // now we create the TiledLayer object that is the // background dungeon map: myLayer = new TiledLayer(16, 16, Image.createImage("/images/stone.png"), DungeonManager.SQUARE_WIDTH, DungeonManager.SQUARE_WIDTH); // now we call an internal utility that reads the array // of data that gives the positions of the blocks in the // walls of this dungeon: decodeDungeon(data, myLayer, 16); } //-------------------------------------------------------// get/set data /** * @return the number of boards currently stored in this class.

*/ public static int getNumBoards() { return (myData.length); } /** * get the coordinates of where the player starts on the map in terms of the * array indices. */ public int[] getPlayerSquare() { return (myPlayerSquare); } /** * get the coordinates of the goal crown in terms of the array indice s. */ public int[] getGoalSquare() { return (myGoalSquare); } /** * get the tiled layer that gives the map of the dungeon. */ public TiledLayer getLayer() { return (myLayer); } /** * Creates the array of door sprites. (call this only once to avoid c reating * redundant sprites). */ DoorKey[] createDoors() { DoorKey[] retArray = new DoorKey[8]; for (int i = 0; i < 4; i++) { retArray[2 * i] = new DoorKey(i, false, myDoors[2 * i]); retArray[2 * i + 1] = new DoorKey(i, false, myDoors[2 * i + 1]); } return (retArray); } /** * Creates the array of key sprites. (call this only once to avoid cr eating * redundant sprites.) */ DoorKey[] createKeys() { DoorKey[] retArray = new DoorKey[4]; for (int i = 0; i < 4; i++) { retArray[i] = new DoorKey(i, true, myKeys[i]); } return (retArray); } //--------------------------------------------------------

//

decoding utilities

/** * Takes a dungeon given as a byte array and uses it to set the tiles of a * tiled layer. * * The TiledLayer in this case is a 16 x 16 grid in which each square can be * either blank (value of 0) or can be filled with a stone block (val ue of * 1). Therefore each square requires only one bit of information. Ea ch byte * of data in the array called "data" records the frame indices of ei ght * squares in the grid. */ private static void decodeDungeon(byte[] data, TiledLayer dungeon, int offset) throws Exception { if (data.length + offset < 32) { throw (new Exception( "BoardDecoder.decodeDungeon-->not enough data!!!")); } // a frame index of zero indicates a blank square // (this is always true in a TiledLayer). // This TiledLayer has only one possible (non-blank) // frame, so a frame index of 1 indicates a stone block int frame = 0; // Each of the 32 bytes in the data array records // the frame indices of eight block in the 16 x 16 // grid. Two bytes give one row of the dungeon, // so we have the array index go from zero to 16 // to set the frame indices fro each of the 16 rows. for (int i = 0; i < 16; i++) { // The flag allows us to look at each bit individually // to determine if it is 1 or 0. The number 128 // corresponds to the highest bit of a byte, so we // start with that one. int flag = 128; // Here we check two bytes at the same time // (the two bytes together correspond to one row // of the dungeon). We use a loop that checks // the bytes bit by bit by performing a bitwise // and (&) between the data byte and a flag: for (int j = 0; j < 8; j++) { if ((data[offset + 2 * i] & flag) != 0) { frame = 1; } else { frame = 0; } dungeon.setCell(j, i, frame); if ((data[offset + 2 * i + 1] & flag) != 0) { frame = 1; } else { frame = 0; } dungeon.setCell(j + 8, i, frame);

// move the flag down one bit so that we can // check the next bit of data on the next pass // through the loop: flag = flag >> 1; } } } } /** * This class contains the loop that keeps the game running. * * @author Carol Hamer */ class GameThread extends Thread { //--------------------------------------------------------// fields /** * Whether or not the main thread would like this thread to pause. */ private boolean myShouldPause; /** * Whether or not the main thread would like this thread to stop. */ private static boolean myShouldStop; /** * A handle back to the graphical components. */ private DungeonCanvas myDungeonCanvas; /** * The System.time of the last screen refresh, used to regulate refre sh * speed. */ private long myLastRefreshTime; //---------------------------------------------------------// initialization /** * standard constructor. */ GameThread(DungeonCanvas canvas) { myDungeonCanvas = canvas; } //---------------------------------------------------------// utilities /**

* Get the amount of time to wait between screen refreshes. Normally we wait * only a single millisecond just to give the main thread a chance to update * the keystroke info, but this method ensures that the game will not * attempt to show too many frames per second. */ private long getWaitTime() { long retVal = 1; long difference = System.currentTimeMillis() - myLastRefreshTime; if (difference < 75) { retVal = 75 - difference; } return (retVal); } //---------------------------------------------------------// actions /** * pause the game. */ void pause() { myShouldPause = true; } /** * restart the game after a pause. */ synchronized void resumeGame() { myShouldPause = false; notify(); } /** * stops the game. */ synchronized void requestStop() { myShouldStop = true; this.notify(); } /** * start the game.. */ public void run() { // flush any keystrokes that occurred before the // game started: myDungeonCanvas.flushKeys(); myShouldStop = false; myShouldPause = false; while (true) { myLastRefreshTime = System.currentTimeMillis(); if (myShouldStop) { break; } myDungeonCanvas.checkKeys();

myDungeonCanvas.updateScreen(); // we do a very short pause to allow the other thread // to update the information about which keys are pressed: synchronized (this) { try { wait(getWaitTime()); } catch (Exception e) { } } if (myShouldPause) { synchronized (this) { try { wait(); } catch (Exception e) { } } } } } } /** * This class contains the data for a game currently in progress. used to store * a game and to resume a stored game. * * @author Carol Hamer */ class GameInfo { //-------------------------------------------------------// fields /** * The name of the datastore. */ public static final String STORE = "GameInfo"; /** * This is set to true if an attempt is made to read a game when no g ame has * been saved. */ private boolean myNoDataSaved; /** * The number that indicates which board the player is currently on. */ private int myBoardNum; /** * The amount of time that has passed. */ private int myTime;

/** * The coordinates of where the player is on the board. coordinate va lues * must be between 0 and 15. */ private int[] myPlayerSquare; /** * The coordinates of where the keys are currently found. MUST BE fou r sets * of two integer coordinates. coordinate values must be between 0 an d 15. */ private int[][] myKeyCoords; /** * The list of which doors are currently open. 0 = open 1 = closed WA RNING: * this array MUST have length 8. */ private int[] myDoorsOpen; /** * The number of the key that is currently being held by the player. if no * key is held, then the value is -1. */ private int myHeldKey; //-------------------------------------------------------// data gets/sets /** * @return true if no saved game records were found. */ boolean getIsEmpty() { return (myNoDataSaved); } /** * @return The number that indicates which board the player is curren tly on. */ int getBoardNum() { return (myBoardNum); } /** * @return The number of the key that is currently being held by the player. * if no key is held, then the value is -1. */ int getHeldKey() { return (myHeldKey); } /**

* @return The amount of time that has passed. */ int getTime() { return (myTime); } /** * @return The coordinates of where the player is on the board. coord inate * values must be between 0 and 15. */ int[] getPlayerSquare() { return (myPlayerSquare); } /** * @return The coordinates of where the keys are currently found. MUS T BE * four sets of two integer coordinates. coordinate values mu st be * between 0 and 15. */ int[][] getKeyCoords() { return (myKeyCoords); } /** * @return The list of which doors are currently open. 0 = open 1 = c losed * WARNING: this array MUST have length 8. */ int[] getDoorsOpen() { return (myDoorsOpen); } //-------------------------------------------------------// constructors /** * This constructor records the game info of a game currently in prog ress. */ GameInfo(int boardNum, int time, int[] playerSquare, int[][] keyCoord s, int[] doorsOpen, int heldKey) throws Exception { myBoardNum = boardNum; myTime = time; myPlayerSquare = playerSquare; myKeyCoords = keyCoords; myDoorsOpen = doorsOpen; myHeldKey = heldKey; encodeInfo(); } /** * This constructor reads the game configuration from memory. This is used

* to reconstruct a saved game. */ GameInfo() { RecordStore store = null; try { // if the record store does not yet exist, don't // create it store = RecordStore.openRecordStore(STORE, false); if ((store != null) && (store.getNumRecords() > 0)) { // the first record has id number 1 // it should also be the only record since this // particular game stores only one game. byte[] data = store.getRecord(1); myBoardNum = data[0]; myPlayerSquare = DataConverter.decodeCoords(data[1]); myKeyCoords = new int[4][]; myKeyCoords[0] = DataConverter.decodeCoords(data[2]); myKeyCoords[1] = DataConverter.decodeCoords(data[3]); myKeyCoords[2] = DataConverter.decodeCoords(data[4]); myKeyCoords[3] = DataConverter.decodeCoords(data[5]); myDoorsOpen = DataConverter.decode8(data[6]); myHeldKey = data[7]; byte[] fourBytes = new byte[4]; System.arraycopy(data, 8, fourBytes, 0, 4); myTime = DataConverter.parseInt(fourBytes); } else { myNoDataSaved = true; } } catch (Exception e) { // this throws when the record store doesn't exist. // for that or any error, we assume no data is saved: myNoDataSaved = true; } finally { try { if (store != null) { store.closeRecordStore(); } } catch (Exception e) { // if the record store is open this shouldn't throw. } } } //-------------------------------------------------------// encoding method /** * Turn the data into a byte array and save it. */ private void encodeInfo() throws Exception { RecordStore store = null; try { byte[] data = new byte[12]; data[0] = (new Integer(myBoardNum)).byteValue(); data[1] = DataConverter.encodeCoords(myPlayerSquare); data[2] = DataConverter.encodeCoords(myKeyCoords[0]); data[3] = DataConverter.encodeCoords(myKeyCoords[1]);

data[4] = DataConverter.encodeCoords(myKeyCoords[2]); data[5] = DataConverter.encodeCoords(myKeyCoords[3]); data[6] = DataConverter.encode8(myDoorsOpen, 0); data[7] = (new Integer(myHeldKey)).byteValue(); byte[] timeBytes = DataConverter.intToFourBytes(myTime); System.arraycopy(timeBytes, 0, data, 8, 4); // if the record store does not yet exist, the second // arg "true" tells it to create. store = RecordStore.openRecordStore(STORE, true); int numRecords = store.getNumRecords(); if (numRecords > 0) { store.setRecord(1, data, 0, data.length); } else { store.addRecord(data, 0, data.length); } } catch (Exception e) { throw (e); } finally { try { if (store != null) { store.closeRecordStore(); } } catch (Exception e) { // if the record store is open this shouldn't throw. } } } } /** * This class contains the data for the map of the dungeon. This is a u tility * class that allows a developer to write the data for a board in a sim ple * format, then this class encodes the data in a format that the game c an use. * * note that the data that this class encodes is hardcoded. that is because * this class is intended to be used only a few times to encode the dat a. Once * the board data has been encoded, it never needs to be encoded again. The * encoding methods used in this class could be generalized to be used to create * a board editor which would allow a user to easily create new boards, but that * is an exercise for another day... * * @author Carol Hamer */ class EncodingUtils { //-------------------------------------------------------// fields

/** * data for which squares are = * filled */ private int[][] mySquares = { { 1, 1, 1, 1, 1, 1, 1, 1, { 1, 0, 0, 0, 0, 0, 1, 1, { 1, 0, 0, 0, 0, 0, 0, 0, { 1, 1, 1, 0, 0, 1, 1, 0, { 1, 1, 1, 0, 0, 0, 0, 1, { 1, 0, 1, 1, 1, 0, 0, 0, { 1, 0, 0, 0, 0, 0, 0, 0, { 1, 0, 0, 1, 1, 1, 0, 0, { 1, 0, 0, 0, 0, 1, 0, 0, { 1, 1, 1, 1, 1, 1, 1, 0, { 1, 0, 0, 0, 0, 0, 1, 0, { 1, 0, 0, 0, 1, 1, 1, 1, { 1, 0, 0, 1, 1, 1, 1, 1, { 1, 0, 0, 0, 0, 0, 0, 1, { 1, 1, 1, 1, 1, 0, 0, 0, { 1, 1, 1, 1, 1, 1, 1, 1,

filled and which are blank. 0 = empty 1

1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1,

1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1,

1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,

1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1,

1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1,

1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1,

1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,

1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

}, }, }, }, }, }, }, }, }, }, }, }, }, }, }, }, };

/** * The coordinates of where the player starts on the map in terms of the * array indices. */ private int[] myPlayerSquare = { 7, 10 }; /** * The coordinates of the goal (crown). */ private int[] myGoalSquare = { 5, 10 }; //-------------------------------------------------------// get/set data /** * Creates the array of door sprites. (call this only once to avoid c reating * redundant sprites). */ int[][] getDoorCoords() { int[][] retArray = new int[8][]; for (int i = 0; i < retArray.length; i++) { retArray[i] = new int[2]; } // red retArray[0][0] = 12; retArray[0][1] = 5; retArray[1][0] = 14; retArray[1][1] = 3; // green retArray[2][0] = 3; retArray[2][1] = 8;

retArray[3][0] = 12; retArray[3][1] = 9; // blue retArray[4][0] = 6; retArray[4][1] = 2; retArray[5][0] = 7; retArray[5][1] = 14; // yellow retArray[6][0] = 11; retArray[6][1] = 1; retArray[7][0] = 3; retArray[7][1] = 13; return (retArray); } /** * Creates the array of key sprites. (call this only once to avoid cr eating * redundant sprites.) */ int[][] getKeyCoords() { int[][] retArray = new int[4][]; for (int i = 0; i < retArray.length; i++) { retArray[i] = new int[2]; } // red retArray[0][0] = 12; retArray[0][1] = 2; // green retArray[1][0] = 2; retArray[1][1] = 2; // blue retArray[2][0] = 13; retArray[2][1] = 5; // yellow retArray[3][0] = 4; retArray[3][1] = 8; return (retArray); } //-------------------------------------------------------// encoding / decoding utilities /** * Encodes the entire dungeon. */ byte[][] encodeDungeon() { byte[][] retArray = new byte[2][]; retArray[0] = new byte[16]; // the first byte is the version number: retArray[0][0] = 0; // the second byte is the board number: retArray[0][1] = 0; // the player's start square: retArray[0][2] = DataConverter.encodeCoords(myPlayerSquare); // the goal (crown) square: retArray[0][3] = DataConverter.encodeCoords(myGoalSquare);

//encode the keys: int[][] keyCoords = getKeyCoords(); for (int i = 0; i < keyCoords.length; i++) { retArray[0][i + 4] = DataConverter.encodeCoords(keyCoords[i]); } //encode the doors: int[][] doorCoords = getDoorCoords(); for (int i = 0; i < doorCoords.length; i++) { retArray[0][i + 8] = DataConverter.encodeCoords(doorCoords[i]); } //encode the maze: try { retArray[1] = encodeDungeon(mySquares); } catch (Exception e) { e.printStackTrace(); } return (retArray); } /** * Takes a dungeon given in terms of an array of 1s and 0s and turns it into * an array of bytes. WARNING: the array MUST BE 16 X 16. */ static byte[] encodeDungeon(int[][] dungeonMap) throws Exception { if ((dungeonMap.length != 16) || (dungeonMap[0].length != 16)) { throw (new Exception( "EncodingUtils.encodeDungeon-->must be 16x16!!!")); } byte[] retArray = new byte[32]; for (int i = 0; i < 16; i++) { retArray[2 * i] = DataConverter.encode8(dungeonMap[i], 0); retArray[2 * i + 1] = DataConverter.encode8(dungeonMap[i], 8); } return (retArray); } //-------------------------------------------------------// main prints the bytes to standard out. // (note that this class is not intended to be run as a MIDlet) /** * Prints the byte version of the board to standard out. */ public static void main(String[] args) { try { EncodingUtils map = new EncodingUtils(); byte[][] data = map.encodeDungeon(); System.out.println("EncodingUtils.main-->dungeon encoded"); System.out.print("{\n " + data[0][0]); for (int i = 1; i < data[0].length; i++) { System.out.print(", " + data[0][i]); } for (int i = 1; i < data[1].length; i++) { System.out.print(", " + data[1][i]); } System.out.println("\n};");

} catch (Exception e) { e.printStackTrace(); } } } /** * This class handles the graphics objects. * * @author Carol Hamer */ class DungeonManager extends LayerManager { //--------------------------------------------------------// dimension fields // (constant after initialization) /** * The xcoordinate of the place on the game canvas where the LayerManager * window should appear, in terms of the coordiantes of the game canv as. */ static int CANVAS_X; /** * The ycoordinate of the place on the game canvas where the LayerManager * window should appear, in terms of the coordiantes of the game canv as. */ static int CANVAS_Y; /** * The width of the display window. */ static int DISP_WIDTH; /** * The height of this object's visible region. */ static int DISP_HEIGHT; /** * the (right or left) distance the player goes in a single keystroke . */ static final int MOVE_LENGTH = 8; /** * The width of the square tiles that this game is divided into. This is the * width of the stone walls as well as the princess and the ghost. */ static final int SQUARE_WIDTH = 24;

/** * The jump index that indicates that no jump is currently in progres s.. */ static final int NO_JUMP = -6; /** * The maximum speed for the player's fall.. */ static final int MAX_FREE_FALL = 3; //--------------------------------------------------------// game object fields /** * the handle back to the canvas. */ private DungeonCanvas myCanvas; /** * the background dungeon. */ private TiledLayer myBackground; /** * the player. */ private Sprite myPrincess; /** * the goal. */ private Sprite myCrown; /** * the doors. */ private DoorKey[] myDoors; /** * the keys. */ private DoorKey[] myKeys; /** * the key currently held by the player. */ private DoorKey myHeldKey; /** * The leftmost xcoordinate that should be visible on the screen in terms * of this objects internal coordinates. */ private int myViewWindowX; /**

* The top ycoordinate that should be visible on the screen in terms of * this objects internal coordinates. */ private int myViewWindowY; /** * Where the princess is in the jump sequence. */ private int myIsJumping = NO_JUMP; /** * Whether or not the screen needs to be repainted. */ private boolean myModifiedSinceLastPaint = true; /** * Which board we're playing on. */ private int myCurrentBoardNum = 0; //----------------------------------------------------// gets/sets /** * Tell the layer manager that it needs to repaint. */ public void setNeedsRepaint() { myModifiedSinceLastPaint = true; } //----------------------------------------------------// initialization // set up or save game data. /** * Constructor merely sets the data. * * @param x * The xcoordinate of the place on the game canvas where the * LayerManager window should appear, in terms of the coor diantes * of the game canvas. * @param y * The ycoordinate of the place on the game canvas where the * LayerManager window should appear, in terms of the coor diantes * of the game canvas. * @param width * the width of the region that is to be occupied by the * LayoutManager. * @param height * the height of the region that is to be occupied by the * LayoutManager. * @param canvas

* on.

the DungeonCanvas that this LayerManager should appear

*/ public DungeonManager(int x, int y, int width, int height, DungeonCanvas canvas) throws Exception { myCanvas = canvas; CANVAS_X = x; CANVAS_Y = y; DISP_WIDTH = width; DISP_HEIGHT = height; // create a decoder object that creates the dungeon and // its associated Sprites from data. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // starts on. int[] playerCoords = decoder.getPlayerSquare(); // create the player sprite myPrincess = new Sprite(Image.createImage("/images/princess.png"), SQUARE_WIDTH, SQUARE_WIDTH); myPrincess.setFrame(1); // we define the reference pixel to be in the middle // of the princess image so that when the princess turns // from right to left (and vice versa) she does not // appear to move to a different location. myPrincess.defineReferencePixel(SQUARE_WIDTH / 2, 0); // the dungeon is a 16x16 grid, so the array playerCoords // gives the player's location in terms of the grid, and // then we multiply those coordinates by the SQUARE_WIDTH // to get the precise pixel where the player should be // placed (in terms of the LayerManager's coordinate system) myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH * playerCoords[1]); // we append all of the Layers (TiledLayer and Sprite) // so that this LayerManager will paint them when // flushGraphics is called. append(myPrincess); // get the coordinates of the square where the crown // should be placed. int[] goalCoords = decoder.getGoalSquare(); myCrown = new Sprite(Image.createImage("/images/crown.png")); myCrown.setPosition( (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); append(myCrown); // The decoder creates the door and key sprites and places // them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); } for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); }

// append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null; System.gc(); } /** * sets all variables back to their initial positions. */ void reset() throws Exception { // first get rid of the old board: for (int i = 0; i < myDoors.length; i++) { remove(myDoors[i]); } myHeldKey = null; for (int i = 0; i < myKeys.length; i++) { remove(myKeys[i]); } remove(myBackground); // now create the new board: myCurrentBoardNum++; // in this version we go back to the beginning if // all boards have been completed. if (myCurrentBoardNum == BoardDecoder.getNumBoards()) { myCurrentBoardNum = 0; } // we create a new decoder object to read and interpret // all of the data for the current board. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // starts on. int[] playerCoords = decoder.getPlayerSquare(); // the dungeon is a 16x16 grid, so the array playerCoords // gives the player's location in terms of the grid, and // then we multiply those coordinates by the SQUARE_WIDTH // to get the precise pixel where the player should be // placed (in terms of the LayerManager's coordinate system) myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WIDTH * playerCoords[1]); myPrincess.setFrame(1); // get the coordinates of the square where the crown // should be placed. int[] goalCoords = decoder.getGoalSquare(); myCrown.setPosition( (SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); // The decoder creates the door and key sprites and places

// them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); } for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); } // append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null; System.gc(); } /** * sets all variables back to the position in the saved game. * * @return the time on the clock of the saved game. */ int revertToSaved() throws Exception { int retVal = 0; // first get rid of the old board: for (int i = 0; i < myDoors.length; i++) { remove(myDoors[i]); } myHeldKey = null; for (int i = 0; i < myKeys.length; i++) { remove(myKeys[i]); } remove(myBackground); // now get the info of the saved game // only one game is saved at a time, and the GameInfo object // will read the saved game's data from memory. GameInfo info = new GameInfo(); if (info.getIsEmpty()) { // if no game has been saved, we start from the beginning. myCurrentBoardNum = 0; reset(); } else { // get the time on the clock of the saved game. retVal = info.getTime(); // get the number of the board the saved game was on. myCurrentBoardNum = info.getBoardNum(); // create the BoradDecoder that gives the data for the // desired board. BoardDecoder decoder = new BoardDecoder(myCurrentBoardNum); // get the background TiledLayer

myBackground = decoder.getLayer(); // get the coordinates of the square that the princess // was on in the saved game. int[] playerCoords = info.getPlayerSquare(); myPrincess.setPosition(SQUARE_WIDTH * playerCoords[0], SQUARE_WID TH * playerCoords[1]); myPrincess.setFrame(1); // get the coordinates of the square where the crown // should be placed (this is given by the BoardDecoder // and not from the data of the saved game because the // crown does not move during the game. int[] goalCoords = decoder.getGoalSquare(); myCrown.setPosition((SQUARE_WIDTH * goalCoords[0]) + (SQUARE_WIDTH / 4), (SQUARE_WIDTH * goalCoords[1]) + (SQUARE_WIDTH / 2)); // The decoder creates the door and key sprites and places // them in the correct locations in terms of the LayerManager's // coordinate system. myDoors = decoder.createDoors(); myKeys = decoder.createKeys(); // get an array of ints that lists whether each door is // open or closed in the saved game int[] openDoors = info.getDoorsOpen(); for (int i = 0; i < myDoors.length; i++) { append(myDoors[i]); if (openDoors[i] == 0) { // if the door was open, make it invisible myDoors[i].setVisible(false); } } // the keys can be moved by the player, so we get their // coordinates from the GameInfo saved data. int[][] keyCoords = info.getKeyCoords(); for (int i = 0; i < myKeys.length; i++) { append(myKeys[i]); myKeys[i].setPosition(SQUARE_WIDTH * keyCoords[i][0], SQUARE_WIDTH * keyCoords[i][1]); } // if the player was holding a key in the saved game, // we have the player hold that key and set it to invisible. int heldKey = info.getHeldKey(); if (heldKey != -1) { myHeldKey = myKeys[heldKey]; myHeldKey.setVisible(false); } // append the background last so it will be painted first. append(myBackground); // this sets the view screen so that the player is // in the center. myViewWindowX = SQUARE_WIDTH * playerCoords[0] - ((DISP_WIDTH - SQUARE_WIDTH) / 2); myViewWindowY = SQUARE_WIDTH * playerCoords[1] - ((DISP_HEIGHT - SQUARE_WIDTH) / 2); // a number of objects are created in order to set up the game, // but they should be eliminated to free up memory: decoder = null;

System.gc(); } return (retVal); } /** * save the current game in progress. */ void saveGame(int gameTicks) throws Exception { int[] playerSquare = new int[2]; // the coordinates of the player are given in terms of // the 16 x 16 dungeon grid. We divide the player's // pixel coordinates to ge the right grid square. // If the player was not precisely alligned with a // grid square when the game was saved, the difference // will be shaved off. playerSquare[0] = myPrincess.getX() / SQUARE_WIDTH; playerSquare[1] = myPrincess.getY() / SQUARE_WIDTH; // save the coordinates of the current locations of // the keys, and if a key is currently held by the // player, we save the info of which one it was. int[][] keyCoords = new int[4][]; int heldKey = -1; for (int i = 0; i < myKeys.length; i++) { keyCoords[i] = new int[2]; keyCoords[i][0] = myKeys[i].getX() / SQUARE_WIDTH; keyCoords[i][1] = myKeys[i].getY() / SQUARE_WIDTH; if ((myHeldKey != null) && (myKeys[i] == myHeldKey)) { heldKey = i; } } // save the information of which doors were open. int[] doorsOpen = new int[8]; for (int i = 0; i < myDoors.length; i++) { if (myDoors[i].isVisible()) { doorsOpen[i] = 1; } } // take all of the information we've gathered and // create a GameInfo object that will save the info // in the device's memory. GameInfo info = new GameInfo(myCurrentBoardNum, gameTicks, playerSquare, keyCoords, doorsOpen, heldKey); } //------------------------------------------------------// graphics methods /** * paint the game graphic on the screen. */ public void paint(Graphics g) throws Exception { // only repaint if something has changed: if (myModifiedSinceLastPaint) { g.setColor(DungeonCanvas.WHITE); // paint the background white to cover old game objects // that have changed position since last paint.

// here coordinates are given // with respect to the graphics (canvas) origin: g.fillRect(0, 0, DISP_WIDTH, DISP_HEIGHT); // here coordinates are given // with respect to the LayerManager origin: setViewWindow(myViewWindowX, myViewWindowY, DISP_WIDTH, DISP_HEIG HT); // call the paint funstion of the superclass LayerManager // to paint all of the Layers paint(g, CANVAS_X, CANVAS_Y); // don't paint again until something changes: myModifiedSinceLastPaint = false; } } //------------------------------------------------------// game movements /** * respond to keystrokes by deciding where to move and then moving th e * pieces and the view window correspondingly. */ void requestMove(int horizontal, int vertical) { if (horizontal != 0) { // see how far the princess can move in the desired // horizontal direction (if not blocked by a wall // or closed door) horizontal = requestHorizontal(horizontal); } // vertical < 0 indicates that the user has // pressed the UP button and would like to jump. // therefore, if we're not currently jumping, // we begin the jump. if ((myIsJumping == NO_JUMP) && (vertical < 0)) { myIsJumping++; } else if (myIsJumping == NO_JUMP) { // if we're not jumping at all, we need to check // if the princess should be falling: // we (temporarily) move the princess down and see if that // causes a collision with the floor: myPrincess.move(0, MOVE_LENGTH); // if the princess can move down without colliding // with the floor, then we set the princess to // be falling. The variable myIsJumping starts // negative while the princess is jumping up and // is zero or positive when the princess is coming // back down. We therefore set myIsJumping to // zero to indicate that the princess should start // falling. if (!checkCollision()) { myIsJumping = 0; } // we move the princess Sprite back to the correct // position she was at before we (temporarily) moved // her down to see if she would fall. myPrincess.move(0, -MOVE_LENGTH);

} // // // // if

if the princess is currently jumping or falling, we calculate the vertical distance she should move (taking into account the horizontal distance that she is also moving). (myIsJumping != NO_JUMP) { vertical = jumpOrFall(horizontal);

} // now that we've calculated how far the princess // should move, we move her. (this is a call to // another internal method of this method // suite, it is not a built-in LayerManager method): move(horizontal, vertical); } /** * Internal to requestMove. Calculates what the real horizontal dista nce * moved should be after taking obstacles into account. * * @return the horizontal distance that the player can move. */ private int requestHorizontal(int horizontal) { // we (temporarily) move her to the right or left // and see if she hits a wall or a door: myPrincess.move(horizontal * MOVE_LENGTH, 0); if (checkCollision()) { // if she hits something, then she's not allowed // to go in that direction, so we set the horizontal // move distance to zero and then move the princess // back to where she was. myPrincess.move(-horizontal * MOVE_LENGTH, 0); horizontal = 0; } else { // if she doesn't hit anything then the move request // succeeds, but we still move her back to the // earlier position because this was just the checking // phase. myPrincess.move(-horizontal * MOVE_LENGTH, 0); horizontal *= MOVE_LENGTH; } return (horizontal); } /** * Internal to requestMove. Calculates the vertical change in the pla yer's * position if jumping or falling. this method should only be called if the * player is currently jumping or falling. * * @return the vertical distance that the player should move this tur n. * (negative moves up, positive moves down) */ private int jumpOrFall(int horizontal) { // by default we do not move vertically

int vertical = 0; // The speed of rise or descent is computed using // the int myIsJumping. Since we are in a jump or // fall, we advance the jump by one (which simulates // the downward pull of gravity by slowing the rise // or accellerating the fall) unless the player is // already falling at maximum speed. (a maximum // free fall speed is necessary because otherwise // it is possible for the player to fall right through // the bottom of the maze...) if (myIsJumping <= MAX_FREE_FALL) { myIsJumping++; } if (myIsJumping < 0) { // if myIsJumping is negative, that means that // the princess is rising. We calculate the // number of pixels to go up by raising 2 to // the power myIsJumping (absolute value). // note that we make the result negative because // the up and down coordinates in Java are the // reverse of the vertical coordinates we learned // in math class: as you go up, the coordinate // values go down, and as you go down the screen, // the coordinate numbers go up. vertical = -(2 << (-myIsJumping)); } else { // if myIsJumping is positive, the princess is falling. // we calculate the distance to fall by raising two // to the power of the absolute value of myIsJumping. vertical = (2 << (myIsJumping)); } // now we temporarily move the princess the desired // vertical distance (with the corresponding horizontal // distance also thrown in), and see if she hits anything: myPrincess.move(horizontal, vertical); if (checkCollision()) { // here we're in the case where she did hit something. // we move her back into position and then see what // to do about it. myPrincess.move(-horizontal, -vertical); if (vertical > 0) { // in this case the player is falling. // so we need to determine precisely how // far she can fall before she hit the bottom vertical = 0; // we temporarily move her the desired horizontal // distance while calculating the corresponding // vertical distance. myPrincess.move(horizontal, 0); while (!checkCollision()) { vertical++; myPrincess.move(0, 1); } // now that we've calculated how far she can fall, // we move her back to her earlier position myPrincess.move(-horizontal, -vertical); // we subtract 1 pixel from the distance calculated

// because once she has actually collided with the // floor, she's gone one pixel too far... vertical--; // now that she's hit the floor, she's not jumping // anymore. myIsJumping = NO_JUMP; } else { // in this case we're going up, so she // must have hit her head. // This next if is checking for a special // case where there's room to jump up exactly // one square. In that case we increase the // value of myIsJumping in order to make the // princess not rise as high. The details // of the calculation in this case were found // through trial and error: if (myIsJumping == NO_JUMP + 2) { myIsJumping++; vertical = -(2 << (-myIsJumping)); // now we see if the special shortened jump // still makes her hit her head: // (as usual, temporarily move her to test // for collisions) myPrincess.move(horizontal, vertical); if (checkCollision()) { // if she still hits her head even // with this special shortened jump, // then she was not meant to jump... myPrincess.move(-horizontal, -vertical); vertical = 0; myIsJumping = NO_JUMP; } else { // now that we've chhecked for collisions, // we move the player back to her earlier // position: myPrincess.move(-horizontal, -vertical); } } else { // if she hit her head, then she should not // jump up. vertical = 0; myIsJumping = NO_JUMP; } } } else { // since she didn't hit anything when we moved // her, then all we have to do is move her back. myPrincess.move(-horizontal, -vertical); } return (vertical); } /** * Internal to requestMove. Once the moves have been determined, actu ally * perform the move. */

private void move(int horizontal, int vertical) { // repaint only if we actually change something: if ((horizontal != 0) || (vertical != 0)) { myModifiedSinceLastPaint = true; } // if the princess is moving left or right, we set // her image to be facing the right direction: if (horizontal > 0) { myPrincess.setTransform(Sprite.TRANS_NONE); } else if (horizontal < 0) { myPrincess.setTransform(Sprite.TRANS_MIRROR); } // if she's jumping or falling, we set the image to // the frame where the skirt is inflated: if (vertical != 0) { myPrincess.setFrame(0); // if she's just running, we alternate between the // two frames: } else if (horizontal != 0) { if (myPrincess.getFrame() == 1) { myPrincess.setFrame(0); } else { myPrincess.setFrame(1); } } // move the position of the view window so that // the player stays in the center: myViewWindowX += horizontal; myViewWindowY += vertical; // after all that work, we finally move the // princess for real!!! myPrincess.move(horizontal, vertical); } //------------------------------------------------------// sprite interactions /** * Drops the currently held key and picks up another. */ void putDownPickUp() { // we do not want to allow the player to put // down the key in the air, so we verify that // we're not jumping or falling first: if ((myIsJumping == NO_JUMP) && (myPrincess.getY() % SQUARE_WIDTH = = 0)) { // since we're picking something up or putting // something down, the display changes and needs // to be repainted: setNeedsRepaint(); // if the thing we're picking up is the crown, // we're done, the player has won: if (myPrincess.collidesWith(myCrown, true)) { myCanvas.setGameOver(); return; } // keep track of the key we're putting down in

// order to place it correctly: DoorKey oldHeld = myHeldKey; myHeldKey = null; // if the princess is on top of another key, // that one becomes the held key and is hence // made invisible: for (int i = 0; i < myKeys.length; i++) { // we check myHeldKey for null because we don't // want to accidentally pick up two keys. if ((myPrincess.collidesWith(myKeys[i], true)) && (myHeldKey == null)) { myHeldKey = myKeys[i]; myHeldKey.setVisible(false); } } if (oldHeld != null) { // place the key we're putting down in the Princess's // current position and make it visible: oldHeld.setPosition(myPrincess.getX(), myPrincess.getY()); oldHeld.setVisible(true); } } } /** * Checks of the player hits a stone wall or a door. */ boolean checkCollision() { boolean retVal = false; // the "true" arg meand to check for a pixel-level // collision (so merely an overlap in image // squares does not register as a collision) if (myPrincess.collidesWith(myBackground, true)) { retVal = true; } else { // Note: it is not necessary to synchronize // this block because the thread that calls this // method is the same as the one that puts down the // keys, so there's no danger of the key being put down // between the moment we check for the key and // the moment we open the door: for (int i = 0; i < myDoors.length; i++) { // if she's holding the right key, then open the door // otherwise bounce off if (myPrincess.collidesWith(myDoors[i], true)) { if ((myHeldKey != null) && (myDoors[i].getColor() == myHeldKey.getColor())) { setNeedsRepaint(); myDoors[i].setVisible(false); } else { // if she's not holding the right key, then // she has collided with the door just the same // as if she had collided with a wall: retVal = true; } } }

} return (retVal); } } /** * This class is the display of the game. * * @author Carol Hamer */ class DungeonCanvas extends GameCanvas { //--------------------------------------------------------// dimension fields // (constant after initialization) /** * the height of the black region below the play area. */ static int TIMER_HEIGHT = 32; /** * the top corner x coordinate according to this object's coordinate * system:. */ static final int CORNER_X = 0; /** * the top corner y coordinate according to this object's coordinate * system:. */ static final int CORNER_Y = 0; /** * the width of the portion of the screen that this canvas can use. */ static int DISP_WIDTH; /** * the height of the portion of the screen that this canvas can use. */ static int DISP_HEIGHT; /** * the height of the font used for this game. */ static int FONT_HEIGHT; /** * the font used for this game. */ static Font FONT; /** * color constant

*/ public static final int BLACK = 0; /** * color constant */ public static final int WHITE = 0xffffff; //--------------------------------------------------------// game object fields /** * a handle to the display. */ private Display myDisplay; /** * a handle to the MIDlet object (to keep track of buttons). */ private Dungeon myDungeon; /** * the LayerManager that handles the game graphics. */ private DungeonManager myManager; /** * whether or not the game has ended. */ private static boolean myGameOver; /** * The number of ticks on the clock the last time the time display wa s * updated. This is saved to determine if the time string needs to be * recomputed. */ private int myOldGameTicks = 0; /** * the number of game ticks that have passed since the beginning of t he * game. */ private int myGameTicks = myOldGameTicks; /** * we save the time string to avoid recreating it unnecessarily. */ private static String myInitialString = "0:00"; /** * we save the time string to avoid recreating it unnecessarily. */ private String myTimeString = myInitialString; //-----------------------------------------------------

//

gets/sets

/** * This is called when the game ends. */ void setGameOver() { myGameOver = true; myDungeon.pauseApp(); } /** * Find out if the game has ended. */ static boolean getGameOver() { return (myGameOver); } /** * Tell the layer manager that it needs to repaint. */ public void setNeedsRepaint() { myManager.setNeedsRepaint(); } //----------------------------------------------------// initialization and game state changes /** * Constructor sets the data, performs dimension calculations, and cr eates * the graphical objects. */ public DungeonCanvas(Dungeon midlet) throws Exception { super(false); myDisplay = Display.getDisplay(midlet); myDungeon = midlet; // calculate the dimensions DISP_WIDTH = getWidth(); DISP_HEIGHT = getHeight(); if ((!myDisplay.isColor()) || (myDisplay.numColors() < 256)) { throw (new Exception("game requires full-color screen")); } if ((DISP_WIDTH < 150) || (DISP_HEIGHT < 170)) { throw (new Exception("Screen too small")); } if ((DISP_WIDTH > 250) || (DISP_HEIGHT > 250)) { throw (new Exception("Screen too large")); } // since the time is painted in white on black, // it shows up better if the font is bold: FONT = Font .getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_MEDIUM); // calculate the height of the black region that the // timer is painted on: FONT_HEIGHT = FONT.getHeight(); TIMER_HEIGHT = FONT_HEIGHT + 8; // create the LayerManager (where all of the interesting

// graphics go!) and give it the dimensions of the // region it is supposed to paint: if (myManager == null) { myManager = new DungeonManager(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT - TIMER_HEIGHT, this); } } /** * This is called as soon as the application begins. */ void start() { myGameOver = false; myDisplay.setCurrent(this); setNeedsRepaint(); } /** * sets all variables back to their initial positions. */ void reset() throws Exception { // most of the variables that need to be reset // are held by the LayerManager: myManager.reset(); myGameOver = false; setNeedsRepaint(); } /** * sets all variables back to the positions from a previously saved g ame. */ void revertToSaved() throws Exception { // most of the variables that need to be reset // are held by the LayerManager, so we // prompt the LayerManager to get the // saved data: myGameTicks = myManager.revertToSaved(); myGameOver = false; myOldGameTicks = myGameTicks; myTimeString = formatTime(); setNeedsRepaint(); } /** * save the current game in progress. */ void saveGame() throws Exception { myManager.saveGame(myGameTicks); } /** * clears the key states. */ void flushKeys() { getKeyStates(); }

/** * If the game is hidden by another app (or a menu) ignore it since n ot much * happens in this game when the user is not actively interacting wit h it. * (we could pause the timer, but it's not important enough to bother with * when the user is just pulling up a menu for a few seconds) */ protected void hideNotify() { } /** * When it comes back into view, just make sure the manager knows tha t it * needs to repaint. */ protected void showNotify() { setNeedsRepaint(); } //------------------------------------------------------// graphics methods /** * paint the game graphics on the screen. */ public void paint(Graphics g) { // color the bottom segment of the screen black g.setColor(BLACK); g.fillRect(CORNER_X, CORNER_Y + DISP_HEIGHT TIMER_HEIGHT, DISP_WIDTH, TIMER_HEIGHT); // paint the LayerManager (which paints // all of the interesting graphics): try { myManager.paint(g); } catch (Exception e) { myDungeon.errorMsg(e); } // draw the time g.setColor(WHITE); g.setFont(FONT); g.drawString("Time: " + formatTime(), DISP_WIDTH / 2, CORNER_Y + DISP_HEIGHT - 4, g.BOTTOM | g.HCENTER); // write "Dungeon Completed" when the user finishes a board: if (myGameOver) { myDungeon.setNewCommand(); // clear the top region: g.setColor(WHITE); g.fillRect(CORNER_X, CORNER_Y, DISP_WIDTH, FONT_HEIGHT * 2 + 1); int goWidth = FONT.stringWidth("Dungeon Completed"); g.setColor(BLACK); g.setFont(FONT); g.drawString("Dungeon Completed", (DISP_WIDTH - goWidth) / 2, CORNER_Y + FONT_HEIGHT, g.TOP | g.LEFT);

} } /** * a simple utility to make the number of ticks look like a time... */ public String formatTime() { if ((myGameTicks / 16) != myOldGameTicks) { myTimeString = ""; myOldGameTicks = (myGameTicks / 16) + 1; int smallPart = myOldGameTicks % 60; int bigPart = myOldGameTicks / 60; myTimeString += bigPart + ":"; if (smallPart / 10 < 1) { myTimeString += "0"; } myTimeString += smallPart; } return (myTimeString); } //------------------------------------------------------// game movements /** * update the display. */ void updateScreen() { myGameTicks++; // paint the display try { paint(getGraphics()); flushGraphics(CORNER_X, CORNER_Y, DISP_WIDTH, DISP_HEIGHT); } catch (Exception e) { myDungeon.errorMsg(e); } } /** * Respond to keystrokes. */ public void checkKeys() { if (!myGameOver) { int vertical = 0; int horizontal = 0; // determine which moves the user would like to make: int keyState = getKeyStates(); if ((keyState & LEFT_PRESSED) != 0) { horizontal = -1; } if ((keyState & RIGHT_PRESSED) != 0) { horizontal = 1; } if ((keyState & UP_PRESSED) != 0) { vertical = -1; } if ((keyState & DOWN_PRESSED) != 0) {

// if the user presses the down key, // we put down or pick up a key object // or pick up the crown: myManager.putDownPickUp(); } // tell the manager to move the player // accordingly if possible: myManager.requestMove(horizontal, vertical); } } }

(31) Game Maze

/* Title: J2ME Games With MIDP2 Authors: Carol Hamer Publisher: Apress ISBN: 1590593820 */

import java.util.Random; import java.util.Vector; import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * This is the main class of the maze game.

* * @author Carol Hamer */ public class Maze extends MIDlet implements CommandListener { //---------------------------------------------------------------// game object fields /** * The canvas that the maze is drawn on. */ private MazeCanvas myCanvas; /** * The screen that allows the user to alter the size parameters * of the maze. */ private SelectScreen mySelectScreen; //---------------------------------------------------------------// command fields /** * The button to exit the game. */ private Command myExitCommand = new Command("Exit", Command.EXIT, 99) ; /** * The command to create a new maze. (This command may appear in a m enu) */ private Command myNewCommand = new Command("New Maze", Command.SCREEN , 1); /** * The command to dismiss an alert error message. In MIDP 2.0 * an Alert set to Alert.FOREVER automatically has a default * dismiss command. This program does not use it in order to * allow backwards com */ private Command myAlertDoneCommand = new Command("Done", Command.EXIT , 1); /** * The command to go to the screen that allows the user * to alter the size parameters. (This command may appear in a menu) */ private Command myPrefsCommand = new Command("Size Preferences", Command.SCREEN, 1); //---------------------------------------------------------------// initialization /** * Initialize the canvas and the commands. */

public Maze() { try { myCanvas = new MazeCanvas(Display.getDisplay(this)); myCanvas.addCommand(myExitCommand); myCanvas.addCommand(myNewCommand); myCanvas.addCommand(myPrefsCommand); myCanvas.setCommandListener(this); } catch(Exception e) { // if there's an error during creation, display it as an alert. Alert errorAlert = new Alert("error", e.getMessage(), null, AlertType.ERROR); errorAlert.setCommandListener(this); errorAlert.setTimeout(Alert.FOREVER); errorAlert.addCommand(myAlertDoneCommand); Display.getDisplay(this).setCurrent(errorAlert); } } //---------------------------------------------------------------// implementation of MIDlet /** * Start the application. */ public void startApp() throws MIDletStateChangeException { if(myCanvas != null) { myCanvas.start(); } } /** * Clean up. */ public void destroyApp(boolean unconditional) throws MIDletStateChangeException { myCanvas = null; System.gc(); } /** * Does nothing since this program occupies no shared resources * and little memory. */ public void pauseApp() { } //---------------------------------------------------------------// implementation of CommandListener /* * Respond to a command issued on the Canvas. * (reset, exit, or change size prefs). */ public void commandAction(Command c, Displayable s) { if(c == myNewCommand) { myCanvas.newMaze(); } else if(c == myAlertDoneCommand) {

try { destroyApp(false); notifyDestroyed(); } catch (MIDletStateChangeException ex) { } } else if(c == myPrefsCommand) { if(mySelectScreen == null) { mySelectScreen = new SelectScreen(myCanvas); } Display.getDisplay(this).setCurrent(mySelectScreen); } else if(c == myExitCommand) { try { destroyApp(false); notifyDestroyed(); } catch (MIDletStateChangeException ex) { } } } }

/** * This class is the display of the game. * * @author Carol Hamer */ class MazeCanvas extends javax.microedition.lcdui.Canvas { //--------------------------------------------------------// static fields /** * color constant */ public static final int BLACK = 0; /** * color constant */ public static final int WHITE = 0xffffff; //--------------------------------------------------------// instance fields /** * a handle to the display. */ private Display myDisplay; /** * The data object that describes the maze configuration. */ private Grid myGrid; /**

* Whether or not the currently displayed maze has * been completed. */ private boolean myGameOver = false; /** * maze dimension: the width of the maze walls. */ private int mySquareSize; /** * maze dimension: the maximum width possible for the maze walls. */ private int myMaxSquareSize; /** * maze dimension: the minimum width possible for the maze walls. */ private int myMinSquareSize; /** * top corner of the display: x-coordiate */ private int myStartX = 0; /** * top corner of the display: y-coordinate */ private int myStartY = 0; /** * how many rows the display is divided into. */ private int myGridHeight; /** * how many columns the display is divided into. */ private int myGridWidth; /** * the maximum number columns the display can be divided into. */ private int myMaxGridWidth; /** * the minimum number columns the display can be divided into. */ private int myMinGridWidth; /** * previous location of the player in the maze: x-coordiate * (in terms of the coordinates of the maze grid, NOT in terms * of the coordinate system of the Canvas.) */ private int myOldX = 1;

/** * previous location of the player in the maze: y-coordinate * (in terms of the coordinates of the maze grid, NOT in terms * of the coordinate system of the Canvas.) */ private int myOldY = 1; /** * current location of the player in the maze: x-coordiate * (in terms of the coordinates of the maze grid, NOT in terms * of the coordinate system of the Canvas.) */ private int myPlayerX = 1; /** * current location of the player in the maze: y-coordinate * (in terms of the coordinates of the maze grid, NOT in terms * of the coordinate system of the Canvas.) */ private int myPlayerY = 1; //----------------------------------------------------// gets / sets /** * Changes the width of the maze walls and calculates how * this change affects the number of rows and columns * the maze can have. * @return the number of columns now that the the * width of the columns has been updated. */ int setColWidth(int colWidth) { if(colWidth < 2) { mySquareSize = 2; } else { mySquareSize = colWidth; } myGridWidth = getWidth() / mySquareSize; if(myGridWidth % 2 == 0) { myGridWidth -= 1; } myGridHeight = getHeight() / mySquareSize; if(myGridHeight % 2 == 0) { myGridHeight -= 1; } myGrid = null; return(myGridWidth); } /** * @return the minimum width possible for the maze walls. */ int getMinColWidth() { return(myMinSquareSize); } /**

* @return the maximum width possible for the maze walls. */ int getMaxColWidth() { return(myMaxSquareSize); } /** * @return the maximum number of columns the display can be divided i nto. */ int getMaxNumCols() { return(myMaxGridWidth); } /** * @return the width of the maze walls. */ int getColWidth() { return(mySquareSize); } /** * @return the number of maze columns the display is divided into. */ int getNumCols() { return(myGridWidth); } //----------------------------------------------------// initialization and game state changes /** * Constructor performs size calculations. * @throws Exception if the display size is too * small to make a maze. */ public MazeCanvas(Display d) throws Exception { myDisplay = d; // a few calculations to make the right maze // for the current display. int width = getWidth(); int height = getHeight(); // tests indicate that 5 is a good default square size, // but the user can change it... mySquareSize = 5; myMinSquareSize = 3; myMaxGridWidth = width / myMinSquareSize; if(myMaxGridWidth % 2 == 0) { myMaxGridWidth -= 1; } myGridWidth = width / mySquareSize; if(myGridWidth % 2 == 0) { myGridWidth -= 1; } myGridHeight = height / mySquareSize; if(myGridHeight % 2 == 0) { myGridHeight -= 1;

} myMinGridWidth = 15; myMaxSquareSize = width / myMinGridWidth; if(myMaxSquareSize > height / myMinGridWidth) { myMaxSquareSize = height / myMinGridWidth; } // if the display is too small to make a reasonable maze, // then we throw an Exception if(myMaxSquareSize < mySquareSize) { throw(new Exception("Display too small")); } } /** * This is called as soon as the application begins. */ void start() { myDisplay.setCurrent(this); repaint(); } /** * discard the current maze and draw a new one. */ void newMaze() { myGameOver = false; // throw away the current maze. myGrid = null; // set the player back to the beginning of the maze. myPlayerX = 1; myPlayerY = 1; myOldX = 1; myOldY = 1; myDisplay.setCurrent(this); // paint the new maze repaint(); } //------------------------------------------------------// graphics methods /** * Create and display a maze if necessary, otherwise just * move the player. Since the motion in this game is * very simple, it is not necessary to repaint the whole * maze each time, just the player + erase the square * that the player just left.. */ protected void paint(Graphics g) { // If there is no current maze, create one and draw it. if(myGrid == null) { int width = getWidth(); int height = getHeight(); // create the underlying data of the maze. myGrid = new Grid(myGridWidth, myGridHeight); // draw the maze: // loop through the grid data and color each square the

// right color for(int i = 0; i < myGridWidth; i++) { for(int j = 0; j < myGridHeight; j++) { if(myGrid.mySquares[i][j] == 0) { g.setColor(BLACK); } else { g.setColor(WHITE); } // fill the square with the appropriate color g.fillRect(myStartX + (i*mySquareSize), myStartY + (j*mySquareSize), mySquareSize, mySquareSize); } } // fill the extra space outside of the maze g.setColor(BLACK); g.fillRect(myStartX + ((myGridWidth-1) * mySquareSize), myStartY, width, height); // erase the exit path: g.setColor(WHITE); g.fillRect(myStartX + ((myGridWidth-1) * mySquareSize), myStartY + ((myGridHeight-2) * mySquareSize), width, height); // fill the extra space outside of the maze g.setColor(BLACK); g.fillRect(myStartX, myStartY + ((myGridHeight-1) * mySquareSize), width, height); } // draw the player (red): g.setColor(255, 0, 0); g.fillRoundRect(myStartX + (mySquareSize)*myPlayerX, myStartY + (mySquareSize)*myPlayerY, mySquareSize, mySquareSize, mySquareSize, mySquareSize); // erase the previous location if((myOldX != myPlayerX) || (myOldY != myPlayerY)) { g.setColor(WHITE); g.fillRect(myStartX + (mySquareSize)*myOldX, myStartY + (mySquareSize)*myOldY, mySquareSize, mySquareSize); } // if the player has reached the end of the maze, // we display the end message. if(myGameOver) { // perform some calculations to place the text correctly: int width = getWidth(); int height = getHeight(); Font font = g.getFont(); int fontHeight = font.getHeight(); int fontWidth = font.stringWidth("Maze Completed"); g.setColor(WHITE); g.fillRect((width - fontWidth)/2, (height - fontHeight)/2, fontWidth + 2, fontHeight); // write in red g.setColor(255, 0, 0); g.setFont(font); g.drawString("Maze Completed", (width - fontWidth)/2, (height - fontHeight)/2,

g.TOP|g.LEFT); } } /** * Move the player. */ public void keyPressed(int keyCode) { if(! myGameOver) { int action = getGameAction(keyCode); switch (action) { case LEFT: if((myGrid.mySquares[myPlayerX-1][myPlayerY] == 1) && (myPlayerX != 1)) { myOldX = myPlayerX; myOldY = myPlayerY; myPlayerX -= 2; repaint(); } break; case RIGHT: if(myGrid.mySquares[myPlayerX+1][myPlayerY] == 1) { myOldX = myPlayerX; myOldY = myPlayerY; myPlayerX += 2; repaint(); } else if((myPlayerX == myGrid.mySquares.length - 2) && (myPlayerY == myGrid.mySquares[0].length - 2)) { myOldX = myPlayerX; myOldY = myPlayerY; myPlayerX += 2; myGameOver = true; repaint(); } break; case UP: if(myGrid.mySquares[myPlayerX][myPlayerY-1] == 1) { myOldX = myPlayerX; myOldY = myPlayerY; myPlayerY -= 2; repaint(); } break; case DOWN: if(myGrid.mySquares[myPlayerX][myPlayerY+1] == 1) { myOldX = myPlayerX; myOldY = myPlayerY; myPlayerY += 2; repaint(); } break; } } } }

/** * This is the screen that allows the user to modify the * width of the maze walls.. * * @author Carol Hamer */ class SelectScreen extends Form implements ItemStateListener, CommandListener { //---------------------------------------------------------------// fields /** * The "Done" button to exit this screen and return to the maze. */ private Command myExitCommand = new Command("Done", Command.EXIT, 1); /** * The gague that modifies the width of the maze walls. */ private Gauge myWidthGauge; /** * The gague that displays the number of columns of the maze. */ private Gauge myColumnsGauge; /** * A handle to the main game canvas. */ private MazeCanvas myCanvas; //---------------------------------------------------------------// initialization /** * Create the gagues and place them on the screen. */ public SelectScreen(MazeCanvas canvas) { super("Size Preferences"); addCommand(myExitCommand); setCommandListener(this); myCanvas = canvas; setItemStateListener(this); myWidthGauge = new Gauge("Column Width", true, myCanvas.getMaxColWidth(), myCanvas.getColWidth()); myColumnsGauge = new Gauge("Number of Columns", false, myCanvas.getMaxNumCols(), myCanvas.getNumCols()); // Warning: the setLayout method does not exist in // MIDP 1.4. If there is any chance that a target // device will be using MIDP 1.4, comment out the // following two lines: //myWidthGauge.setLayout(Item.LAYOUT_CENTER); //myColumnsGauge.setLayout(Item.LAYOUT_CENTER); append(myWidthGauge);

append(myColumnsGauge); } //---------------------------------------------------------------// implementation of ItemStateListener /** * Respond to the user changing the width. */ public void itemStateChanged(Item item) { if(item == myWidthGauge) { int val = myWidthGauge.getValue(); if(val < myCanvas.getMinColWidth()) { myWidthGauge.setValue(myCanvas.getMinColWidth()); } else { int numCols = myCanvas.setColWidth(val); myColumnsGauge.setValue(numCols); } } } //---------------------------------------------------------------// implementation of CommandListener /* * Respond to a command issued on this screen. * (either reset or exit). */ public void commandAction(Command c, Displayable s) { if(c == myExitCommand) { myCanvas.newMaze(); } } }

/** * This class contains the data necessary to draw the maze. * * @author Carol Hamer */ class Grid { /** * Random number generator to create a random maze. */ private Random myRandom = new Random(); /** * data for which squares are filled and which are blank. * 0 = black * 1 = white * values higher than 1 are used during the maze creation * algorithm. * 2 = the square could possibly be appended to the maze this round. * 3 = the square's color is not yet decided, and the square is

* not close enough to be appended to the maze this round. */ int[][] mySquares; //-------------------------------------------------------// maze generation methods /** * Create a new maze. */ public Grid(int width, int height) { mySquares = new int[width][height]; // initialize all of the squares to white except a lattice // framework of black squares. for(int i = 1; i < width - 1; i++) { for(int j = 1; j < height - 1; j++) { if((i % 2 == 1) || (j % 2 == 1)) { mySquares[i][j] = 1; } } } // the entrance to the maze is at (0,1). mySquares[0][1] = 1; createMaze(); } /** * This method randomly generates the maze. */ private void createMaze() { // create an initial framework of black squares. for(int i = 1; i < mySquares.length - 1; i++) { for(int j = 1; j < mySquares[i].length - 1; j++) { if((i + j) % 2 == 1) { mySquares[i][j] = 0; } } } // initialize the squares that can be either black or white // depending on the maze. // first we set the value to 3 which means undecided. for(int i = 1; i < mySquares.length - 1; i+=2) { for(int j = 1; j < mySquares[i].length - 1; j+=2) { mySquares[i][j] = 3; } } // Then those squares that can be selected to be open // (white) paths are given the value of 2. // We randomly select the square where the tree of maze // paths will begin. The maze is generated starting from // this initial square and branches out from here in all // directions to fill the maze grid. Vector possibleSquares = new Vector(mySquares.length * mySquares[0].length); int[] startSquare = new int[2]; startSquare[0] = getRandomInt(mySquares.length / 2)*2 + 1; startSquare[1] = getRandomInt(mySquares[0].length / 2)*2 + 1;

mySquares[startSquare[0]][startSquare[1]] = 2; possibleSquares.addElement(startSquare); // Here we loop to select squares one by one to append to // the maze pathway tree. while(possibleSquares.size() > 0) { // the next square to be joined on is selected randomly. int chosenIndex = getRandomInt(possibleSquares.size()); int[] chosenSquare = (int[])possibleSquares.elementAt(chosenIndex ); // we set the chosen square to white and then // remove it from the list of possibleSquares (i.e. squares // that can possibly be added to the maze), and we link // the new square to the maze. mySquares[chosenSquare[0]][chosenSquare[1]] = 1; possibleSquares.removeElementAt(chosenIndex); link(chosenSquare, possibleSquares); } // now that the maze has been completely generated, we // throw away the objects that were created during the // maze creation algorithm and reclaim the memory. possibleSquares = null; System.gc(); } /** * internal to createMaze. Checks the four squares surrounding * the chosen square. Of those that are already connected to * the maze, one is randomly selected to be joined to the * current square (to attach the current square to the * growing maze). Those squares that were not previously in * a position to be joined to the maze are added to the list * of "possible" squares (that could be chosen to be attached * to the maze in the next round). */ private void link(int[] chosenSquare, Vector possibleSquares) { int linkCount = 0; int i = chosenSquare[0]; int j = chosenSquare[1]; int[] links = new int[8]; if(i >= 3) { if(mySquares[i - 2][j] == 1) { links[2*linkCount] = i - 1; links[2*linkCount + 1] = j; linkCount++; } else if(mySquares[i - 2][j] == 3) { mySquares[i - 2][j] = 2; int[] newSquare = new int[2]; newSquare[0] = i - 2; newSquare[1] = j; possibleSquares.addElement(newSquare); } } if(j + 3 <= mySquares[i].length) { if(mySquares[i][j + 2] == 3) { mySquares[i][j + 2] = 2; int[] newSquare = new int[2]; newSquare[0] = i;

newSquare[1] = j + 2; possibleSquares.addElement(newSquare); } else if(mySquares[i][j + 2] == 1) { links[2*linkCount] = i; links[2*linkCount + 1] = j + 1; linkCount++; } } if(j >= 3) { if(mySquares[i][j - 2] == 3) { mySquares[i][j - 2] = 2; int[] newSquare = new int[2]; newSquare[0] = i; newSquare[1] = j - 2; possibleSquares.addElement(newSquare); } else if(mySquares[i][j - 2] == 1) { links[2*linkCount] = i; links[2*linkCount + 1] = j - 1; linkCount++; } } if(i + 3 <= mySquares.length) { if(mySquares[i + 2][j] == 3) { mySquares[i + 2][j] = 2; int[] newSquare = new int[2]; newSquare[0] = i + 2; newSquare[1] = j; possibleSquares.addElement(newSquare); } else if(mySquares[i + 2][j] == 1) { links[2*linkCount] = i + 1; links[2*linkCount + 1] = j; linkCount++; } } if(linkCount > 0) { int linkChoice = getRandomInt(linkCount); int linkX = links[2*linkChoice]; int linkY = links[2*linkChoice + 1]; mySquares[linkX][linkY] = 1; int[] removeSquare = new int[2]; removeSquare[0] = linkX; removeSquare[1] = linkY; possibleSquares.removeElement(removeSquare); } } /** * a randomization utility. * @param upper the upper bound for the random int. * @return a random non-negative int less than the bound upper. */ public int getRandomInt(int upper) { int retVal = myRandom.nextInt() % upper; if(retVal < 0) { retVal += upper; } return(retVal);

} }

(32) Game Checkers


/* Title: J2ME Games With MIDP2 Authors: Carol Hamer Publisher: Apress ISBN: 1590593820 */ import java.util.Vector; import java.io.*; import javax.microedition.io.*; import javax.microedition.rms.*;

import javax.microedition.midlet.*; import javax.microedition.lcdui.*; /** * This is the main class of the checkers game. * * @author Carol Hamer */ public class Checkers extends MIDlet implements CommandListener { //----------------------------------------------------// game object fields /** * The canvas that the checkerboard is drawn on. */ private CheckersCanvas myCanvas; /** * The class that makes the http connection. */ private Communicator myCommunicator; //----------------------------------------------------// command fields /** * The button to exit the game. */ private Command myExitCommand = new Command("Exit", Command.EXIT, 99) ; //----------------------------------------------------// initialization and game state changes

/** * Initialize the canvas and the commands. */ public Checkers() { try { //create the canvas and set up the commands: myCanvas = new CheckersCanvas(Display.getDisplay(this)); myCanvas.addCommand(myExitCommand); myCanvas.setCommandListener(this); CheckersGame game = myCanvas.getGame(); myCommunicator = new Communicator(this, myCanvas, game); game.setCommunicator(myCommunicator); } catch(Exception e) { // if there's an error during creation, display it as an alert. errorMsg(e); } } //---------------------------------------------------------------// implementation of MIDlet // these methods may be called by the application management // software at any time, so we always check fields for null // before calling methods on them. /** * Start the application. */ public void startApp() throws MIDletStateChangeException { // tell the canvas to set up the game data and paint the // checkerboard. if(myCanvas != null) { myCanvas.start(); } // tell the communicator to start its thread and make a // connection. if(myCommunicator != null) { myCommunicator.start(); } } /** * Throw out the garbage. */ public void destroyApp(boolean unconditional) throws MIDletStateChangeException { // tell the communicator to send the end game // message to the other player and then disconnect: if(myCommunicator != null) { myCommunicator.endGame(); } // throw the larger game objects in the garbage: myCommunicator = null; myCanvas = null; System.gc(); } /**

* Pause the game. * This method merely ends the game because this * version of the Checkers game does not support * re-entering a game that is in play. A possible * improvement to the game would be to allow * a player to diconeect and leave a game and then * later return to it, using some sort of session * token to find the correct game in progress on * the server side. */ public void pauseApp() { try { destroyApp(false); notifyDestroyed(); } catch (MIDletStateChangeException ex) { } } //---------------------------------------------------------------// implementation of CommandListener /* * Respond to a command issued on the Canvas. */ public void commandAction(Command c, Displayable s) { if(c == myExitCommand) { try { destroyApp(false); notifyDestroyed(); } catch (MIDletStateChangeException ex) { } } } //------------------------------------------------------// error methods /** * Converts an exception to a message and displays * the message.. */ void errorMsg(Exception e) { e.printStackTrace(); if(e.getMessage() == null) { errorMsg(e.getClass().getName()); } else { errorMsg(e.getMessage()); } } /** * Displays an error message alert if something goes wrong. */ void errorMsg(String msg) { Alert errorAlert = new Alert("error", msg, null, AlertType.ERROR); errorAlert.setCommandListener(this);

errorAlert.setTimeout(Alert.FOREVER); Display.getDisplay(this).setCurrent(errorAlert); } } /** * This class is the display of the game. * * @author Carol Hamer */ class CheckersCanvas extends Canvas { //--------------------------------------------------------// static fields /** * color constant */ public static final int BLACK = 0; /** * color constant */ public static final int WHITE = 0xffffff; /** * color constant. * (not quite bright red) */ public static final int RED = 0xf96868; /** * color constant */ public static final int GREY = 0xc6c6c6; /** * color constant */ public static final int LT_GREY = 0xe5e3e3; /** * how many rows and columns the display is divided into. */ public static final int GRID_WIDTH = 8; //--------------------------------------------------------// instance fields /** * The black crown to draw on the red pieces.. */ private Image myBlackCrown; /** * The red crown to draw on the black pieces..

*/ private Image myWhiteCrown; /** * a handle to the display. */ private Display myDisplay; /** * a handle to the object that stores the game logic * and game data. */ private CheckersGame myGame; /** * checkers dimension: the width of the squares of the checkerboard. */ private int mySquareSize; /** * checkers dimension: the minimum width possible for the * checkerboard squares. */ private int myMinSquareSize = 15; /** * whether or not we're waiting for another player to join * the game. */ private boolean myIsWaiting; //----------------------------------------------------// gets / sets /** * @return a handle to the class that holds the logic of the * checkers game. */ CheckersGame getGame() { return(myGame); } /** * Display a screen to inform the player that we're * waiting for another player. */ void setWaitScreen(boolean wait) { myIsWaiting = wait; } //----------------------------------------------------// initialization and game state changes /** * Constructor performs size calculations. * @throws Exception if the display size is too * small to make a checkers.

*/ CheckersCanvas(Display d) throws Exception { myDisplay = d; myGame = new CheckersGame(); // a few calculations to make the right checkerboard // for the current display. int width = getWidth(); int height = getHeight(); // get the smaller dimension fo the two possible // screen dimensions in order to determine how // big to make the checkerboard. int screenSquareWidth = height; if(width < height) { screenSquareWidth = width; } mySquareSize = screenSquareWidth / GRID_WIDTH; // if the display is too small to make a reasonable checkerboard, // then we throw an Exception if(mySquareSize < myMinSquareSize) { throw(new Exception("Display too small")); } // initialize the crown images: myBlackCrown = Image.createImage("/blackCrown.png"); myWhiteCrown = Image.createImage("/whiteCrown.png"); } /** * This is called as soon as the application begins. */ void start() { myDisplay.setCurrent(this); // prepare the game data for the first move: myGame.start(); } //------------------------------------------------------// graphics methods /** * Repaint the checkerboard.. */ protected void paint(Graphics g) { int width = getWidth(); int height = getHeight(); g.setColor(WHITE); // clear the board (including the region around // the board, which can get menu stuff and other // garbage painted onto it...) g.fillRect(0, 0, width, height); // If we need to wait for another player to join the // game before we can start, this displays the appropriate // message: if(myIsWaiting) { // perform some calculations to place the text correctly: Font font = g.getFont(); int fontHeight = font.getHeight(); int fontWidth = font.stringWidth("waiting for another player");

g.setColor(WHITE); g.fillRect((width - fontWidth)/2, (height - fontHeight)/2, fontWidth + 2, fontHeight); // write in black g.setColor(BLACK); g.setFont(font); g.drawString("waiting for another player", (width fontWidth)/2, (height - fontHeight)/2, g.TOP|g.LEFT); return; } // now draw the checkerboard: // first the dark squares: byte offset = 0; for(byte i = 0; i < 4; i++) { for(byte j = 0; j < 8; j++) { // the offset is used to handle the fact that in every // other row the dark squares are shifted one place // to the right. if(j % 2 != 0) { offset = 1; } else { offset = 0; } // now if this is a selected square, we draw it lighter: if(myGame.isSelected(i, j)) { g.setColor(LT_GREY); g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize, mySquareSize, mySquareSize); } else { // if it's not selected, we draw it dark grey: g.setColor(GREY); g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize, mySquareSize, mySquareSize); } // now put the pieces in their places: g.setColor(RED); int piece = myGame.getPiece(i, j); int circleOffset = 2; int circleSize = mySquareSize - 2*circleOffset; if(piece < 0) { // color the piece in black g.setColor(BLACK); g.fillRoundRect((2*i + offset)*mySquareSize + circleOffset, j*mySquareSize + circleOffset, circleSize, circleSize, circleSize, circleSize); // if the player is a king, draw a crown on: if(piece < -1) { g.drawImage(myWhiteCrown, (2*i + offset)*mySquareSize + mySquareSize/2, j*mySquareSize + 1 + mySquareSize/2, Graphics.VCENTER|Graphics.HCENTER); } } else if(piece > 0) { // color the piece in red g.fillRoundRect((2*i + offset)*mySquareSize + circleOffset,

j*mySquareSize + circleOffset, circleSize, circleSize, circleSize, circleSize); // if the player is a king, draw a crown on: if(piece > 1) { g.drawImage(myBlackCrown, (2*i + offset)*mySquareSize + mySquareSize/2, j*mySquareSize + 1 + mySquareSize/2, Graphics.VCENTER|Graphics.HCENTER); } } } } // now the blank squares: // actually, this part is probably not necessary... g.setColor(WHITE); for(int i = 0; i < 4; i++) { for(int j = 0; j < 8; j++) { if(j % 2 == 0) { offset = 1; } else { offset = 0; } g.fillRect((2*i + offset)*mySquareSize, j*mySquareSize, mySquareSize, mySquareSize); } } // if the player has reached the end of the game, // we display the end message. if(myGame.getGameOver()) { // perform some calculations to place the text correctly: Font font = g.getFont(); int fontHeight = font.getHeight(); int fontWidth = font.stringWidth("Game Over"); g.setColor(WHITE); g.fillRect((width - fontWidth)/2, (height - fontHeight)/2, fontWidth + 2, fontHeight); // write in black g.setColor(BLACK); g.setFont(font); g.drawString("Game Over", (width - fontWidth)/2, (height - fontHeight)/2, g.TOP|g.LEFT); } } //------------------------------------------------------// handle keystrokes /** * Move the player. */ public void keyPressed(int keyCode) { if(myGame.isMyTurn()) { int action = getGameAction(keyCode); switch (action) { case LEFT: myGame.leftPressed();

break; case RIGHT: myGame.rightPressed(); break; case UP: myGame.upPressed(); break; case DOWN: myGame.deselect(); break; } repaint(); serviceRepaints(); } } } /** * This class contacts a remote server in order to * play a game of checkers against an opponent.. * * @author Carol Hamer */ class Communicator extends Thread { //-------------------------------------------------------// static fields /** * This is the URL to contact. * IMPORTANT: before compiling, the following URL * must be changed to the correct URL of the * machine running the server code. */ public static final String SERVER_URL = "socket://malbec:8007"; /** * The int to signal that the game is to begin. */ public static final byte START_GAME_FLAG = -4; /** * The byte to signal that the game is to end. */ public static final byte END_GAME_FLAG = -3; /** * The byte to signal the end of a turn. */ public static final byte END_TURN_FLAG = -2; //-------------------------------------------------------// game instance fields /**

* The MIDlet subclass, used to set the Display * in the case where an error message needs to be sent.. */ private Checkers myCheckers; /** * The Canvas subclass, used to set the Display * in the case where an error message needs to be sent.. */ private CheckersCanvas myCanvas; /** * The game logic class that we send the opponent's * moves to.. */ private CheckersGame myGame; /** * Whether or not the MIDlet class has requested the * game to end. */ private boolean myShouldStop; //-------------------------------------------------------// data exchange instance fields /** * The data from the local player that is to * be sent to the opponent. */ private byte[] myMove; /** * Whether or not the current turn is done and * should be sent. */ private boolean myTurnIsDone = true; //-------------------------------------------------------// initialization /** * Constructor is used only when the program wants * to spawn a data-fetching thread, not for merely * reading local data with static methods. */ Communicator(Checkers checkers, CheckersCanvas canvas, CheckersGame game) { myCheckers = checkers; myCanvas = canvas; myGame = game; } //-------------------------------------------------------// methods called by CheckersGame to send move // information to the opponent.

/** * Stop the game entirely. Notify the servlet that * the user is exiting the game. */ synchronized void endGame() { myShouldStop = true; if(myGame != null) { myGame.setGameOver(); } notify(); } /** * This is called when the player moves a piece. */ synchronized void move(byte sourceX, byte sourceY, byte destinationX, byte destinationY) { myMove = new byte[4]; myMove[0] = sourceX; myMove[1] = sourceY; myMove[2] = destinationX; myMove[3] = destinationY; myTurnIsDone = false; notify(); } /** * This is called when the local player's turn is over. */ synchronized void endTurn() { myTurnIsDone = true; notify(); } //-------------------------------------------------------// main communication method /** * Makes a connection to the server and sends and receives * information about moves. */ public void run() { DataInputStream dis = null; DataOutputStream dos = null; SocketConnection conn = null; byte[] fourBytes = new byte[4]; try { // tell the user that we're waiting for the other player to join: myCanvas.setWaitScreen(true); myCanvas.repaint(); myCanvas.serviceRepaints(); // now make the connection: conn = (SocketConnection)Connector.open(SERVER_URL); conn.setSocketOption(SocketConnection.KEEPALIVE, 1); dos = conn.openDataOutputStream(); dis = conn.openDataInputStream();

// we read four bytes to make sure the connection works... dis.readFully(fourBytes); if(fourBytes[0] != START_GAME_FLAG) { throw(new Exception("server-side error")); } // On this line it will block waiting for another // player to join the game or make a move: dis.readFully(fourBytes); // if the server sends the start game flag again, // that means that we start with the local player's turn. // Otherwise, we read the other player's first move from the // stream: if(fourBytes[0] != START_GAME_FLAG) { // verify that the other player sent a move // and not just a message ending the game... if(fourBytes[0] == END_GAME_FLAG) { throw(new Exception("other player quit")); } // we move the opponent on the local screen. // then we read from the opponent again, // in case there's a double-jump: while(fourBytes[0] != END_TURN_FLAG) { myGame.moveOpponent(fourBytes); dis.readFully(fourBytes); } } // now signal the local game that the opponent is done // so the board must be updated and the local player // prompted to make a move: myGame.endOpponentTurn(); myCanvas.setWaitScreen(false); myCanvas.repaint(); myCanvas.serviceRepaints(); // begin main game loop: while(! myShouldStop) { // now it's the local player's turn. // wait for the player to move a piece: synchronized(this) { wait(); } // after every wait, we check if the game // ended while we were waiting... if(myShouldStop) { break; } while(! myTurnIsDone) { // send the current move: if(myMove != null) { dos.write(myMove, 0, myMove.length); myMove = null; } // If the player can continue the move with a double // jump, we wait for the player to do it: synchronized(this) { // make sure the turn isn't done before we start waiting // (the end turn notify might accidentally be called // before we start waiting...)

if(! myTurnIsDone) { wait(); } } } // after every wait, we check if the game // ended while we were waiting... if(myShouldStop) { break; } // now we tell the other player the this player's // turn is over: fourBytes[0] = END_TURN_FLAG; dos.write(fourBytes, 0, fourBytes.length); // now that we've sent the move, we wait for a response: dis.readFully(fourBytes); while((fourBytes[0] != END_TURN_FLAG) && (fourBytes[0] != END_GAME_FLAG) && (!myShouldStop)) { // we move the opponent on the local screen. // then we read from the opponent again, // in case there's a double-jump: myGame.moveOpponent(fourBytes); dis.readFully(fourBytes); } // if the other player has left the game, we tell the // local user that the game is over. if((fourBytes[0] == END_GAME_FLAG) || (myShouldStop)) { endGame(); break; } myGame.endOpponentTurn(); myCanvas.repaint(); myCanvas.serviceRepaints(); } // end while loop } catch(Exception e) { // if there's an error, we display its messsage and // end the game. myCheckers.errorMsg(e.getMessage()); } finally { // now we send the information that we're leaving the game, // then close up and delete everything. try { if(dos != null) { dos.write(END_GAME_FLAG); dos.close(); } if(dis != null) { dis.close(); } if(conn != null) { conn.close(); } dis = null; dos = null; conn = null; } catch(Exception e) { // if this throws, at least we made our best effort

// to close everything up.... } } // one last paint job to display the "Game Over" myCanvas.repaint(); myCanvas.serviceRepaints(); } }

/** * This class is a set of simple utility functions that * can be used to convert standard data types to bytes * and back again. It is used especially for data storage, * but also for sending and receiving data. * * @author Carol Hamer */ class DataConverter { //-------------------------------------------------------// utilities to encode small, compactly-stored small ints. /** * Encodes a coordinate pair into a byte. * @param coordPair a pair of integers to be compacted into * a single byte for storage. * WARNING: each of the two values MUST BE * between 0 and 15 (inclusive). This method does not * verify the length of the array (which must be 2!) * nor does it verify that the ints are of the right size. */ public static byte encodeCoords(int[] coordPair) { // get the byte value of the first coordinate: byte retVal = (new Integer(coordPair[0])).byteValue(); // move the first coordinate's value up to the top // half of the storage byte: retVal = (new Integer(retVal << 4)).byteValue(); // store the second coordinate in the lower half // of the byte: retVal += (new Integer(coordPair[1])).byteValue(); return(retVal); } /** * Encodes eight ints into a byte. * This could be easily modified to encode eight booleans. * @param eight an array of at least eight ints. * WARNING: all values must be 0 or 1! This method does * not verify that the values are in the correct range * nor does it verify that the array is long enough. * @param offset the index in the array eight to start * reading data from. (should usually be 0) */ public static byte encode8(int[] eight, int offset) { // get the byte value of the first int:

byte retVal = (new Integer(eight[offset])).byteValue(); // progressively move the data up one bit in the // storage byte and then record the next int in // the lowest spot in the storage byte: for(int i = offset + 1; i < 8 + offset; i++) { retVal = (new Integer(retVal << 1)).byteValue(); retVal += (new Integer(eight[i])).byteValue(); } return(retVal); } //-------------------------------------------------------// utilities to decode small, compactly-stored small ints. /** * Turns a byte into a pair of coordinates. */ public static int[] decodeCoords(byte coordByte) { int[] retArray = new int[2]; // we perform a bitwise and with the value 15 // in order to just get the bits of the lower // half of the byte: retArray[1] = coordByte & 15; // To get the bits of the upper half of the // byte, we perform a shift to move them down: retArray[0] = coordByte >> 4; // bytes in Java are generally assumed to be // signed, but in this coding algorithm we // would like to treat them as unsigned: if(retArray[0] < 0) { retArray[0] += 16; } return(retArray); } /** * Turns a byte into eight ints. */ public static int[] decode8(byte data) { int[] retArray = new int[8]; // The flag allows us to look at each bit individually // to determine if it is 1 or 0. The number 128 // corresponds to the highest bit of a byte, so we // start with that one. int flag = 128; // We use a loop that checks // the data bit by bit by performing a bitwise // and (&) between the data byte and a flag: for(int i = 0; i < 8; i++) { if((flag & data) != 0) { retArray[i] = 1; } else { retArray[i] = 0; } // move the flag down one bit so that we can // check the next bit of data on the next pass // through the loop:

flag = flag >> 1; } return(retArray); }

//-------------------------------------------------------// standard integer interpretation /** * Uses an input stream to convert an array of bytes to an int. */ public static int parseInt(byte[] data) throws IOException { DataInputStream stream = new DataInputStream(new ByteArrayInputStream(data)); int retVal = stream.readInt(); stream.close(); return(retVal); } /** * Uses an output stream to convert an int to four bytes. */ public static byte[] intToFourBytes(int i) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(4); DataOutputStream dos = new DataOutputStream(baos); dos.writeInt(i); baos.close(); dos.close(); byte[] retArray = baos.toByteArray(); return(retArray); } //-------------------------------------------------------// integer interpretation illustrated /** * Java appears to treat a byte as being signed when * returning it as an int--this function converts from * the signed value to the corresponding unsigned value. * This method is used by nostreamParseInt. */ public static int unsign(int signed) { int retVal = signed; if(retVal < 0) { retVal += 256; } return(retVal); } /** * Takes an array of bytes and returns an int. * This version will return the same value as the * method parseInt above. This version is included * in order to illustrate how Java encodes int values * in terms of bytes. * @param data an array of 1, 2, or 4 bytes.

*/ public static int nostreamParseInt(byte[] data) { // byte 0 is the high byte which is assumed // to be signed. As we add the lower bytes // one by one, we unsign them because because // a single byte alone is interpreted as signed, // but in an int only the top byte should be signed. // (note that the high byte is the first one in the array) int retVal = data[0]; for(int i = 1; i < data.length; i++) { retVal = retVal << 8; retVal += unsign(data[i]); } return(retVal); } /** * Takes an arbitrary int and returns * an array of four bytes. * This version will return the same byte array * as the method intToFourBytes above. This version * is included in order to illustrate how Java encodes * int values in terms of bytes. */ public static byte[] nostreamIntToFourBytes(int i) { byte[] fourBytes = new byte[4]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get all four bytes by taking the lowest // byte four times and moving the whole int // down by one byte between each one. // (note that the high byte is the first one in the array) fourBytes[3] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[2] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; fourBytes[0] = (new Integer(i)).byteValue(); return(fourBytes); }

/** * Takes an int between -32768 and 32767 and returns * an array of two bytes. This does not verify that * the argument is of the right size. If the absolute * value of i is too high, it will not be encoded * correctly. */ public static byte[] nostreamIntToTwoBytes(int i) { byte[] twoBytes = new byte[2]; // when you take the byte value of an int, it // only gives you the lowest byte. So we // get the lower two bytes by taking the lowest // byte twice and moving the whole int // down by one byte between each one.

twoBytes[1] = (new Integer(i)).byteValue(); i = i >> 8; twoBytes[0] = (new Integer(i)).byteValue(); return(twoBytes); } }

/** * This class takes care of the underlying logic and data of * the checkers game being played. That includes where * all of the pieces are on the board and where it is okay * for them to move to. * * @author Carol Hamer */ class CheckersGame { //------------------------------------------------------// static fields /** * The length of the checkerboard in the x-direction. */ public static final byte X_LENGTH = 4; /** * The length of the checkerboard in the y-direction. */ public static final byte Y_LENGTH = 8; //------------------------------------------------------// instance fields /** * a handle to the communications class that exchanges * data with the server. */ private Communicator myCommunicator; /** * This array represents the black squares of the * checkerboard. The two dimensions of the array * represent the two dimensions of the checkerboard. * The value represents what type of piece is on * the square. * 0 = empty * 1 = local player's piece * 2 = local player's king * -1 = remote player's piece * -2 = remote player's king */ private byte[][] myGrid; /**

* If the user has currently selected a piece to move, * this is its X grid coordinate. (-1 if none selected) */ private byte mySelectedX = -1; /** * If the user has currently selected a piece to move, * this is its Y grid coordinate.(-1 if none selected) */ private byte mySelectedY = -1; /** * If the user has currently selected a possible * destination square for a move, this is its X coordinate.. * (-1 if none selected) */ private byte myDestinationX = -1; /** * If the user has currently selected a possible * destination square for a move, this is its Y coordinate.. * (-1 if none selected) */ private byte myDestinationY = -1; /** * This Vector contains the coordinates of all of the * squares that the player could currently move to. */ private Vector myPossibleMoves = new Vector(4); /** * Whether or not the currently displayed checkers has * been completed. */ private boolean myGameOver = false; /** * Whether or not it is currently this player's turn. */ private boolean myTurn = false; /** * This is true if the player has just jumped and can * jump again. */ private boolean myIsJumping = false; //------------------------------------------------------// get/set data /** * get the piece on the given grid square. */ byte getPiece(byte x, byte y) { return(myGrid[x][y]); }

/** * This is callsed by CheckersCanvas to determine if * the square is currently selected (as containing * a piece to move or a destination square). */ boolean isSelected(byte x, byte y) { boolean retVal = false; if((x == mySelectedX) && (y == mySelectedY)) { retVal = true; } else if((x == myDestinationX) && (y == myDestinationY)) { retVal = true; } return(retVal); } /** * This tells whether or not the keystrokes should currently * be taken into account. */ boolean isMyTurn() { boolean retVal = false; if((!myGameOver) && ((myTurn) || (myIsJumping))) { retVal = true; } return(retVal); } /** * This tells whether or not the game has ended. */ boolean getGameOver() { boolean retVal = false; if(myGameOver) { retVal = true; } return(retVal); } /** * tell the CheckersGame that the other player has ended the game. */ void setGameOver() { myGameOver = true; } /** * set the communicator object. */ void setCommunicator(Communicator comm) { myCommunicator = comm; } //------------------------------------------------------// initialization /**

* Constructor puts the pieces in their initial positions: */ CheckersGame() { myGrid = new byte[X_LENGTH][]; for(byte i = 0; i < myGrid.length; i++) { myGrid[i] = new byte[Y_LENGTH]; for(byte j = 0; j < myGrid[i].length; j++) { if(j < 3) { // fill the top of the board with remote players myGrid[i][j] = -1; } else if(j > 4) { // fill the bottom of the board with local players myGrid[i][j] = 1; } } } } /** * This is called just before the player makes the * first move. */ void start() { mySelectedX = 0; mySelectedY = 5; myTurn = true; getMoves(mySelectedX, mySelectedY, myPossibleMoves, false); } //------------------------------------------------------// move the opponent // to be called by Communicator /** * This is called when the opponent wants to move * its piece. * @param moveData an array of four bytes: * moveData[0] = opponent's initial X coordinate * moveData[1] = opponent's initial Y coordinate * moveData[2] = opponent's destination X coordinate * moveData[3] = opponent's destination Y coordinate */ void moveOpponent(byte[] moveData) { // since both players appear on their own screens // as the red side (bottom of the screen), we need // to invert the opponent's move: moveData[0] = (new Integer(X_LENGTH - moveData[0] 1)).byteValue(); moveData[2] = (new Integer(X_LENGTH - moveData[2] 1)).byteValue(); moveData[1] = (new Integer(Y_LENGTH - moveData[1] 1)).byteValue(); moveData[3] = (new Integer(Y_LENGTH - moveData[3] 1)).byteValue(); myGrid[moveData[2]][moveData[3]] = myGrid[moveData[0]][moveData[1]]; myGrid[moveData[0]][moveData[1]] = 0;

// deal with an opponent's jump: if((moveData[1] - moveData[3] > 1) || (moveData[3] - moveData[1] > 1)) { int jumpedY = (moveData[1] + moveData[3])/2; int jumpedX = moveData[0]; int parity = moveData[1] % 2; if((parity > 0) && (moveData[2] > moveData[0])) { jumpedX++; } else if((parity == 0) && (moveData[0] > moveData[2])) { jumpedX--; } myGrid[jumpedX][jumpedY] = 0; } // if the opponent reaches the far side, // make him a king: if(moveData[3] == Y_LENGTH - 1) { myGrid[moveData[2]][moveData[3]] = -2; } } /** * This is called when the opponent's turn is over. * Note that the turn doesn't automatically end after * the opponent moves because the opponent may make * a double or triple jump. */ void endOpponentTurn() { myTurn = true; // Now begin the local player's turn: // First select the first local piece that can be // moved. (rightPressed will select an appropriate // piece or end the game if the local player has // no possible moves to make) mySelectedX = 0; mySelectedY = 0; myDestinationX = -1; myDestinationY = -1; rightPressed(); // the local player's thread has been waiting // for the opponent's turn to end. synchronized(this) { notify(); } } //------------------------------------------------------// handle keystrokes // to be called by CheckersCanvas /** * if the left button is pressed, this method takes * the correct course of action depending on the situation. */ void leftPressed() { // in the first case the user has not yet selected a // piece to move: if(myDestinationX == -1) {

// find the next possible piece (to the left) // that can move: selectPrevious(); // if selectPrevious fails to fill myPossibleMoves, that // means that the local player cannot move, so the game // is over: if(myPossibleMoves.size() == 0) { myCommunicator.endGame(); } } else { // if the user has already selected a piece to move, // we give the options of where the piece can move to: for(byte i = 0; i < myPossibleMoves.size(); i++) { byte[] coordinates = (byte[])myPossibleMoves.elementAt(i); if((coordinates[0] == myDestinationX) && (coordinates[1] == myDestinationY)) { i++; i = (new Integer(i % myPossibleMoves.size())).byteValue(); coordinates = (byte[])myPossibleMoves.elementAt(i); myDestinationX = coordinates[0]; myDestinationY = coordinates[1]; break; } } } } /** * if the left button is pressed, this method takes * the correct course of action depending on the situation. */ void rightPressed() { // in the first case the user has not yet selected a // piece to move: if(myDestinationX == -1) { // find the next possible piece that can // move: selectNext(); // if selectNext fails to fill myPossibleMoves, that // means that the local player cannot move, so the game // is over: if(myPossibleMoves.size() == 0) { myCommunicator.endGame(); } } else { // if the user has already selected a piece to move, // we give the options of where the piece can move to: for(byte i = 0; i < myPossibleMoves.size(); i++) { byte[] coordinates = (byte[])myPossibleMoves.elementAt(i); if((coordinates[0] == myDestinationX) && (coordinates[1] == myDestinationY)) { i++; i = (new Integer(i % myPossibleMoves.size())).byteValue(); coordinates = (byte[])myPossibleMoves.elementAt(i); myDestinationX = coordinates[0]; myDestinationY = coordinates[1]; break;

} } } } /** * If no piece is selected, we select one. If a piece * is selected, we move it. */ void upPressed() { // in the first case the user has not yet selected a // piece to move: if(myDestinationX == -1) { fixSelection(); } else { // if the source square and destination square // have been chosen, we move the piece: move(); } } /** * If the user decided not to move the selected piece * (and instead wants to select again), this undoes * the selection. This corresponds to pressing the * DOWN key. */ void deselect() { // if the player has just completed a jump and // could possibly jump again but decides not to // (i.e. deselects), then the turn ends: if(myIsJumping) { mySelectedX = -1; mySelectedY = -1; myDestinationX = -1; myDestinationY = -1; myIsJumping = false; myTurn = false; myCommunicator.endTurn(); } else { // setting the destination coordinates to -1 // is the signal that the the choice of which // piece to move can be modified: myDestinationX = -1; myDestinationY = -1; } } //------------------------------------------------------// internal square selection methods /** * When the player has decided that the currently selected * square contains the piece he really wants to move, this * is called. This method switches to the mode where * the player selects the destination square of the move. */

private void fixSelection() { byte[] destination = (byte[])myPossibleMoves.elementAt(0); // setting the destination coordinates to valid // coordinates is the signal that the user is done // selecting the piece to move and now is choosing // the destination square: myDestinationX = destination[0]; myDestinationY = destination[1]; } /** * This method starts from the currently selected square * and finds the next square that contains a piece that * the player can move. */ private void selectNext() { // Test the squares one by one (starting from the // currently selected square) until we find a square // that contains one of the local player's pieces // that can move: byte testX = mySelectedX; byte testY = mySelectedY; while(true) { testX++; if(testX >= X_LENGTH) { testX = 0; testY++; testY = (new Integer(testY % Y_LENGTH)).byteValue(); } getMoves(testX, testY, myPossibleMoves, false); if((myPossibleMoves.size() != 0) || ((testX == mySelectedX) && (testY == mySelectedY))) { mySelectedX = testX; mySelectedY = testY; break; } } } /** * This method starts from the currently selected square * and finds the next square (to the left) that contains * a piece that the player can move. */ private void selectPrevious() { // Test the squares one by one (starting from the // currently selected square) until we find a square // that contains one of the local player's pieces // that can move: byte testX = mySelectedX; byte testY = mySelectedY; while(true) { testX--; if(testX < 0) { testX += X_LENGTH; testY--; if(testY < 0) {

testY += Y_LENGTH; } } getMoves(testX, testY, myPossibleMoves, false); if((myPossibleMoves.size() != 0) || ((testX == mySelectedX) && (testY == mySelectedY))) { mySelectedX = testX; mySelectedY = testY; break; } } } //------------------------------------------------------// internal utilities /** * Once the user has selected the move to make, this * updates the data accordingly. */ private void move() { // the piece that was on the source square is // now on the destination square: myGrid[myDestinationX][myDestinationY] = myGrid[mySelectedX][mySelectedY]; // the source square is emptied: myGrid[mySelectedX][mySelectedY] = 0; if(myDestinationY == 0) { myGrid[myDestinationX][myDestinationY] = 2; } // tell the communicator to inform the other player // of this move: myCommunicator.move(mySelectedX, mySelectedY, myDestinationX, myDestinationY); // deal with the special rules for jumps:: if((mySelectedY - myDestinationY > 1) || (myDestinationY - mySelectedY > 1)) { int jumpedY = (mySelectedY + myDestinationY)/2; int jumpedX = mySelectedX; int parity = mySelectedY % 2; // the coordinates of the jumped square depend on // what row we're in: if((parity > 0) && (myDestinationX > mySelectedX)) { jumpedX++; } else if((parity == 0) && (mySelectedX > myDestinationX)) { jumpedX--; } // remove the piece that was jumped over: myGrid[jumpedX][jumpedY] = 0; // now get ready to jump again if possible: mySelectedX = myDestinationX; mySelectedY = myDestinationY; myDestinationX = -1; myDestinationY = -1; // see if another jump is possible. // The "true" argument tells the program to return // only jumps because the player can go again ONLY

// if there's a jump: getMoves(mySelectedX, mySelectedY, myPossibleMoves, true); // if there's another jump possible with the same piece, // allow the player to continue jumping: if(myPossibleMoves.size() != 0) { myIsJumping = true; byte[] landing = (byte[])myPossibleMoves.elementAt(0); myDestinationX = landing[0]; myDestinationY = landing[1]; } else { myTurn = false; myCommunicator.endTurn(); } } else { // since it's not a jump, we just end the turn // by deselecting everything. mySelectedX = -1; mySelectedY = -1; myDestinationX = -1; myDestinationY = -1; myPossibleMoves.removeAllElements(); myTurn = false; // tell the other player we're done: myCommunicator.endTurn(); } } /** * Given a square on the grid, get the coordinates * of one of the adjoining (diagonal) squares. * 0 = top left * 1 = top right * 2 = bottom left * 3 = bottom right. * @return the coordinates or null if the desired corner * is off the board. */ private byte[] getCornerCoordinates(byte x, byte y, byte corner) { byte[] retArray = null; if(corner < 2) { y--; } else { y++; } // Where the corner is on the grid depends on // whether this is an odd row or an even row: if((corner % 2 == 0) && (y % 2 != 0)) { x--; } else if((corner % 2 != 0) && (y % 2 == 0)) { x++; } try { if(myGrid[x][y] > -15) { // we don't really care about the value, this // if statement is just there to get it to // throw if the coordinates aren't on the board. retArray = new byte[2];

retArray[0] = x; retArray[1] = y; } } catch(ArrayIndexOutOfBoundsException e) { // this throws if the coordinates do not correspond // to a square on the board. It's not a problem, // so we do nothing--we just return null instead // of returning coordinates since no valid // coordinates correspond to the desired corner. } return(retArray); } /** * Determines where the piece in the given * grid location can move. Clears the Vector * and fills it with the locations that * the piece can move to. * @param jumpsOnly if we should return only moves that * are jumps. */ private void getMoves(byte x, byte y, Vector toFill, boolean jumpsOnl y) { toFill.removeAllElements(); // if the square does not contain one of the local player's // pieces, then there are no corresponding moves and we just // return an empty vector. if(myGrid[x][y] <= 0) { return; } // check each of the four corners to see if the // piece can move there: for(byte i = 0; i < 4; i++) { byte[] coordinates = getCornerCoordinates(x, y, i); // if the coordinate array is null, then the corresponding // corner is off the board and we don't deal with it. // The later two conditions in the following if statement // ensure that either the move is a forward move or the // current piece is a king: if((coordinates != null) && ((myGrid[x][y] > 1) || (i < 2))) { // if the corner is empty (and we're not looking // for just jumps), then this is a possible move // so we add it to the vector of moves: if((myGrid[coordinates[0]][coordinates[1]] == 0) && (! jumpsOnly)) { toFill.addElement(coordinates); // if the space is occupied by an opponent, see if we can jump it: } else if(myGrid[coordinates[0]][coordinates[1]] < 0) { byte[] jumpLanding = getCornerCoordinates(coordinates[0], coordinates[1], i); // if the space on the far side of the opponent's piece // is on the board and is unoccupied, then a jump // is possible, so we add it to the vector of moves: if((jumpLanding != null) && (myGrid[jumpLanding[0]][jumpLanding[1]] == 0)) { toFill.addElement(jumpLanding); }

} } } // end for loop } }

(33) AttributesMIDlet
import import import import import import import import import javax.microedition.lcdui.Canvas; javax.microedition.lcdui.Command; javax.microedition.lcdui.CommandListener; javax.microedition.lcdui.Display; javax.microedition.lcdui.Displayable; javax.microedition.lcdui.Form; javax.microedition.lcdui.Graphics; javax.microedition.lcdui.StringItem; javax.microedition.midlet.MIDlet;

public class AttributesMIDlet extends MIDlet implements CommandListener { private Display display; protected boolean started; private Command exitCommand; protected void startApp() { if (!started) { display = Display.getDisplay(this); Canvas canvas = new DummyCanvas(); Form form = new Form("Attributes"); exitCommand = new Command("Exit", Command.EXIT, 0); form.addCommand(exitCommand); boolean isColor = display.isColor(); form.append(new StringItem(isColor ? "Colors: " : "Grays: ", Stri ng.valueOf(display .numColors()))); form.append(new StringItem("Width: ", String.valueOf(canvas.getWi dth()))); form.append(new StringItem("Height: ", String.valueOf(canvas.getH eight()))); form.append(new StringItem("Pointer? ", String.valueOf(canvas.has PointerEvents()))); form.append(new StringItem("Motion? ", String.valueOf(canvas.hasP ointerMotionEvents()))); form.append(new StringItem("Repeat? ", String.valueOf(canvas.hasR epeatEvents()))); form.append(new StringItem("Buffered? ", String.valueOf(canvas.is

DoubleBuffered()))); form.setCommandListener(this); display.setCurrent(form); started = true; } } protected void pauseApp() { } protected void destroyApp(boolean unconditional) { } public void commandAction(Command c, Displayable d) { if (c == exitCommand) { notifyDestroyed(); } } } class DummyCanvas extends Canvas { protected void paint(Graphics g) { } }

You might also like