MIDlets
All applications for the MID Profile must be derived from a special class, MIDlet. The MIDlet class manages the life cycle of the application. It is located in the package javax. microedition.midlet.
MIDlets can be compared to J2SE applets, except that their state is more independent from the display state. A MIDlet can exist in four different states: loaded, active, paused, and destroyed. Figure 3.1 gives an overview of the MIDlet lifecycle. When a MIDlet is loaded into the device and the constructor is called, it is in the loaded state. This can happen at any time before the program manager starts the application by calling thestartApp() method. After startApp() is called, the MIDlet is in the active state until the program manager calls pauseApp() or destroyApp();pauseApp() pauses the MIDlet, and desroyApp() terminates the MIDlet. All state change callback methods should terminate quickly, because the state is not changed completely before the method returns.
Figure 3.1 The life cycle of a MIDlet.
In the pauseApp() method, applications should stop animations and release resources that are not needed while the application is paused. This behavior avoids resource conflicts with the application running in the foreground and unnecessary battery consumption. The destroyApp() method provides an unconditional parameter; if it is set to false, the MIDlet is allowed to refuse its termination by throwing aMIDletStateChangeException. MIDlets can request to resume activity by calling resumeRequest(). If a MIDlet decides to go to the paused state, it should notify the application manager by calling notifyPaused(). In order to terminate, a MIDlet can call notifyDestroyed(). Note thatSystem.exit() is not supported in MIDP and will throw an exception instead of terminating the application.
Note - Some devices might terminate a MIDlet under some circumstances without calling destroyApp(), for example on incoming phone calls or when the batteries are exhausted. Thus, it might be dangerous to rely on destroyApp() for saving data entered or modified by the user.
Display and Displayable
MIDlets can be pure background applications or applications interacting with the user. Interactive applications can get access to the display by obtaining an instance of the Display class. A MIDlet can get its Display instance by calling Display.getDisplay (MIDlet midlet), where the MIDlet itself is given as parameter.
The Display class and all other user interface classes of MIDP are located in the package javax.microedition.lcdui. The Display class provides a setCurrent() method that sets the current display content of the MIDlet. The actual device screen is not required to reflect the MIDlet display immediately—the setCurrent() method just influences the internal state of the MIDlet display and notifies the application manager that the MIDlet would like to have the given Displayable object displayed. The difference between Display and Displayable is that the Display class represents the display hardware, whereas Displayable is something that can be shown on the display. The MIDlet can call the isShown() method ofDisplayable in order to determine whether the content is really shown on the screen.
HelloMidp Revisited
The HelloMidp example from Chapter 1, "Java 2 Micro Edition Overview," is already a complete MIDlet. Now that you have the necessary foundation, you can revisit HelloMidp from an API point of view.
First, you import the necessary midlet and lcdui packages:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
Like all MIDP applications, the HelloMidp example is required to extend the MIDlet class:
public class HelloMidp extends MIDlet {
In the constructor, you obtain the Display and create a Form:
Display display;
Form mainForm;
public HelloMidp () {
mainForm = new Form ("HelloMidp");
}
A Form is a specialized Displayable class. The Form has a title that is given in the constructor. You do not add content to the form yet, so only the title will be displayed. (A detailed description of the Form class is contained in the next section.)
When your MIDlet is started the first time, or when the MIDlet resumes from a paused state, the startApp() method is called by the program manager. Here, you set the display to your form, thus requesting the form to be displayed:
public void startApp() {
display = Displayable.getDisplay (this);
display.setCurrent (mainForm);
}
When the application is paused, you do nothing because you do not have any allocated resources to free. However, you need to provide an empty implementation because implementation of pauseApp() is mandatory:
public void pauseApp() {
}
Like pauseApp(), implementation of destroyApp() is mandatory. Again, you don't need to do anything here for this simple application:
public void destroyApp(boolean unconditional) {
}
}
Note - The HelloMidp Midlet does not provide a command to exit the MIDlet, assuming that the device provides a general method of terminating MIDlets. For real MIDP applications, we recommend that you add a command to terminate the MIDlet because the MIDP specification does not explicitly support this assumption. More information about commands can be found in the section "Using Commands for User Interaction."
MIDP User Interface APIs
The MIDP user interface API is divided into a high- and low-level API. The high-level API provides input elements such as text fields, choices, and gauges. In contrast to the Abstract Windows Toolkit (AWT), the high-level components cannot be positioned or nested freely. There are only two fixed levels: Screens and Items. The Items can be placed in a Form, which is a specialized Screen.
The high-level Screens and the low-level class Canvas have the common base class Displayable. All subclasses of Displayable fill the whole screen of the device. Subclasses of Displayable can be shown on the device using the setCurrent() method of the Display object. The display hardware of a MIDlet can be accessed by calling the static method getDisplay(), where the MIDlet itself is given as parameter. In the HelloMidpexample, this step is performed in the following two lines:
Display display = Display.getDisplay (this);
...
display.setCurrent (mainForm);
Figure 3.2 shows an overview of the MIDP GUI classes and their inheritance structure.
The following sections first describe the high-level API and then the low-level API. A more complex sample application that uses both levels of thelcdui package together is shown in Chapter 9, "Advanced Application: Blood Sugar Log."
Figure 3.2 The MIDP GUI classes.
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
The most important subclass of Screen is the class Form. A Form can hold any number of Items such as StringItems, TextFields, andChoiceGroups. 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.
Items can 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.
Table 3.1 All Subclasses of Item
Item | Description |
ChoiceGroup | Enables the selection of elements in group. |
DateField | Used for editing date and time information. |
Gauge | Displays a bar graph for integer values. |
ImageItem | Used to control the layout of an Image. |
StringItem | Used for read-only text elements. |
TextField | 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 version label. After creation, the label is added to the main form in the constructor of the HelloMidp 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 Imageobject, 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 | Value |
LAYOUT_CENTER | The image is centered horizontally. |
LAYOUT_DEFAULT | A device-dependent default formatting is applied to the image. |
LAYOUT_LEFT | The image is left-aligned. |
LAYOUT_NEWLINE_AFTER | A new line will be started after the image is drawn. |
LAYOUT_NEWLINE_BEFORE | A new line will be started before the image is drawn. |
LAYOUT_RIGHT | The image is aligned to the 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 Constraint Constant Values
Constant | Value |
ANY | Allows any text to be added. |
EMAILADDR | Adds a valid e-mail address, for instancemyemail@mydomain.com. |
NUMERIC | Allows integer values. |
PASSWORD | Lets the user enter a password, where the entered text is masked. |
PHONENUMBER | Lets the user enter a phone number. |
URL | 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 simpleStrings, 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 | Value |
EXCLUSIVE | Specifies a ChoiceGroup or List having only one element selected at the same time. |
IMPLICIT | Valid for Lists only. It lets the List sendCommands to indicate state changes. |
MULTIPLE | In contrast to EXPLICIT, MULTIPLE allows the selection of multiple elements. |
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 nullbecause 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.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 theItemStateListener interface are able to receive notifications for the following events:
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 theItemStateListener 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 allItems 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 currencyChoiceGroup.
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, andTextBox for the high-level API and Canvas for the low-level API.
No positioning or layout information is passed to the Command—the 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 | Value |
Command.BACK | Used for navigation commands that are used to return the user to the previous Screen. |
Command.CANCEL | Needed to notify the screen that a negative answer occurred. |
Command.EXIT | Used to specify a Command for exiting the application. |
Command.HELP | Passed when the application requests a help screen. |
Command.ITEM | A command type to tell the application that it is appended to an explicit item on the screen. |
Command.OK | Needed to notify the screen that a positive answer occurred. |
Command.SCREEN | A type that specifies a screen-specific Commandof the application. |
Command.STOP | Interrupts a procedure that is currently running. |
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 lcduipackage contains the interface CommandListener. A CommandListener can be registered to any Displayable class using the setCommandListenermethod. 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 correspondingcommandAction() 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 device-dependent 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() andgetMaxValue() 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. PossibleDateField mode constants are listed in Table 3.6.
Table 3.6 DateField Mode Constants
Constant | Value |
DATE | Passed if the DateField should be used for entering a date only. |
DATE_TIME | Used for creating a DateField to enter both date and time information. |
TIME | 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.
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 Listand 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 theList is selected, a Command of the type List.SELECT_COMMAND is issued. As in the ChoiceGroup, the elements consist of Strings and optionalImages.
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 classCurrencyList 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 {
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 theCurrencyList, 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 Stringarrays 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 for applying the entered text and clearCommand for clearing the text:
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 theTeleTransfer 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 methoditemStateChanged(). To display the two new Screens CurrencyList and TransferReason, you implement the two commands currencyCommandand 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 TeleTransferapplication; 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.java—The 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);
}
}
}
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 thelcdui 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 callingserviceRepaints(). (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 andGraphics 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() andgetHeight(). 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. ThesetColor() 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 ofsetGrayScale(). 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 theGraphics 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 radiusint 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 arcint w, int h, | covering the specified rectangle, using the current int startAng, int arcArc) color and stroke style. The resulting arc begins at startAngand extends for arcAngdegrees. 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 areaint w, int h) with the current color. |
fillRoundRect (int x, int y, | Related to fillRect() asdrawRoundRect() is int w, int h, related todrawRect(). 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 theclipRect() 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 explicitFont 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 combined—for 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, andkeyReleased() 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, sopaint() 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 background—for 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 itsIDEAL_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 theJavagochi'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 thetranslate() 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 thedrawEye() 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 andkeyMinor 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.java—The 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 callingcallSerially() 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 callingdrawArc():
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) {
}
}