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.)
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 |
Resulting Color |
setColor (255, 0, 0) |
Red |
setColor (0, 255, 0) |
Green |
setColor (0, 0, 255) |
Blue |
setColor (128, 0, 0) |
Dark red |
setColor (255, 255, 0) |
Yellow |
setColor (0, 0, 0) |
Black |
setColor (255, 255, 255) |
White |
setColor (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 |
Purpose |
drawImage (Image image, |
Draws an Image. Explained in detail in the int x, int y, int align) "Images" section. |
drawString (String text, |
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. |
drawRoundRect (int x, int y, |
Like drawRect(), except that an additional radius int w, int h, int r) is given for rounded corners of the rectangle. |
drawLine (int x0, int y0, |
Draws a line from (x0,y0) to (x1,y1). int x1, int y1) |
drawArc (int x, int y, Draws the outline of a circular or elliptical arc int w, int h, |
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 counter-clockwise rotation while a negative value indicates a clockwise rotation. |
fillRect (int x, int y, |
Similar to drawRect(), but fills the given area int w, int h) with the current color. |
fillRoundRect (int x, int y, |
Related to fillRect() as drawRoundRect() is int w, int h, related to drawRect(). int startAng, int endAng); |
fillArc (int x, int y, |
Like drawArc(), but fills the corresponding region. int w, int h, int startAng, int endAng); |
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 getHeight() 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.
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(), getClipWidth(), 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.
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 |
SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE |
Style |
STYLE_PLAIN, STYLE_ITALICS, STYLE_BOLD, STYLE_UNDERLINED |
Face |
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.
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); } }
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;
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 String array inside the Face class:
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) { } }
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.
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) { } }