IDP provides a lot of efficiency and flexibility when building user interfaces. The efficiency part can be seen in what MIDP calls the high-level user interface controls, which is what I discussed last month. This month I will explore the flexibility side of MIDP user interfaces in what MIDP calls the low-level user interface APIs. The MIDP low-level user interface APIs provide much more fine-grained control and nearly limitless customization over an application’s user interface. However, there are risks to be managed and tradeoffs to be made when using the low-level APIs. In this article I explore the effort involved in creating low-level user interfaces and discuss some of the implications and risks you are likely to encounter.
The low-level user interface is essentially composed of the Canvas class and a handful of support classes such as Graphics, Font, and Image. The supporting classes are used within an instance of Canvas to create different visual effects.
Canvas is an abstract class. In order to create a user interface using the low-level APIs, an application must create a subclass of Canvas and implement the paint() method. The paint() method is the only method requiring implementation. An instance of Graphics, for example, is passed as a parameter to paint(), which provides access to the device drawing capabilities that allow for directly interacting with the display at the pixel level.
The nice thing about working with the MIDP low-level classes is that very complex user interfaces can be created with only a few classes. However, there are caveats to using this approach. Take the following example, which draws a string on the screen.
public void paint(Graphics graphics) { graphics.drawString("This is a very boring MIDlet "+ "that is nearly putting me to sleep!", 0, 0, Graphics.TOP|Graphics.LEFT);}
Figure 1 shows the MIDlet running in the J2ME Wireless Toolkit where the Canvas has been set as the current display. Although the MIDlet paints the string to the screen there are a few problems that immediately begin to surface.
![]() Figure 1. Overlap: Here’s an example of using a Canvas to write some text. However, the Canvas was not cleared first and so the text was drawn over the existing image. | ![]() Figure 2. Set to Black: Here I called Graphics.fillRect() to clear the screen. However, because the color is set to black for both the fillRect() and drawString() calls, the text disappears. |
The first problem is that the Canvas was not cleared before writing the sentence. As a result, the string painted directly onto the existing image, which happened to be the application launch screen of the Application Management Software (AMS). In the next example, I add a line of code to clear the screen. This technique fills the entire screen with the same color pixels.
public void paint(Graphics graphics) { graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.drawString("This is a very boring MIDlet "+ "that is nearly putting me to sleep!", 0, 0, Graphics.TOP|Graphics.LEFT);}
But now I have a new problem. The screen seems to have gone blank (see Figure 2). What happened is the Canvas filled the screen with pixels of the same color, but the current Canvas color happened to be set to black. Then, when the drawString() method was called, the text was drawn in black as well, having no visible effect at all. To get the desired effect, I’m going to have to manipulate the colors.
Playing with Color
![]() | |
Figure 3. Hue Knew? The color values in the color bar correspond to the different calls to setColor() (see left). |
Color and grayscale effects can be manipulated using calls to setColor() and setGrayScale(), respectively. setColor() takes three parameters that describe the desired RGB (red, green, blue) values. In calls to setColor(), a value between 0 and 255 is passed for each RGB color dimension. Passing different values will cause the corresponding color to become more or less saturated in the result. For example a call with the values setColor(0, 100, 255) means the color will have no red, about 40 percent green, and 100 percent blue. A call with setColor(255, 0, 0) will be bright red. Figure 3 shows a palate of colors corresponding to following lines of code:
· graphics.setColor(255,255,255);· graphics.setColor(0,0,0);· graphics.setColor(100,100,100);· graphics.setColor(255,0,0);· graphics.setColor(0,255,0);· graphics.setColor(0,0,255);
![]() | |
Figure 4. Contrast: Setting the background color to white and the text color to black means this time the user can see the text when it writes. |
Calls to setGrayScale() require only a single parameter that describes the desired balance of white and black. The gray scale range is 0 to 255, where 0 is black and 255 is white.
With an idea of how setColor() works, I now can add code to first clear the screen to a white background and then paint black text on top of the white background (see Figure 4).
public void paint(Graphics graphics) { graphics.setColor(255,255,255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.setColor(0,0,0); graphics.drawString("This is a very boring MIDlet "+ "that is nearly putting me to sleep!", 0, 0, Graphics.TOP|Graphics.LEFT);}
Now things are looking better. The AMS image is cleared by the call to fillRect(), turning the background to white, and the text is now displaying in black.
However, there is one more thing I need to fix. The string is too long for the screen. The Graphics.drawString() method does not support line wrapping, so I’ll have to handle it myself. With an example this simple, I can probably get away with splitting the sentence into two parts and calling drawstring() twice. However, I still need to figure out how far down to start drawing the next line. I could guess that 10 or 15 pixels would be sufficient; however, there are more scientific ways of figuring this out using font metrics.
Using Font Metrics
Font metrics, along with other font information, is available from the Font class. To get the current font in use, make a call to Graphics.getFont(). The height of the font can be found with a call to Font.getHeight(). The following code improves on the example and shows how font metrics can be used to split the lines into two parts.
public void paint(Graphics graphics) { String text1 = "This is a very boring MIDlet "; String text2 = "that is nearly putting me to sleep!"; graphics.setColor(255,255,255); graphics.fillRect(0, 0, getWidth(), getHeight()); graphics.setColor(0,0,0); Font font = graphics.getFont(); int fontHeight = font.getHeight(); graphics.drawString(text1, 0, 0, Graphics.TOP|Graphics.LEFT); graphics.drawString(text2, 0, fontHeight, Graphics.TOP|Graphics.LEFT);}
![]() | |
Figure 5. That’s a Wrap? This method splits the text into two parts and calls drawString() twice to get a line-wrapping effect. |
Figure 5. That’s a Wrap? This method splits the text into two parts and calls drawString() twice to get a line-wrapping effect.
At this point, my simple example is becoming more and more complex, and all I have done is print a line of text to the screen. Although Canvas provides a lot of fine-grain control and flexibility in creating user interfaces, you can see how many more details need attending.
These details become even more important to manage when writing applications for multiple devices. Screen size and color support is one of the most likely things to change between devices. Great care must be taken to ensure that a Canvas can adapt to any environment. In other words, guessing about font height, line wrapping, image size, and so forth will probably land you in trouble.
Simple Line Wrapping.
With portability in mind, I’m going to take another look at the example thus far and see what I can do to enhance portability and allow any string to be drawn correctly.
Line-wrapping algorithms can become rather complex in a hurry. To keep this example simple, lines will be wrapped at the character level, rather than the word level. Also, vertical scrolling will not be considered, meaning that this implementation expects the full text to fit on one screen.
This approach examines each character in the string one at a time and determines where to draw the character in relation to all the other characters on the screen. Each character is drawn one a time along the Y access, incrementing the X access by the width of the character each time. When the right edge of the screen is reached, and the next character does not fit in the space remaining, the value of Y is incremented by the font height and X is reset to 0. The result is a character-level line wrapping effect.
The Font class contains a method for measuring the size of a specific character for a given font. When dealing with true-type fonts it is important to understand that characters differ in width. For example “W” is wider than “i.” This requires each character to be measured individually using Font.charWidth(). The example enhanced with line wrapping is shown in Listing 1.
My simple, even boring, example has become rather interesting. It illustrates how much fine-grain control is provided by the low-level APIs. However, it also highlights the complexity that ensues in order to do seemingly simple tasks such as line-wrap text. This realization should cause developers to think through the benefits of using the low-level APIs to make sure that the benefits are worth the cost to implement and maintain code of this complexity.
So far, I have demonstrated the basics for drawing text on the screen. However, the Graphics class also defines methods for drawing shapes and images as well. The principles for using these other Graphics methods are essentially the same as what has been demonstrated thus far. So, at this point I want to shift gears and discuss some of the event-handling capabilities of the Canvas class.
Low-Level Event Handling
The low-level UI provides a lot of event-handling options. Commands can be added to a Canvas in order to map soft-keys (physical keys on the keypad that map to labels on the display) just like high-level UI components such as List and Form. However, Canvas also allows for capturing raw key-press events for most of the keys available on the device. The Canvas APIs for monitoring key press activity are as follows:
Event | Description |
keyPressed() | This event is called when a physical key on the keypad is pressed. |
keyReleased() | This event is called when a pressed key is released. |
keyRepeated() | This event is called when a key is held down. This event may not be supported on all platforms. Support of this method can be verified with a call to hasRepeatEvents(). |
In order to monitor key events, a subclass of Canvas must override one of these methods. An int code corresponding to the key to which the event pertains is passed as a parameter. It is important to understand that key codes are likely to change between platforms, making key event handling another sensitive area of low-level UI portability. For example, on device A, pressing “2” on the keypad may come through as key code 25 but on device B the same key might come through as key code 12. Fortunately, there are tools provided by MIDP to assist in normalizing key press events.
Key Handling Across Devices
Given that key codes can vary across devices, MIDP provides key mappings so that device implementers can map the raw key events into known constant values. The simplest form of this is the numeric key mappings in the Canvas class. There are 10 constant values corresponding to a 10-digit numeric keypad, plus two more for the “*” and “#” keys. So if an application needs to detect when the “6” key is pressed, an application can do something like the following:
public void keyPressed(int code){ if (code == KEY_NUM6) { System.out.println("Key 6 Pressed!"); }}
The catch here is that if a device supports more than a 10-digit keypad, such as an alphanumeric keypad, MIDP does not provide constants to map all the keys. If an application needs to monitor an extended key code range, device-specific key handling may be needed.
In addition to the data-related keys, MIDP provides mapping support for some of the more standard control keys, such as up, down, left, right, and so forth. The method Canvas.getGameAction() can be used within one of the key-handling events to translate a raw key code to one of the following constant values defined in the Canvas class: UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C, GAME_D. In some cases, the mapping will work rather well. Many devices have some type of scrolling behavior that maps to the up, down, left and right keys. However, mappings for the FIRE key or the generic GAME_B key may translate in a way that is less than optimal for a particular application.
The following code shows a keyPressed() event translating the FIRE and GAME_B keys, which, for the J2ME Wireless Toolkit, are mapped to the “select” key in the middle of the jog dial and the “3” key, respectively. The following table shows how the game key constant, the actual key (in the wireless toolkit emulator), and the raw key code relate to one another for these two key mappings
Game Constant | Physical Key (J2ME Wireless Toolkit) | Key code value |
FIRE | “select” key | -5 |
GAME_B | “3” key | 51 |
Below is an example showing how the getGameAction() method can be used to make decisions in key-handling events.
public void keyPressed(int code){ int action = getGameAction(code); switch (action) { case FIRE: { System.out.println("FIRE Pressed!"); break; } case GAME_B: { System.out.println("GAME_B Pressed!"); break; } }}
While the low-level APIs can be challenging to implement, they provide a way for developers to address portability issues that would difficult or impossible otherwise. With some thought and attention to detail, the low-level APIs can be used to create a custom user interface with a unique look and feel catering to a specific application’s need. This flexibility does not need to come at the expense of portability either. By making use of font metrics, Canvas screen size methods and key mappings, MIDP applications can take advantage of the low-level APIs while remaining very portable across devices.