MIDP Programming with J2ME
December 26, 2002
This is Chapter 3: MIDP Programming from the book J2ME Application Development (ISBN:0-672-323095-9) written by Michael Kroll and Stefan Haustein , published by Sams Publishing.
© Copyright Pearson Education. All rights reserved.
Chapter 3: MIDP Programming
In This Chapter
MIDlets
High-Level API
Low-Level API
This chapter handles the life cycle and user interface of Mobile Information
Device Profile (MIDP) applications. First, the general design of MIDP
applications will be discussed. Then, the high-level user interface API will be
explained. Finally, the low-level user interface API for free graphics and games
will be described.
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 the
startApp() 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 a MIDletStateChangeException. 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 that System.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 immediatelythe 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 of Displayable 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 HelloMidp example, 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 the
lcdui 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, and ChoiceGroups.
Items can be added to the Form using the append()
method.
The Item class itself is an abstract base class that cannot be
instantiated. It provides a label that is a common property of all subclasses.
The label can be set and queried using the setLabel()and
getLabel() methods, respectively. The label is optional, and a
null value indicates that the item does not have a label. However,
several widgets switch to separate screens for user interaction, where the label
is used as the title of the screen. In order to allow the user to keep track of
the program state, it is recommended that you provide a label at least for
interactive items.
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 Image object, a layout parameter, and an
alternative text string that is displayed when the device is not able to display
the image. The image given to the constructor must be non-mutable. All images
loaded from the MIDlet suite's JAR file are not mutable. (Details
about adding resources to a JAR file are explained in Chapter 2, "The
Connected Limited Device Configuration.")
The difference between mutable and non-mutable Images is described
in more detail in the section about Images in the "Low Level
API" section of this chapter. For now, we will treat the Image
class as a "black box" that has a string constructor that denotes the
location of the image in the JAR file. Please note that Image
construction from a JAR file throws an IOException if the image cannot
be loaded for some reason. The layout parameter is one of the integer constants
listed in Table 3.2, where the newline constants can be combined with the
horizontal alignment constants.
Table 3.2 ImageItem Layout Constants
|
Constant
|
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 instance myemail@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 simple
Strings, but can display an optional image per element as well.
ChoiceGroups can be of two different types. Corresponding type
constants are defined in the Choice interface. These constants are used
in the List class as well; the List class allows an additional
third type. The three type constants are listed in Table 3.4.
Table 3.4 Choice Type Constants
|
Constant
|
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 send
Commands 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 null because you
do not want Images to be displayed at this time:
ChoiceGroup currency = new ChoiceGroup
("Currency", Choice.EXCLUSIVE,
new String[] {"USD", "EUR", "JPY"}, null);
You still need to add the currency ChoiceGroup to your main
Form. As for the text fields, this is done via the append()
method of Form:
mainForm.append (currency);
Figure 3.4 shows the TeleTransfer
application extended to choose a currency using a ChoiceGroup.
Figure 3.4 The TeleTransfer
MIDlet extended to enable the user to choose a currency.
Receiving Changes from Interactive UI Items
If you run the new version of the TeleTransfer MIDlet, you can
change the currency using the ChoiceGroup, but the TextField
labels for Dollar and Cent are not changed accordingly. You need a way to notify
the application if a selection is made in the currency ChoiceGroup.
Receiving changes of interactive high-level UI items in MIDP is based on a
listener model similar to AWT. Classes implementing the
ItemStateListener interface are able to receive notifications for the
following events:
The events are sent to the method itemStateChanged() of the
ItemStateListener, where the item that has changed is given as a
parameter. In order to actually receive these events, the
ItemStateChangeListener must be registered using the
setItemStateListener() method of the corresponding Form.
Now that you know about item state change events, you can add the desired
functionality to your TeleTransfer MIDlet. First, you need to add the
ItemStateListener interface to the class declaration:
public class TeleTransfer extends MIDlet
implements ItemStateListener {
You also need to implement a corresponding itemStateChanged()
method. Since the itemStateChanged() method is called for changes of
all Items in the Form, you need to check the item parameter
indicating the event source first. If the source of the event is the currency
ChoiceGroup, you set the labels of the amount and fraction
TextFields correspondingly:
public void itemStateChanged (Item item) {
if (item == currency) {
int index = currency.getSelectedIndex ();
switch (index) {
case 0: amountWhole.setLabel ("Dollar");
amountFraction.setLabel ("Cent");
break;
case 1: amountWhole.setLabel ("Euro");
amountFraction.setLabel ("Cent");
break;
case 2: amountWhole.setLabel ("Yen");
amountFraction.setLabel ("Sen");
}
}
}
Just adding the interface and implementing the corresponding methods is not
sufficient to enable the MIDlet to receive state changes. Additionally, you need
to register your ItemStateListener at the Form containing the
currency item. You do so by calling the setItemStateListener() method
in the TeleTransfer constructor:
public TeleTransfer () {
mainForm.append (senderAccount);
...
mainForm.append (currency);
mainForm.setItemStateListener (this);
}
Figure 3.5 shows the new version of the
TeleTransfer example, where the labels are changed depending on the
state of the currency ChoiceGroup.
Figure 3.5 The TeleTransfer
MIDlet extended to change the labels depending on the state of the currency
ChoiceGroup.
Using Commands for User Interaction
Now you can enter all the information required for a telegraphic transfer,
but you have no means to initiate the actual transfer.
In contrast to desktop computers, which have plenty of screen space for
displaying buttons or menus, a different approach is necessary for mobile
devices. Some devices provide so-called soft buttons, which are buttons
without fixed functionality that are assigned dynamically depending on the
application context. The number of soft buttons may vary if they are available.
Other mobile devices do not even have space for soft buttons, but provide
scrolling menus. MIDP needs to abstract from the concrete device and to provide
a mechanism that is suitable for all devices, independent of the availability
and number of soft buttons. Thus, the lcdui package does not provide
buttons or menus, but an abstraction called Command.
Commands can be added to all classes derived from the
Displayable class. These classes are Screen and its subclasses
such as Form, List, and TextBox for the high-level
API and Canvas for the low-level API.
No positioning or layout information is passed to the
Commandthe Displayable class itself is completely
responsible for arranging the visible components corresponding to
Commands on a concrete device. The only layout and display information
that can be assigned to a Command except from the command label is
semantic information. The semantic information consists of a type and a
priority. The priority allows the device to decide which commands are displayed
as soft buttons if the number of commands exceeds the number of soft buttons
available. For additional commands not displayed as soft buttons, a separate
menu is created automatically. The type information is an additional hint for
the device about how to display the command. For example, if the Exit command is
always assigned to the leftmost soft button in native applications of a certain
device type, the MIDP implementation is able to make the same assignment. Thus,
a consistent look and feel can be accomplished for a device.
The available command type constants are listed in Table 3.5.
Table 3.5 Command Type Constants
|
Constant
|
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 Command of 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
lcdui package contains the interface CommandListener. A
CommandListener can be registered to any Displayable class
using the setCommandListener method. After registration, the method
commandAction() of the Commandlistener is invoked whenever the
user issues a Command. In contrast to AWT, only one listener is allowed
for each Displayable class. The commandAction() callback
method provides the Displayable class where the command was issued and
the corresponding Command object as parameters.
With this information, you can extend your TeleTransfer application
with the desired Commands. But before going into actual command
implementation, you need to add some corresponding functionality. You'll
add three commands: a Send command, a Clear command, and an Exit command. For
Clear, you just add a method setting the content of the fields of your form to
empty strings:
public void clear () {
receiverName.setString ("");
receiverAccount.setString ("");
amountWhole.setString ("");
amountFraction.setString ("");
}
The Send command is a bit more difficult since you do not yet have the
background to really submit information over the network. (Network connections
will be handled in Chapter 6, "Networking: The Generic Connection
Framework.") So you just display the content to be transmitted in an alert
screen as a temporary replacement:
public void send () {
Alert alert = new Alert ("Send");
alert.setString ("transfer " + amountWhole.getString ()
+ "." + amountFraction.getString () + " "
+ amountWhole.getLabel ()
+ "\nto Acc#" + receiverAccount.getString ()
+ "\nof " + receiverName.getString ());
alert.setTimeout (2000);
display.setCurrent (alert);
clear ();
}
For leaving the application, the MIDlet already provides the
notifyDestroyed() method, so you do not need to add anything here.
Now that you have implemented the corresponding functionality, the next step
is to add the actual Command objects to your application class:
static final Command sendCommand = new Command ("Send", Command.SCREEN, 1);
static final Command clearCommand = new Command ("Clear", Command.SCREEN, 2);
static final Command exitCommand = new Command ("Exit", Command.EXIT, 2);
In order to enable the MIDlet to receive command actions, you need to
implement the CommandListener interface, and the corresponding
commandAction() method. Depending on the command received, you call
send(), clear(), or notifyDestroyed():
public class TeleTransfer extends MIDlet
implements ItemStateListener, CommandListener {
public void commandAction (Command c, Displayable d) {
if (c == exitCommand) {
notifyDestroyed();
}
else if (c == sendCommand) {
send ();
}
else if (c == clearCommand) {
clear ();
}
}
With these modifications, your TeleTransfer MIDlet is able to handle
the desired commands. You still need to add the Commands to the
Form, and register the TeleTransfer MIDlet as a
CommandListener in order to actually receive the commands:
public TeleTransfer () {
...
mainForm.addCommand (sendCommand);
mainForm.addCommand (clearCommand);
mainForm.addCommand (exitCommand);
mainForm.setCommandListener (this);
}
Figure 3.6 shows the Send Alert
of the new version of your TeleTransfer application.
Figure 3.6 The TeleTransfer
MIDlet showing an alert that displays the transfer information as a summary
before sending.
Further Item Classes: Gauge and
DateField
Now you have used all the Item subclasses except Gauge and
DateField. Both classes are specialized input elements, where the
Gauge may also make sense as a pure read-only information item.
The Gauge item visualizes an integer value by displaying a
horizontal bar. It is initialized with a label, a flag indicating whether it is
interactive, and a maximum and an initial value. If a Gauge is
interactive, the user is allowed to change the value using a 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() and getMaxValue() methods let you access the
maximum value of the Gauge.
The DateField is a specialized widget for entering date and time
information in a simple way. It can be used to enter a date, a time, or both
types of information at once. The appearance of the DateField is
specified using three possible input mode constants in the constructor. Possible
DateField mode constants are listed in Table 3.6.
Table 3.6 DateField Mode Constants
|
Constant
|
Value
|
|
DATE
|
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 List
and TextBox classes.
The List Class
One possibility to clean up the user interface is to move the currency
selection to a separate screen. It takes a lot of space and may need even more
room if additional options are added. Also, you can assume that the currency is
not changed very often.
You could create a new Form and just move the ChoiceGroup
there. However, lcdui provides a special List class inherited
from Screen for this purpose. The advantage of the List class
is that it provides the IMPLICIT mode that was already mentioned in the
section "Selecting Elements Using ChoiceGroups." Using the
IMPLICIT mode, the application gets immediate notification when an item
is selected. Whenever an element in the List is selected, a
Command of the type List.SELECT_COMMAND is issued. As in the
ChoiceGroup, the elements consist of Strings and optional
Images.
For initializing the List, the lcdui packages offers
constructors. The constructors work like the ChoiceGroup constructors.
The first one creates an empty List with a given title and type only.
The second one takes the title, the type, an array of Strings as
initial amount of List elements, and an optional array of
Images for each List element. In the implementation of the
TeleTransfer application, you implement a new class
CurrencyList extending List that will be used as your new
currency selector. Since you will use the IMPLICIT mode, you need to
implement a command listener, so you can already add the corresponding
declaration:
public class CurrencyList extends List implements
CommandListener {
To set the labels of the main form TextFields according to the index
of the selected element in the CurrencyList, you create two
String arrays, CURRENCY_NAMES and
CURRENCY_FRACTIONS:
static final String [] CURRENCY_NAMES = {"Dollar", "Euro", "Yen"};
static final String [] CURRENCY_FRACTIONS = {"Cent", "Cent", "Sen"};
In order to set the labels of the main forms TextFields for the
whole and the fractional amount according to the selected currency in the
CurrencyList, you need a reference back to the main
TeleTransfer MIDlet. For this reason, you store the
TeleTransfer reference in a variable called teleTransfer. The
reference is set in the constructor of your CurrencyList:
TeleTransfer teleTransfer;
In the constructor, you also add currency symbol images to the list. You need
to load them, but the call to the super constructor must be the first statement
in a constructor. So you call the constructor of the super class by specifying
the title and type only. Then you create the Images needed for each
list element, which are stored in the MIDlet suite's JAR file. You also
call setCommandListener() to register the currency list for handling
commands that are issued:
public CurrencyList (TeleTransfer teletransfer) {
super ("Select Currency", Choice.IMPLICIT);
this.teleTransfer = teletransfer;
try {
append ("USD", Image.createImage ("/Dollar.png"));
append ("EUR", Image.createImage ("/Euro.png"));
append ("JPY", Image.createImage ("/Yen.png"));
}
catch (java.io.IOException x) {
throw new RuntimeException ("Images not found");
}
setCommandListener (this);
}
The final step in creating the CurrencyList is to implement the
commandAction() method of the CommandListener interface. As
you already know, a List of IMPLICIT type issues a
List.SELECT_COMMAND to the registered CommandListener whenever
a new element is selected to indicate the selection change. In case of a
selection change, you modify the labels of the main form TextFields.
The actual labels are obtained from the String arrays
CURRENCY_NAMES and CURRENCY_FRACTIONS. Using the
teleTransfer reference, you can access the TextFields.
Finally, you call the new method teleTransfer.back(), which sets the
screen back to the main form (the back() method will be given at the
end of this section):
public void commandAction (Command c, Displayable d) {
if (c == List.SELECT_COMMAND) {
teleTransfer.amountWhole.setLabel
(CURRENCY_NAMES [getSelectedIndex ()]);
teleTransfer.amountFraction.setLabel
(CURRENCY_FRACTIONS [getSelectedIndex ()]);
teleTransfer.back ();
}
}
}
Figure 3.7 shows currency Images
and abbreviations in the CurrencyList.
Figure 3.7 The new
CurrencyList.
The TextBox Class
Beneath Alert, List, and Form, there is only one
further subclass of Screen: the TextBox. The TextBox
allows the user to enter multi-line text on a separate screen. The constructor
parameters and the constraint constants are identical to those of
TextField.
As for the currency list, you can also add a new screen enabling the user to
enter a transfer reason if desired. Similar to the CurrencyList, you
implement a new class handling the commands related to the new screen. However,
this time it is derived from the TextBox. Again, you implement the
CommandListener interface:
public class TransferReason extends TextBox implements CommandListener {
In the TextBox, you provide two commands, okCommand 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 the
TeleTransfer implementation somewhat. Since the
TeleTransfer's ChoiceGroup for selecting the currency is
replaced by the CurrencyList, you do not need the
ItemStateListener for detecting item changes any more. So you remove
the listener and also the corresponding callback method
itemStateChanged(). To display the two new Screens
CurrencyList and TransferReason, you implement the two
commands currencyCommand and reasonCommand. The new commands
are added to the MIDlet in the constructor using the addCommand()
method. In the clear() method, the new TextBox is also cleared
by calling the corresponding setString() method. Finally you add the
back() method to the TeleTransfer application; this method is
called from the new Screens to return to the main form. The
commandAction() method is extended to handle the new commands,
displaying the new Screens. Listing 3.1 shows the complete source code
of the final version of the TeleTransfer application.
Listing 3.1 TeleTransfer.javaThe Complete TeleTransfer
Sample Source Code
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
class CurrencyList extends List implements CommandListener {
TeleTransfer teleTransfer;
static final String [] CURRENCY_NAMES = {"Dollar", "Euro", "Yen"};
static final String [] CURRENCY_FRACTIONS = {"Cent", "Cent", "Sen"};
public CurrencyList (TeleTransfer teletransfer) {
super ("Select Currency", Choice.IMPLICIT);
this.teleTransfer = teletransfer;
try {
append ("USD", Image.createImage ("/Dollar.png"));
append ("EUR", Image.createImage ("/Euro.png"));
append ("JPY", Image.createImage ("/Yen.png"));
}
catch (java.io.IOException x) {
throw new RuntimeException ("Images not found");
}
setCommandListener (this);
}
public void commandAction (Command c, Displayable d) {
if (c == List.SELECT_COMMAND) {
teleTransfer.amountWhole.setLabel
(CURRENCY_NAMES [getSelectedIndex ()]);
teleTransfer.amountFraction.setLabel
(CURRENCY_FRACTIONS [getSelectedIndex ()]);
teleTransfer.back ();
}
}
}
class TransferReason extends TextBox implements CommandListener {
static final Command okCommand = new Command ("OK", Command.BACK, 1);
static final Command clearCommand = new Command
("Clear", Command.SCREEN, 2);
TeleTransfer teleTransfer;
public TransferReason (TeleTransfer teleTransfer) {
super ("Transfer Reason", "", 50, TextField.ANY);
this.teleTransfer = teleTransfer;
addCommand (okCommand);
addCommand (clearCommand);
setCommandListener (this);
}
public void commandAction (Command c, Displayable d) {
if (c == clearCommand) {
setString ("");
}
else if (c == okCommand) {
teleTransfer.back ();
}
}
}
public class TeleTransfer extends MIDlet implements CommandListener {
static final Command sendCommand = new Command ("Send", Command.SCREEN, 2);
static final Command clearCommand = new Command
("Clear", Command.SCREEN, 2);
static final Command exitCommand = new Command ("Exit", Command.SCREEN, 1);
static final Command currencyCommand = new Command
("Currency", Command.SCREEN, 2);
static final Command reasonCommand = new Command
("Reason", Command.SCREEN, 2);
Form mainForm = new Form ("TeleTransfer");
TextField receiverName = new TextField
("Receiver Name", "", 20, TextField.ANY);
TextField receiverAccount = new TextField
("Receiver Account#", "", 8, TextField.NUMERIC);
TextField amountWhole = new TextField ("Dollar", "", 6, TextField.NUMERIC);
TextField amountFraction = new TextField
("Cent", "", 2, TextField.NUMERIC);
CurrencyList currencyList = new CurrencyList (this);
TransferReason transferReason = new TransferReason (this);
Display display;
public TeleTransfer () {
mainForm.append (receiverName);
mainForm.append (receiverAccount);
mainForm.append (amountWhole);
mainForm.append (amountFraction);
mainForm.addCommand (currencyCommand);
mainForm.addCommand (reasonCommand);
mainForm.addCommand (sendCommand);
mainForm.addCommand (exitCommand);
mainForm.setCommandListener (this);
}
public void startApp () {
display = Display.getDisplay (this);
display.setCurrent (mainForm);
}
public void clear () {
receiverName.setString ("");
receiverAccount.setString ("");
amountWhole.setString ("");
amountFraction.setString ("");
transferReason.setString ("");
}
public void send () {
Alert alert = new Alert ("Send");
alert.setString ("transfer " + amountWhole.getString ()
+ "." + amountFraction.getString ()
+ " " + amountWhole.getLabel ()
+ "\nto Acc#" + receiverAccount.getString ()
+ "\nof " + receiverName.getString ());
alert.setTimeout (2000);
display.setCurrent (alert);
clear ();
}
public void pauseApp () {
}
public void destroyApp (boolean unconditional) {
}
public void back () {
display.setCurrent (mainForm);
}
public void commandAction (Command c, Displayable d) {
if (c == exitCommand) {
notifyDestroyed();
}
else if (c == sendCommand) {
sendTransferInformation ();
}
else if (c == clearCommand) {
resetTransferInformation ();
}
else if (c == currencyCommand) {
display.setCurrent (currencyList);
}
else if (c == reasonCommand) {
display.setCurrent (transferReason);
}
}
}
Low-Level API
In contrast to the high-level API, the low-level API allows full control of
the MID display at pixel level. For this purpose, the lcdui package
contains a special kind of screen called Canvas. The Canvas
itself does not provide any drawing methods, but it does provide a
paint() callback method similar to the paint() method in AWT
components. Whenever the program manager determines that it is necessary to draw
the content of the screen, the paint() callback method of
Canvas is called. The only parameter of the paint() method is
a Graphics object. In contrast to the lcdui high-level
classes, there are many parallels to AWT in the low-level API.
The Graphics object provides all the methods required for actually
drawing the content of the screen, such as drawLine() for drawing
lines, fillRect() for drawing a filled rectangular area or
drawstring() for drawing text strings.
In contrast to AWT, lcdui does not let you mix high-level and
low-level graphics. It is not possible to display high-level and low-level
components on the screen simultaneously.
The program manager knows that it must call the paint() method of
Canvas when the instance of Canvas is shown on the screen.
However, a repaint can also be triggered by the application at any time. By
calling the repaint() method of Canvas, the system is notified
that a repaint is necessary, and it will call the paint() method. The
call of the paint() method is not performed immediately; it may be
delayed until the control flow returns from the current event handling method.
The system may also collect several repaint requests before paint() is
actually called. This delay normally is not a problem, but when you're
doing animation, the safest way to trigger repaints is to use
Display.callSerially() or to request the repaint from a separate
Thread or TimerTask. Alternatively, the application can force
an immediate repaint by calling serviceRepaints(). (For more
information, see the section "Animation" at the end of this
chapter.)
The Canvas class also provides some input callback methods that are
called when the user presses or releases a key or touches the screen with the
stylus (if one is supported by the device).
Basic Drawing
Before we go into the details of user input or animation, we will start with
a small drawing example showing the concrete usage of the Canvas and
Graphics classes.
The example clears the screen by setting the color to white and filling a
rectangle the size of the screen, determined by calling getWidth() and
getHeight(). Then it draws a line from coordinates (0,0) to (100,200).
Finally, it draws a rectangle starting at (20,30), 30 pixels wide and 20 pixels
high:
import javax.microedition.lcdui.*;
class DrawingDemoCanvas extends Canvas {
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.setGrayScale (0);
g.drawLine (0, 0, 100, 200);
g.fillRect (20, 30, 30, 20);
}
}
As you can see in the example code, you create a custom class
DrawingDemoCanvas in order to fill the paint() method.
Actually, it is not possible to draw custom graphics without creating a new
class and implementing the paint() method.
In order to really see your Canvas implementation running, you still
need a corresponding MIDlet. Here's the missing code:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class DrawingDemo extends MIDlet {
public void startApp () {
Display.getDisplay (this).setCurrent (new DrawingDemoCanvas ());
}
public void pauseApp () {}
public void destroyApp (boolean forced) {}
}
Now you can start your DrawingDemo MIDlet. Depending on the screen
size of the device, it will create output similar to Figure
3.9. In most subsequent examples, you will omit the MIDlet since it is basically
the same as this one, except that the name of your Canvas class will
be different.
Figure 3.9 Output
of the DrawingDemo MIDlet.
In the example, the screen is cleared before drawing because the
system relies on the paint() method to fill every pixel of the draw
region with a valid value. You don't erase the previous content of the
screen automatically because doing so may cause flickering of animations. The
application cannot make any assumptions about the content of the Screen
before paint() is called. The screen may be filled with the content
drawn at the last call of paint(), but it may also be filled with an
alert box remaining from an incoming phone call, for example.
Drawing Style and Color
In the DrawingDemoCanvas implementation, you can find two calls to
setGrayScale(). The setGrayScale() method sets the gray scale
value for the following drawing operations. Valid grayscale values range from 0
to 255, where 0 means black and 255 means white. Not all possible values may
actually render to different gray values on the screen. If the device provides
fewer than 256 shades of gray, the best fitting value supported by the device is
chosen. In the example, the value is first set to white, and the screen is
cleared by the following call to drawRect(). Then, the color is set to
black for the subsequent drawing operations.
The setGrayScale() method is not the only way to influence the color
of subsequent drawing. MIDP also provides a setColor() method. The
setColor() method has three parameters holding the red, green, and blue
components of the desired color. Again, the values range from 0 to 255, where
255 means brightest and 0 means darkest. If all three parameters are set to the
same value, the call is equivalent to a corresponding call of
setGrayScale(). If the device is not able to display the desired color,
it chooses the best fitting color or grayscale supported by the device
automatically. Some examples are listed in Table 3.7.
Table 3.7 Example Color Parameter Settings
|
Parameter Settings
|
Resulting Color
|
|
setColor (255, 0, 0)
|
Red
|
|
setColor (0, 255, 0)
|
Green
|
|
setColor (0, 0, 255)
|
Blue
|
|
setColor (128, 0, 0)
|
Dark red
|
|
setColor (255, 255, 0)
|
Yellow
|
|
setColor (0, 0, 0)
|
Black
|
|
setColor (255, 255, 255)
|
White
|
|
setColor (128, 128, 128)
|
50% gray
|
The only other method that influences the current style of
drawing is the setStrokeStyle() method. The setStrokeStyle()
command sets the drawing style of lines to dotted or solid. You determine the
style by setting the parameter to one of the constants DOTTED or
SOLID, defined in the Graphics class.
When the paint() method is entered, the initial drawing color is
always set to black and the line style is SOLID.
Simple Drawing Methods
In the example, you have already seen fillRect() and
drawLine(). Table 3.8 shows all drawing primitives contained in the
Graphics class. All operations where the method names begin with
draw, except drawstring() and drawImage(), are
influenced by the current color and line style. They draw the outline of a
figure, whereas the fill methods fill the corresponding area with the
current color and do not depend on the line style.
Table 3.8 Drawing Methods of the Graphics Class
|
Method
|
Purpose
|
|
drawImage (Image image,
|
Draws an Image. Explained in detail in the int x, int y, int align)
"Images" section.
|
|
drawString (String text,
|
Draws a text string at the given position in the int x, int y, int
align) current color; see "Text and Fonts."
|
|
drawRect (int x, int y,
|
Draws an empty rectangle with the upper-left int w, int h) corner
at the given (x,y)coordinate, with the given width and a height. The next
section explains why the rectangle is one pixel larger than you might
expect.
|
|
drawRoundRect (int x, int y,
|
Like drawRect(), except that an additional radius int w, int
h, int r) is given for rounded corners of the rectangle.
|
|
drawLine (int x0, int y0,
|
Draws a line from (x0,y0) to (x1,y1). int x1, int y1)
|
|
drawArc (int x, int y, Draws the outline of a circular or elliptical
arc int w, int h,
|
covering the specified rectangle, using the current int startAng, int
arcArc) color and stroke style. The resulting arc begins at
startAng and extends for arcAng degrees. Angles are
interpreted such that 0 degrees is at the 3 o'clock position. A positive
value indicates a counter-clockwise rotation while a negative value indicates a
clockwise rotation.
|
|
fillRect (int x, int y,
|
Similar to drawRect(), but fills the given area int w,
int h) with the current color.
|
|
fillRoundRect (int x, int y,
|
Related to fillRect() as drawRoundRect() is int w, int
h, related to drawRect(). int startAng, int endAng);
|
|
fillArc (int x, int y,
|
Like drawArc(), but fills the corresponding region. int w, int
h, int startAng, int endAng);
|
Coordinate System and Clipping
In the drawing example, we already have used screen coordinates without explaining
what they actually mean. You might know that the device display consists of
little picture elements (pixels). Each of these pixels is addressed by its position
on the screen, measured from the upper-left corner of the device, which is the
origin of the coordinate system. Figure 3.10 shows the lcdui coordinate system.
Actually, in Java the coordinates do not address the pixel itself, but the
space between two pixels, where the "drawing pen" hangs to the lower
right. For drawing lines, this does not make any difference, but for rectangles
and filled rectangles this results in a difference of one pixel in width and
height: In contrast to filled rectangles, rectangles become one pixel wider and
higher than you might expect. While this may be confusing at first glance, it
respects the mathematical notation that lines are infinitely thin and avoids
problems when extending the coordinate system to real distance measures, as in
the J2SE class Graphics2D.
Figure 3.10 The lcdui
coordinate system.
In all drawing methods, the first coordinate (x) denotes the
horizontal distance from the origin and the second coordinate (y) denotes the
vertical distance. Positive coordinates mean a movement down and to the right.
Many drawing methods require additional width and height parameters. An
exception is the drawLine() method, which requires the absolute
coordinates of the destination point.
The origin of the coordinate system can be changed using the
translate() method. The given coordinates are added to all subsequent
drawing operations automatically. This may make sense if addressing coordinates
relative to the middle of the display is more convenient for some applications,
as shown in the section "Scaling and Fitting," later in the chapter.
The actual size of the accessible display area can be queried using the
getWidth() and getHeight() methods, as performed in the first
example that cleared the screen before drawing. The region of the screen where
drawing takes effect can be further limited to a rectangular area by the
clipRect() method. Drawing outside the clip area will have no effect.
The following example demonstrates the effects of the clipRect()
method. First, a dotted line is drawn diagonally over the display. Then a
clipping region is set. Finally, the same line as before is drawn using the
SOLID style:
import javax.microedition.lcdui.*;
class ClipDemoCanvas extends Canvas {
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
int m = Math.min (getWidth (), getHeight ());
g.setGrayScale (0);
g.setStrokeStyle (Graphics.DOTTED);
g.drawLine (0, 0, m, m);
g.setClip (m / 4, m / 4, m / 2, m / 2);
g.setStrokeStyle (Graphics.SOLID);
g.drawLine (0, 0, m, m);
}
}
Figure 3.11 shows the resulting image. Although
both lines have identical start and end points, only the part covered by the
clipping area is replaced by a solid line.
Figure 3.11 Output
of the clipRect() example: Only the part covered by the clipping area
is redrawn solid, although the line coordinates are identical.
When the paint() method is called from
the system, a clip area may already be set. This may be the case if the
application just requested repainting of a limited area using the parameterized
repaint call, or if the device just invalidated a limited area of the display,
for example if a pop-up dialog indicating an incoming call was displayed but did
not cover the whole display area.
Actually, clipRect() does not set a new clipping area, but instead
shrinks the current clip area to the intersection with the given rectangle. In
order to enlarge the clip area, use the setClip() method.
The current clip area can be queried using the getClipX(),
getClipY(), getClipWidth(), and getClipHeight()
methods. When drawing is computationally expensive, this information can be
taken into account in order to redraw only the areas of the screen that need an
update.
Text and Fonts
For drawing text, lcdui provides the method drawstring().
In addition to the basic drawstring() method, several variants let
you draw partial strings or single characters. (Details about the additional
methods can be found in the lcdui API documentation.) The simple drawstring()
method takes four parameters: The character string to be displayed, the x and
y coordinates, and an integer determining the horizontal and vertical alignment
of the text. The alignment parameter lets you position the text relative to
any of the four corners of its invisible surrounding box. Additionally, the
text can be aligned to the text baseline and the horizontal center. The sum
or logical or (|) of a constant for horizontal alignment (LEFT,
RIGHT, and HCENTER) and constants for vertical alignment (TOP,
BOTTOM, and BASELINE) determine the actual alignment. Figure
3.12 shows the anchor points for the valid constant combinations.
Figure 3.12 Valid
combinations of the alignment constants and the corresponding anchor points.
The following example
illustrates the usage of the drawstring() method. By choosing the
anchor point correspondingly, the text is displayed relative to the upper-left
and lower-right corner of the screen without overlapping the screen border:
import javax.microedition.lcdui.*;
class TextDemoCanvas extends Canvas {
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.setGrayScale (0);
g.drawString ("Top/Left", 0, 0, Graphics.TOP | Graphics.LEFT);
g.drawString ("Baseline/Center", getWidth () / 2, getHeight () / 2,
Graphics.HCENTER | Graphics.BASELINE);
g.drawString ("Bottom/Right", getWidth (), getHeight (),
Graphics.BOTTOM | Graphics.RIGHT);
}
}
Figure 3.13 shows the output of the TextDemo
example.
Figure 3.13 Output
of the TextDemo example.
In addition to the current drawing color, the result of the
drawstring() method is influenced by the current font. MIDP provides
support for three different fonts in three different sizes and with the three
different attributes: bold, italic, and underlined.
A font is not selected directly, but the setFont() method takes a
separate Font object, describing the desired font, as a parameter.
The explicit Font class provides additional information about the font,
such as its width and height in pixels, baseline position, ascent and descent,
and so on. Figure 3.14 illustrates the meaning
of the corresponding values. This information is important for operations such
as drawing boxes around text strings. In addition, word-wrapping algorithms
rely on the actual pixel width of character strings when rendered to the screen.
Figure 3.14 Font properties
and the corresponding query methods.
A Font object is created by calling the static method
createFont() of the class Font in the lcdui package.
The createFont() method takes three parameters: the font type, style,
and size of the font. Similar to the text alignment, there are predefined
constants for setting the corresponding value; these constants are listed in
Table 3.9.
Table 3.9 createFont() Property Constants
|
Property
|
Constants
|
|
Size
|
SIZE_SMALL, SIZE_MEDIUM, SIZE_LARGE
|
|
Style
|
STYLE_PLAIN, STYLE_ITALICS, STYLE_BOLD,
STYLE_UNDERLINED
|
|
Face
|
FACE_SYSTEM, FACE_MONOSPACE, FACE_PROPORTIONAL
|
The style constants can be combinedfor example,
STYLE_ITALICS | STYLE_BOLD will result in a bold italics font
style.
The following example shows a list of all fonts available, as far as the list
fits on the screen of the device:
import javax.microedition.lcdui.*;
class FontDemoCanvas extends Canvas {
static final int [] styles = {Font.STYLE_PLAIN,
Font.STYLE_BOLD,
Font.STYLE_ITALIC};
static final int [] sizes = {Font.SIZE_SMALL,
Font.SIZE_MEDIUM,
Font.SIZE_LARGE};
static final int [] faces = {Font.FACE_SYSTEM,
Font.FACE_MONOSPACE,
Font.FACE_PROPORTIONAL};
public void paint (Graphics g) {
Font font = null;
int y = 0;
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.setGrayScale (0);
for (int size = 0; size < sizes.length; size++) {
for (int face = 0; face < faces.length; face++) {
int x = 0;
for (int style = 0; style < styles.length; style++) {
font = Font.getFont
(faces [face], styles [style], sizes [size]);
g.setFont (font);
g.drawString
("Test", x+1, y+1, Graphics.TOP | Graphics.LEFT);
g.drawRect
(x, y, font.stringWidth ("Test")+1,
font.getHeight () + 1);
x += font.stringWidth ("Test")+1;
}
y += font.getHeight () + 1;
}
}
}
}
Figure 3.15 shows the output of the FontDemo
example.
Figure 3.15 Output
of the FontDemo example.
Images
The Graphics class also provides a method for drawing images. As
shown in the final version of TeleTransfer application, Images
can be predefined and contained in the JAR file of the MIDlet. The only file
format that is mandatory for MIDP is the Portable Network Graphics (PNG) file
format. The PNG format has several advantages over other graphics formats; for
example, it is license free and supports true color images, including a full
transparency (alpha) channel. PNG images are always compressed with a loss-less
algorithm. The algorithm is identical to the algorithm used for JAR files, so
the MIDP implementation can save space by using the same algorithm for both
purposes.
An image can be loaded from the JAR file using the static method
Image.create (String name). The name parameter denotes the
filename of the image in the JAR file. Please note that this create()
method may throw an IOException.
The drawImage() method in Graphics requires an Image
object, the coordinates, and an integer denoting the alignment as parameters.
The alignment parameter is similar the alignment of drawString(), except
that the BASELINE constant is not supported. An additional alignment
constant available for images only is VCENTER, which forces the image
to be vertically centered relative to the given coordinates. Figure
3.16 shows the valid constant combinations and the corresponding anchor
points.
Figure 3.16 Alignment
constant combinations valid for images and the corresponding anchor points.
The following example first loads the image logo.png from the MIDlet
JAR file in the constructor, and then displays the image three times. One image
is drawn in the upper-left corner, one in the lower-right corner, and one in
the center of the display, as shown in Figure 3.17:
import java.io.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
class ImageDemoCanvas extends Canvas {
Image image;
public ImageDemoCanvas () {
try {
image = Image.createImage ("/logo.png");
}
catch (IOException e) {
throw new RuntimeException ("Unable to load Image: "+e);
}
}
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.drawImage (image, 0, 0, Graphics.TOP | Graphics.LEFT);
g.drawImage (image, getWidth () / 2, getHeight () / 2,
Graphics.HCENTER | Graphics.VCENTER);
g.drawImage (image, getWidth (), getHeight (),
Graphics.BOTTOM | Graphics.RIGHT);
}
}
Figure 3.17 Output
of the ImageDemo example.
Images can also be created at runtime from scratch. The static
method Image.create (int width, int height) creates a new dynamic image
of the given size. In contrast to images loaded from a JAR file, these images
are mutable. Mutable images can be modified by calling getGraphics ().
The Graphics object returned can be used for modifying the image with
all the methods provided by the Graphics class. Please note that images
loaded from a JAR file cannot be modified. However, it is possible to create a
mutable image, and then draw any other image in the mutable image.
By modifying the constructor of the previous example canvas as follows, the
image drawn in the paint() method is created and filled at runtime
instead of loading an image from the JAR file:
public ImageDemoCanvas () {
image = Image.createImage (10,10);
image.getGraphics ().fillArc (0,0,10,10,0, 360);
}
The disadvantage of mutable images is that they cannot be used in high-level
GUI elements since it is possible to modify them at any time, possibly leading
to inconsistent display of widgets. For that reason, another static create
method, createImage(Image image), is provided that creates an immutable
image from another image.
Interaction
Because the Canvas class is a subclass of Displayable, it
provides the same support for commands as the high-level screen classes. Here,
you will concentrate on the additional interaction possibilities the
Canvas class offers: direct key input and pointer support.
Please note that all input events and command notifications and the
paint() method are called serially. That means that the application
manager will call none of the methods until the previous event handling method
has returned. So all these methods should return quickly, or the user will be
unable to interact with the application. For longer tasks, a separate thread can
be started.
Key Input
For key input, the Canvas class provides three callback methods:
keyPressed(), keyReleased(), and keyRepeated(). As
the names suggest, keyPressed() is called when a key is pressed,
keyRepeated() is called when the user holds down the key for a longer
period of time, and keyReleased() is called when the user releases the
key.
All three callback methods provide an integer parameter, denoting the Unicode
character code assigned to the corresponding key. If a key has no Unicode
correspondence, the given integer is negative. MIDP defines the following
constant for the keys of a standard ITU-T keypad: KEY_NUM0,
KEY_NUM1, KEY_NUM2, KEY_NUM3, KEY_NUM4,
KEY_NUM5, KEY_NUM6, KEY_NUM7, KEY_NUM8,
KEY_NUM9, KEY_POUND, and KEY_STAR. Applications
should not rely on the presence of any additional key codes. In particular,
upper- and lowercase or characters generated by pressing a key multiple times
are not supported by low-level key events. A "name" assigned to the
key can be queried using the getKeyName() method.
Some keys may have an additional meaning in games. For this purpose, MIDP
provides the constants UP, DOWN, LEFT,
RIGHT, FIRE, GAME_A, GAME_B,
GAME_C, and GAME_D. The "game" meaning of a keypress
can be determined by calling the getGameAction() method. The mapping
from key codes to game actions is device dependent, so different keys may map to
the same game action on different devices. For example, some devices may have
separate cursor keys; others may map the number pad to four-way movement. Also,
several keys may be mapped to the same game code. The game code can be
translated back to a key code using the getKeyCode() method. This also
offers a way to get the name of the key assigned to a game action. For example,
the help screen of an application may display
"press "+getKeyName (getKeyCode (GAME_A))
instead of "press GAME_A".
The following canvas implementation shows the usage of the key event methods.
For each key pressed, repeated, or released, it shows the event type, character
and code, key name, and game action.
The first part of the implementation stores the event type and code in two
variables and schedules a repaint whenever a key event occurs:
import javax.microedition.lcdui.*;
class KeyDemoCanvas extends Canvas {
String eventType = "- Press any!";
int keyCode;
public void keyPressed (int keyCode) {
eventType = "pressed";
this.keyCode = keyCode;
repaint ();
}
public void keyReleased (int keyCode) {
eventType = "released";
this.keyCode = keyCode;
repaint ();
}
public void keyRepeated (int keyCode) {
eventType = "repeated";
this.keyCode = keyCode;
repaint ();
}
The second part prints all event properties available to the device screen.
For this purpose, you first implement an additional write() method that
helps the paint() method to identify the current y position on the
screen. This is necessary because drawText() does not advance to a new
line automatically. The write() method draws the string at the given y
position and returns the y position plus the line height of the current font, so
paint() knows where to draw the next line:
public int write (Graphics g, int y, String s) {
g.drawString (s, 0, y, Graphics.LEFT|Graphics.TOP);
return y + g.getFont ().getHeight ();
}
The paint() method analyzes the keyCode and prints the result
by calling the write() method defined previously, as shown in Figure
3.18:
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.setGrayScale (0);
int y = 0;
y = write (g, y, "Key "+ eventType);
if (keyCode == 0) return;
y = write (g, y, "Char/Code: "+ ((keyCode < 0) ? "N/A" : ""
+(char) keyCode) + "/" + keyCode);
y = write (g, y, "Name: "+getKeyName (keyCode));
String gameAction;
switch (getGameAction (keyCode)) {
case LEFT: gameAction = "LEFT"; break;
case RIGHT: gameAction = "RIGHT"; break;
case UP: gameAction = "UP"; break;
case DOWN: gameAction = "DOWN"; break;
case FIRE: gameAction = "FIRE"; break;
case GAME_A: gameAction = "GAME_A"; break;
case GAME_B: gameAction = "GAME_B"; break;
case GAME_C: gameAction = "GAME_C"; break;
case GAME_D: gameAction = "GAME_D"; break;
default: gameAction = "N/A";
}
write (g, y, "Action: "+gameAction);
}
}
Figure 3.18 Output
of the KeyDemo example when the "Fire" key was released.
Pointer
Events
For devices supporting a pointer device such as a stylus, touch screen, or
trackball, the Canvas class provides three notification methods:
pointerPressed(), pointerDragged(), and
pointerReleased(). These methods work similarly to the key event
methods, except that they provide two integer parameters, denoting the x and y
position of the pointer when the corresponding event occurs. (Please note that
pointer support is optional in MIDP, so the application should not rely on the
presence of a pointer. Such devices are uncommon for devices such as mobile
phones.) The following sample program demonstrates the usage of the three
methods:
import javax.microedition.lcdui.*;
class PointerDemoCanvas extends Canvas {
String eventType = "Press Pointer!";
int x;
int y;
public void pointerPressed (int x, int y) {
eventType = "Pointer Pressed";
this.x = x;
this.y = y;
repaint ();
}
public void pointerReleased (int x, int y) {
eventType = "Pointer Released";
this.x = x;
this.y = y;
repaint ();
}
public void pointerDragged (int x, int y) {
eventType = "Pointer Repeated";
this.x = x;
this.y = y;
repaint ();
}
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
g.setGrayScale (0);
g.drawString (eventType + " " +x +"/"+y,
0, 0, Graphics.TOP|Graphics.LEFT);
g.drawLine (x-4, y, x+4, y);
g.drawLine (x, y-4, x, y+4);
}
}
Foreground and Background Notifications
For several reasons, the Canvas may move into the
backgroundfor example, if the display is set to another displayable object
or if the device displays a system dialog. In these cases, the Canvas
is notified by the hideNotify() method. When the Canvas
becomes visible (again), the corresponding counterpart, showNotify(),
is called.
Javagochi Example
Now that you are familiar with the Canvas object and the basic
drawing methods of the Graphics class, you are ready to develop a small
interactive application, the Javagochi.
As you can see in the following code, the MIDlet implementation of
Javagochi is already finished, but the Face class is missing:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class Javagochi extends MIDlet {
static final int IDEAL_WEIGHT = 100;
Display display = Display.getDisplay (this);
Face face = new Face (this);
int weight = IDEAL_WEIGHT;
Timer consumption;
int score;
Before you begin development, let us first say a few words about the
Javagochi itself. A Javagochi has a weight that is
initialized with its IDEAL_WEIGHT. It also owns an instance of
Display, Face, and Consumption, which will be
explained later. Finally, it stores a score value for the care the owner spends
on the Javagochi.
The happiness of the Javagochi is determined by the deviation of its
current weight from the ideal weight, ranging from 10 to 0:
public int getHappiness () {
return 20 - (weight > IDEAL_WEIGHT
? 10 * weight / IDEAL_WEIGHT
: 10 * IDEAL_WEIGHT / weight);
if (happiness < 0) happiness = 0;
if (happiness > 10) happiness = 10;
}
This formula also demonstrates how to circumvent problems with the absence of
floating point arithmetic. In order to avoid loss of significant fractions, the
values are scaled up before division.
Like all other known life forms, the Javagochi can die.
Javagochies only die from sadness when their happiness level reaches
zero:
public boolean isDead () {
return getHappiness <= 0;
}
The only other action a Javagochi can perform besides dying is to
transform energy to matter and back. Since a weight change may change the
Javagochi's look, a repaint is requested in the
transform() method:
public void transform (int amount) {
if (!isDead ()) {
weight += amount;
face.repaint ();
}
}
When the Javagochi MIDlet is started, it displays itself and starts
a consumption Timer that keeps track of the power the
Javagochi needs for living:
public void startApp () {
display.setCurrent (face);
consumption = new Consumption (this).start ();
}
When the MIDlet is paused, the Javagochi goes to sleep by telling
the consumption thread to terminate itself. The destroyApp() method
does nothing because the life cycle will enter sleep anyway, and no further
cleanup is needed:
public void pauseApp () {
consumption.leave = true;
}
public void destroyApp (boolean forced) {
}
}
The consumption Thread is a separate class that monitors the power
the Javagochi needs for living. In the run() method, every 0.5
seconds the score is updated depending on the Javagochi's
happiness and the small amount of body mass that is transformed back to life
energy:
public class Consumption extends Thread {
Javagochi javagochi;
boolean leave = false;
public Consumption (Javagochi javagochi) {
this.javagochi = javagochi;
}
public void run () {
while (!leave) {
try {
sleep (500);
}
catch (InterruptedException e) {break;}
javagochi.score += 10 - javagochi.deviation;
javagochi.transform (-5);
}
}
}
Now that you know how a Javagochi works, it is your job to give the
Javagochi an appropriate appearance by implementing the missing
Face class.
Scaling and Fitting
In many cases, it is a good idea to scale displayed graphics depending on the
actual screen size. Otherwise, the display will look nice on one particular
device type, but won't fit the screen on devices with a lower screen
resolution or become unnecessarily small on devices with higher screen
resolutions.
We will now show how scaling works for the Javagochi example. A picture
of a Javagochi is shown in Figure 3.19.
You will start by drawing the shape of the face, a simple ellipse. In this case,
the ellipse will reflect the Javagochi's weight. If the Javagochi
is at its ideal weight, the ellipse becomes a circle.
Figure 3.19 A happy
Javagochi at its ideal weight.
In order to leave some space for the Javagochi to grow,
the diameter of the ideal circle is half the minimum of the screen width and
height. Thus, the height of the Javagochi is calculated using the
following formula:
int height = Math.min (getHeight (), getWidth ()) / 2;
Based on the current weight, the ideal weight, and the calculated height,
which is also the diameter of the "ideal" Javagochi, you can
now calculate the width of the Javagochi:
int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;
Other applications may of course have other dependencies from the actual
screen size, but this example should be sufficient to show the general idea.
The Javagochi's skin color is dependent on its happiness. If
the Javagochi feels well, its skin has a bright yellow color. With
decreasing happiness, the Javagochi becomes pale. This is reflected by
the following setColor() command:
setColor (255, 255, 28 * javagochi.happiness);
Using the given width and height, you can now implement your first version of
the Javagochi's Face class:
import javax.microedition.lcdui.*;
class Face extends Canvas implements CommandListener {
Javagochi javagochi;
Face (Javagochi javagochi) {
this.javagochi = javagochi;
}
public void paint (Graphics g) {
g.setColor (255, 255, 255);
g.fillRect (0, 0, getWidth (), getHeight ());
int height = Math.min (getHeight (), getWidth ()) / 2;
int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;
g.translate (getWidth () / 2, getHeight () / 2);
g.setColor (255, 255, 255 - javagochi.getHappiness () * 25);
g.fillArc (- width / 2, - height / 2, width, height, 0, 360);
g.setColor (0, 0, 0);
g.drawArc (- width / 2, - height / 2, width, height, 0, 360);
}
}
In order to simplify the centered display of the Javagochi, you set
the origin of the coordinate system to the center of the screen using the
translate() method. The outline of the Javagochi's face
is then drawn using the drawArc() method.
Unfortunately, the outline of the Javagochi looks a bit boring, so
you will add a simple face now. In order to avoid duplicated code, you put the
drawing of the eyes in a separate method. The drawEye() method takes
the Graphics object, the coordinates of the eye, and a size
parameter:
void drawEye (Graphics g, int x, int y, int size) {
if (javagochi.isDead ()) {
graphics.drawLine (x - size/2, y, x + size/2, y);
graphics.drawLine (x, y - size/2, x, y + size/2);
}
else
graphics.drawArc (x-size/2, y-size/2, size, size, 0, 360);
}
Now you can insert the rest of the drawing code into the paint()
method, just after drawArc(). You will start with the eyes by calling
the drawEye() method defined previously. By using fractions of the
current width and height of the Javagochi, the eyes are positioned and
sized correctly:
drawEye (g, - width / 6, - height / 5, height / 15 + 1);
drawEye (g, width / 6, - height / 5, height / 15 + 1);
Now you draw the mouth, depending on the current happiness of the
Javagochi. Again, you use fractions of the Javagochi size for
positioning and sizing:
switch (javagochi.getHappiness () / 3) {
case 0:
case 1: g.drawArc (-width/6, height/7, width/3, height/6, 0, 180); break;
case 2: g.drawLine (-width/6, height/7, width/6, height/7); break;
default: g.drawArc (-width/6, height/7, width/3, height/6, 0, -180);
}
Simple Interaction
When you run the first version of the Javagochi application, the
Javagochi starts out happy, but dies quickly from starvation.
Obviously, you need a way to transfer energy from the device's battery to
the Javagochi. One possibility would be to add a corresponding
command.
However, in the "High-Level API" section you learned that commands
may be delegated to a sub-menu. When the Javagochi urgently needs
feeding, you would like to be able to react quickly.
So you just use the key event corresponding to the game action FIRE
for feeding the Javagochi:
public void keyPressed (int keyCode) {
if (getGameAction (keyCode) == FIRE)
javagochi.transform (10);
}
Now you can save the Javagochi from starvation using the
FIRE game key.
Canvas and Text Input
As mentioned in the introduction to interaction, it is not possible to
receive composed key events using the low-level API. But what can you do if you
need this kind of input, such as for a text input trainer?
Let's just assume simple feeding is not enough for your
Javagochi. Depending on its current state, it needs special vitamins,
denoted by letters ranging from A to Z. On phones providing keys 0 through 9
only, this is a problem. The only solution is to emulate the key input mechanism
in software. On cellular phones, there are also three to four letters printed on
the number keys. In text input mode, pressing a number makes the first letter
appear. If the same number is pressed again in a limited period of time, the
second letter appears instead of the first one. This way you can cycle through
all the letters on a number key. When no key is pressed for about three quarters
of a second, or another key is pressed, the letter currently displayed is
confirmed as input key.
For emulation of this mechanism, you define the letters on the keys 2 through
9 in a String array inside the Face class:
public static final String[] keys = {"abc", "def", "ghi", "jkl",
"mno", "pqrs", "tuv", "wxyz"};
You also need a timer to measure the time until confirmation of the current
key. The timer is stored in keyTimer. The variables keyMajor
and keyMinor contain the index in the keys array and the index inside
the corresponding string. The variable needed stores the vitamin
currently needed by the Javagochi:
Timer keyTimer;
int keyMajor = -1;
int keyMinor;
char needed = 'a';
What do you do if a numeric key is pressed? If you already have a timer
running, you cancel it since a key was pressed. Then, you subtract the code of
the 2 key from the current key code in order to calculate the index in your key
array. If the given event does not represent a numeric key between 2 and 9, you
set keyMajor to the special value 1, denoting that no valid
character is being entered. Otherwise, you check whether the key is identical to
the last key. If so, keyMinor is incremented in order to cycle through
the letters assigned to a single numeric key. If another key is pressed,
keyMajor is changed accordingly and keyMinor is set back to 0.
A new timer is scheduled for half a second later:
public synchronized void keyPressed (int keyCode) {
if (keyTimer != null) keyTimer.cancel ();
int index = keyCode - KEY_NUM2;
if (index < 0 || index > keys.length)
keyMajor = -1;
else {
if (index != keyMajor) {
keyMinor = 0;
keyMajor = index;
}
else {
keyMinor++;
if (keyMinor >= keys [keyMajor].length ())
keyMinor = 0;
}
keyTimer = new Timer ();
keyTimer.schedule (new KeyConfirmer (this), 500);
}
repaint ();
}
Now you need to implement a timer task that confirms the letter if no other
key is pressed for half a second. In that case, the KeyConfirmer class
just calls keyConfirmed() in the original Face class:
import java.util.*;
public class KeyConfirmer extends TimerTask {
Face face;
public KeyConfirmer (Face face) {
this.face = face;
}
public void run () {
face.keyConfirmed ();
}
}
Back in the Face class, you can now implement the functionality
performed when the letter is finally confirmed. You just compare the letter to
the vitamin needed by the Javagochi. If the right vitamin is fed, the
weight of the Javagochi is increased 10 units by calling
transform():
synchronized void keyConfirmed () {
if (keyMajor != -1) {
if (keys [keyMajor].charAt (keyMinor) == needed) {
javagochi.score += javagochi.getHappiness ();
if (!javagochi.isDead ())
needed = (char) ('a'
+ ((System.currentTimeMillis () / 10) % 26));
javagochi.transform (10);
}
keyMajor = -1;
repaint ();
}
}
}
Finally, you add some status information about the current score and selected
key to the Face.paint() method. Just insert the following code at the
end of the previous implementation of paint():
String keySelect = "";
if (keyMajor != -1) {
String all = keys [keyMajor];
keySelect = all.substring (0, keyMinor) + "[" + all.charAt (keyMinor)
+ "]" + all.substring (keyMinor+1);
}
g.drawString ("Feed: " + needed + " " + keySelect, 0,
getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER);
g.drawString ("Score: "+javagochi.score, 0,
-getHeight ()/2, Graphics.TOP|Graphics.HCENTER);
Figure 3.20 shows the Javagochi
being fed with vitamins. The complete source code is contained in Listing 3.2.
Figure 3.20 A Javagochi
being fed with vitamins.
Listing 3.2 Javagochi.javaThe Complete Javagochi Sample
Source Code
import java.util.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
class Consumption extends TimerTask {
Javagochi javagochi;
public Consumption (Javagochi javagochi) {
this.javagochi = javagochi;
}
public void run () {
javagochi.transform (-1 - javagochi.score/100 );
}
}
class KeyConfirmer extends TimerTask {
Face face;
public KeyConfirmer (Face face) {
this.face = face;
}
public void run () {
face.keyConfirmed ();
}
}
class Face extends Canvas {
public static final String[] keys = {"abc", "def", "ghi", "jkl",
"mno", "pqrs", "tuv", "wxyz"};
Javagochi javagochi;
Timer keyTimer;
int keyMajor = -1;
int keyMinor;
char needed = 'a';
Face (Javagochi javagochi) {
this.javagochi = javagochi;
}
public void paint (Graphics g) {
g.setColor (255, 255, 255);
g.fillRect (0, 0, getWidth (), getHeight ());
int height = Math.min (getHeight (), getWidth ()) / 2;
int width = height * javagochi.weight
/ javagochi.IDEAL_WEIGHT;
g.translate (getWidth () / 2, getHeight () / 2);
g.setColor (255, 255, 255 - javagochi.getHappiness () * 25);
g.fillArc (- width / 2, - height / 2, width, height, 0, 360);
g.setColor (0, 0, 0);
g.drawArc (- width / 2, - height / 2, width, height, 0, 360);
g.drawString ("Score: "+javagochi.score, 0, -getHeight ()/2,
Graphics.TOP|Graphics.HCENTER);
String keySelect = "";
if (keyMajor != -1) {
String all = keys [keyMajor];
keySelect = all.substring
(0, keyMinor) + "[" + all.charAt (keyMinor)
+ "]" + all.substring (keyMinor+1);
}
g.drawString ("Feed: " + needed + " " + keySelect,
0, getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER);
drawEye (g, - width / 6, - height / 5, height / 15 + 1);
drawEye (g, width / 6, - height / 5, height / 15 + 1);
switch (javagochi.getHappiness () / 3) {
case 0:
case 1:
g.drawArc (-width/6, height/7, width/3, height/6, 0, 180);
break;
case 2:
g.drawLine (-width/6, height/7, width/6, height/7);
break;
default:
g.drawArc (-width/6, height/7, width/3, height/6, 0, -180);
}
}
void drawEye (Graphics graphics, int x0, int y0, int w) {
if (javagochi.isDead ()) {
graphics.drawLine (x0 - w/2, y0, x0 + w/2, y0);
graphics.drawLine (x0, y0 - w/2, x0, y0 + w/2);
}
else
graphics.fillArc (x0-w/2, y0-w/2, w, w, 0, 360);
}
public synchronized void keyPressed (int keyCode) {
int index = keyCode - KEY_NUM2;
if (keyTimer != null) keyTimer.cancel ();
if (index < 0 || index > keys.length)
keyMajor = -1;
else {
if (index != keyMajor) {
keyMinor = 0;
keyMajor = index;
}
else {
keyMinor++;
if (keyMinor >= keys [keyMajor].length ())
keyMinor = 0;
}
keyTimer = new Timer ();
keyTimer.schedule (new KeyConfirmer (this), 500);
}
repaint ();
}
synchronized void keyConfirmed () {
if (keyMajor != -1) {
if (keys [keyMajor].charAt (keyMinor) == needed) {
javagochi.score += javagochi.getHappiness ();
if (!javagochi.isDead ())
needed = (char) ('a'
+ ((System.currentTimeMillis () / 10) % 26));
javagochi.transform (10);
}
keyMajor = -1;
repaint ();
}
}
}
public class Javagochi extends MIDlet {
static final int IDEAL_WEIGHT = 100;
Display display;
Face face = new Face (this);
int weight = IDEAL_WEIGHT;
Timer consumption;
int score;
public int getHappiness () {
int happiness = 20 - (weight > IDEAL_WEIGHT
? 10 * weight / IDEAL_WEIGHT
: 10 * IDEAL_WEIGHT / weight);
if (happiness < 0) happiness = 0;
else if (happiness > 10) happiness = 10;
return happiness;
}
public boolean isDead () {
return getHappiness () == 0;
}
public void transform (int amount) {
if (!isDead ()) {
weight += amount;
face.repaint ();
}
}
public void startApp () {
display = Display.getDisplay (this);
display.setCurrent (face);
consumption = new Timer ();
consumption.scheduleAtFixedRate (new Consumption (this), 500, 500);
}
public void pauseApp () {
consumption.cancel ();
}
public void destroyApp (boolean forced) {
}
}
Animation
With animation, there are normally two main problems: Display
flickering and synchronization of painting with calculation of new frames. We
will first address how to get the actual painting and application logic in sync,
and then solve possible flickering.
Synchronization of Frame Calculation and
Drawing
When you perform animations, you can first calculate the display content and
then call repaint() in order to paint the new frame. But how do you
know that the call to paint() has finished? One possibility would be to
call serviceRepaints(), which blocks until all pending display updates
are finished. The problem with serviceRepaints() is that
paint() may be called from another thread. If the thread calling
serviceRepaints() holds any locks that are required in
paint(), a deadlock may occur. Also, calling serviceRepaints()
makes sense only from a thread other than the event handling thread. Otherwise,
key events may be blocked until the animation is over. An alternative to
serviceRepaints() is calling callSerially() at the end of the
paint() method. The callSerially() method lets you put
Runnable objects in the event queue. The run() method of the
Runnable object is then executed serially like any other event handling
method. In the run() method, the next frame can be set up, and a new
repaint can be requested there.
To demonstrate this execution model, you will build a simple stopwatch that
counts down a given number of seconds by showing a corresponding pie slice using
the fillArc() method, as shown in Figure 3.21.
Figure 3.21 A very
simple stopwatch.
The
Canvas implementation stores the current slice in degree, the start
time, the total amount of seconds and the MIDlet display in local variables. In
order to make use of callSerially(), your Canvas implements
the Runnable interface:
class StopWatchCanvas extends Canvas implements Runnable {
int degree = 360;
long startTime;
int seconds;
Display display;
When the StopWatchCanvas is created, you store the given display and
seconds. Then, the current time is determined and stored, too:
StopWatchCanvas (Display display, int seconds) {
this.display = display;
this.seconds = seconds;
startTime = System.currentTimeMillis ();
}
In the paint() method, you clear the display. If you need to draw
more than 0 degrees, you fill a corresponding arc with red color and request
recalculation of the pie slice using callSerially(). Finally, you draw
the outline of the stopwatch by setting the color to black and calling
drawArc():
public void paint (Graphics g) {
g.setGrayScale (255);
g.fillRect (0, 0, getWidth (), getHeight ());
if (degree > 0) {
g.setColor (255, 0, 0);
g.fillArc (0,0, getWidth (), getHeight (), 90, degree);
display.callSerially (this);
}
g.setGrayScale (0);
g.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360);
}
This method is invoked by the event handling thread as a result of the
previous display.callSerially(this) statement. In this case, it just
calculates a new pie slice and requests a repaint():
public void run () {
int permille = (int) ((System.currentTimeMillis ()
- startTime) / seconds);
degree = 360 - (permille * 360) / 1000;
repaint ();
}
}
As always, you need a MIDlet to actually display your
StopWatchCanvas implementation. The following code creates a stopwatch
set to 10 seconds when the application is started:
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class StopWatch extends MIDlet {
public void startApp () {
Display display = Display.getDisplay (this);
display.setCurrent (new StopWatchCanvas (display, 10));
}
public void pauseApp () {
}
public void destroyApp (boolean forced) {
}
}
Avoiding Flickering
On some devices, the stopwatch implementation will flicker. This is due to
the fact that the display is cleared completely before a new stopwatch is drawn.
However, on some other devices, the stopwatch will not flicker because those
devices provide automated double buffering. Before the screen is updated, all
drawing methods are performed in a hidden buffer area. Then, when the
paint() method is finished, the complete display is updated from the
offscreen buffer at once. The method isDoubleBuffered() in the
Canvas class is able to determine whether the device screen is double
buffered.
In order to avoid flickering of your animation in all cases, you can add your
own offscreen image, which is allocated only if the system does not provide
double buffering:
Image offscreen = isDoubleBuffered () ? null :
Image.createImage (getWidth (), getHeight ());
In the paint() method, you just check if the offscreen image is not
null, and if so, you delegate all drawing to your offscreen buffer. The
offscreen buffer is then drawn immediately at the end of the paint()
method, without first clearing the screen. Clearing the screen is not necessary
in that case since the offscreen buffer was cleared before drawing and it fills
every pixel of the display:
public void paint (Graphics g) {
Graphics g2 = offscreen == null ? g : offscreen.getGraphics ();
g2.setGrayScale (255);
g2.fillRect (0, 0, getWidth (), getHeight ());
if (degree > 0) {
g2.setColor (255, 0, 0);
g2.fillArc (0,0, getWidth (), getHeight (), 90, degree);
display.callSerially (this);
}
g2.setGrayScale (0);
g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360);
if (offscreen != null)
g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT);
}
Listing 3.3 gives the complete source code for the buffered stopwatch.
Listing 3.3 BufferedStopWatch.java The Complete Source Code of
the Buffered Stopwatch
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
class BufferedStopWatchCanvas extends Canvas implements Runnable {
int degree = 360;
long startTime;
int seconds;
Display display;
Image offscreen;
BufferedStopWatchCanvas (Display display, int seconds) {
this.display = display;
this.seconds = seconds;
if (!isDoubleBuffered () && false)
offscreen = Image.createImage (getWidth (), getHeight ());
startTime = System.currentTimeMillis ();
}
public void paint (Graphics g) {
Graphics g2 = offscreen == null
? g
: offscreen.getGraphics ();
g2.setGrayScale (255);
g2.fillRect (0, 0, getWidth (), getHeight ());
if (degree > 0) {
g2.setColor (255, 0, 0);
g2.fillArc (0,0, getWidth (), getHeight (), 90, degree);
display.callSerially (this);
}
g2.setGrayScale (0);
g2.drawArc (0, 0, getWidth ()-1, getHeight ()-1, 0, 360);
if (offscreen != null)
g.drawImage (offscreen, 0, 0, Graphics.TOP | Graphics.RIGHT);
}
public void run () {
int permille = (int) ((System.currentTimeMillis ()
- startTime) / seconds);
degree = 360 - (permille * 360) / 1000;
repaint ();
}
}
public class BufferedStopWatch extends MIDlet {
public void startApp () {
Display display = Display.getDisplay (this);
display.setCurrent (new BufferedStopWatchCanvas (display, 10));
}
public void pauseApp () {
}
public void destroyApp (boolean forced) {
}
}
Summary
In this chapter, you learned the general life cycle of MIDP applications. You
know how to build a user interface using the high-level lcdui widgets,
and how to interact using the listener mechanism. You have learned to perform
custom graphics using the low-level API, including device-independent
flicker-free animation and coordination of graphics calculation and drawing.
The next chapter gives a corresponding overview of the PDAP life cycle and
user interface. The PDAP introduction focuses on the differences between the
J2SE AWT classes and the subset included in PDAP, but still gives a basic
introduction to AWT programming.
About the Authors
Michael Kroll and Stefan Haustein are the creators of the kAWT-Project, an abstract window toolkit designed to allow graphical J2ME programming on devices using the KVM. Since the first release of KVM, both have been members of the Expert group specifying the J2ME PDA Profile.
Michael Kroll's experience in professional J2ME programming includes an application for viewing biosignals like ECGs and EEGs on a Palm organizer. Michael is working for Visus Technology Transfer in Germany developing a Java based radiology imaging system called JiveX.
Stefan Haustein studied Computer Science at the University of Dortmund, and is working on his Ph.D in the AI-Unit. He wrote his diploma thesis in the Neuros-Project at the "Institut fur Neuroinformatik" at the University of Bochum about graph-based robot navigation.
Source of this material
 |
This is Chapter 3: MIDP Programming from the book J2ME Application Development (ISBN:0-672-323095-9) written by Michael Kroll and Stefan Haustein, published by Sams Publishing.
© Copyright Pearson Education. All rights reserved. |
To access the full Table of Contents for the book
Other Chapters from Sams Publishing:
Web Services and Flows (WSFL)
Overview of JXTA
Introduction to EJBs
Processing Speech with Java
The Java Database Control in BEA Weblogic
|