HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

A UI Framework for the MIDP Low-Level API, by Glen Cordrey

I recently had the opportunity to develop Java applications for Nextel's i85s cell phone, the first Java 2 Micro Edition-enabled cell phone in the U.S. In this article I describe some of the issues I encountered when developing the user interface and the framework I created to address those issues. The primary goals of this framework are to allow the development of custom components and to make it easy to assemble those components into screens.

J2ME, CLDC, and MIDP
The Java 2 Platform, Micro Edition (J2ME) includes virtual machine and application program interface (API) specifications for running Java applications in devices with limited computing resources, such as personal digital assistant devices (PDAs) and cellular phones. J2ME defines different configurations that in turn define profiles.

The Connected Limited Device Configuration (CLDC) is intended for devices that have limited (not always on) connectivity to the Internet. The CLDC includes three packages - java.io, java.lang, and java.util - that are subsets of the Java 2 Standard Edition packages of the same name, and the javax.io.microedition package, which is specific to the CLDC. The CLDC also defines the kilobyte virtual machine (KVM), a Java VM that specifically targets devices with limited computing resources. CLDC devices have between 160KB and 512KB of memory available for use by the Java platform.

The CLDC defines a number of profiles; however, the Mobile Information Device Profile (MIDP) is the only one that has been finalized as of the writing of this article. The MIDP targets devices such as cell phones and pagers, with a minimum screen size of 96x54 pixels. MIDP applications are referred to as MIDlets.

The MIDP contains package javax.microedition.rms for record management services, javax.microedition.midlet for MIDlet lifecycle management, and javax.microedition.lcdui containing user interface classes. Within the lcdui package the MIDP distinguishes between what it calls high-level and low-level user interface APIs. Figure 1 shows the primary classes in these APIs.

figure 1
Figure 1: The primary MIDP high-level API classes are in white and the low-level API classes in blue

The High-Level API
In the high-level API, Screen is the base class for screen displays and has four subclasses - Alert, Form, List, and TextBox. Other than a title and a ticker (a string that continuously scrolls across a part of the screen), which are available to all Screen subclasses, List and TextBox each provide only a single visual element on the screen - that is, the structures of List and TextBox are fixed and other components can't be added to them. List displays a list of choices, with each choice consisting of a string and an optional image. TextBox displays and allows the entry and editing of text. The Alert class supports the display of both a string and an image. Form can contain and display any mix of Item objects - one or more ChoiceGroup (containing radio buttons and check boxes), DateField, Gauge, ImageItem, StringItem, and/or TextField objects.

The high-level API targets applications for which portability is essential and relegates look and feel issues to the platforms that the application is deployed on. One price of this portability is that you have minimal control over layout and display attributes such as text fonts and color. In addition, with the high-level API, you can't intercept key events and therefore can't provide contextual filtering of data input.

An Example of High-Level API Limitations
I'll illustrate the limitations of the high-level APIs via the screen shown in Figure 2.

figure 2
Figure 2:  Screen for entry of expenses information

This screen lets users enter expense information into their cellular phone. An expense consists of an amount, an expense type such as airfare or hotel, and the date of the expense.

This screen was implemented using the low-level API because the following obstacles would be encountered if implementing with a high-level one:

  • Layout: The amount and date fields are centered on the screen, which is not possible with the high-level API because it doesn't allow you to specify the position of the elements. Also, with the high-level API, the layout for most devices is vertical, meaning that each focusable item (items that receive input) is always placed on a new line. This means that with the high-level API you can't create elements containing multiple data fields on the same line, such as the amount field in Figure 2.
  • Custom components: With the high-level API you can't create custom components such as the expense-type scroll list shown in Figure 2. With the high-level API such a custom component would need to extend Item and then override Item's paint method to paint the custom component on the screen. However, Item's paint method is declared with the package scope, so any class that extends Item needs to be in the javax.microedition.lcdui package.

    Even if you elected to place your custom component in this package, there would be no guarantee that Form's append method, which you would call to add the Item to the display, would treat it correctly, as stated in this response to a message posted to KVM-Interest@Java.Sun.com, Sun's mailing list for J2ME developers: "The high-level Screens like Form don't expose the low-level functions to paint your Item and take events. Most implementations will either not allow you to add a custom Item to a Form or it will only display the Label at best."

  • Processing key presses: While the high-level API's DateField class provides a display similar to the date field in Figure 2, DateField doesn't prevent the entry of invalid dates, such as a month greater than 12, or a day of 31 when the month is 4. Since code that uses the high-level API can't intercept key presses, there's no way for you to add such a filtering capability.
The Low-Level API
The low-level API provides far more control over the look and feel, but at the potential expense of portability (whether or not portability is sacrificed depends upon which specific features of the low-level API are used). In the low-level API, Canvas is the base class for all screen displays and Graphics provides methods for drawing on the screen (although Graphics is also used by the internals of the high-level API; meanwhile, in the high-level API, graphics objects are not accessible to the developer except when using an image). With graphics methods you can draw and fill rectangles and arcs, draw lines, and specify fonts and colors. You can also specify precisely where these elements are drawn on the display. However, the low-level API doesn't contain any predefined visual components such as text boxes or radio buttons. Therefore if you use the low-level API, you have to create your own visual components.

With the low-level API you can also process key events via the keyPressed method in Canvas, which is called by the KVM whenever a key is pressed. You can use this capability to provide the user with the ability to navigate between, enter data into, and select values from your custom components.

The UI Framework
The limitations of the high-level API and the degree of control available in the low-level API drove me to use the latter for my development, and to implement a framework that supports the creation, placement, and display of navigation between, and the processing of key presses by, custom components. A class diagram of the framework is provided in Figure 3. Since these classes are similar in function to some AWT classes, I've followed Swing's lead and given them names that begin with M (for MIDP), followed by the name of the similar AWT class. Also, methods that behave similarly to their AWT counterparts are named the same as in AWT, although the method signatures may differ.

figure 3
Figure 3:  Classes in the UI framework

The relationship between MContainer and MComponent is the core of the framework. MContainer (see Listing 1) defines the method add, which adds a component to the container. The code fragment below shows how the ExpenseItem class, which creates the screen in Figure 2, uses this method to add objects of a custom component to the screen display (for brevity I don't show the dollar sign and cents separators being drawn).

NumberField dollars =
new NumberField( 5 );
add(dollars, x, y);
NumberField cents =
new NumberField( 2 );
add(cents, x +
dollars.getWidth() +
3 /* spacer */, y);

Figure 4 shows the relationship between these custom classes - ExpenseItem extends MContainer and uses NumberField and MScrollList classes (both of which extend MComponent) for its visual elements.

figure 4
Figure 4: The classes that implement the expense item entry screen.

MComponent and MContainer
In exploring the relationship between MComponent and MContainer let's start by examining the code for MComponent, shown in Listing 2. A component must implement four methods - getHeight, getWidth, paint, and keyPressed. getHeight and getWidth let you position components relative to each other, as shown by the use of getWidth in the previous code fragment. paint and keyPressed will be discussed in detail later. A component also contains its screen coordinates, which are set by the MContainer's add method as seen in Listing 1. In addition, a component contains a reference to the container in which it's placed, which is also set by the container's add method. Finally, a component contains a Boolean indicating whether it has focus.

Painting
In the low-level API a Canvas is written to the screen when the KVM calls the Canvas's paint method, which is declared as:

protected abstract void paint(Graphics g)

The Graphics object passed in this call has methods that draw specific visual elements on the screen. These methods include:

  • drawArc: Draws a curved line
  • drawChar, drawChars: Draws one or more characters
  • drawImage: Draws an image
  • drawLine: Draws a straight line
  • drawRect: Draws a rectangle
  • drawRoundRect: Draws a rectangle with rounded corners
  • drawString, drawSubstring: Draws strings and substrings
  • fillArc: Fills in a circular or elliptical arc
  • fillRect, fillRoundRect: Fills in rectangles with sharp and rounded corners
Graphics also has methods setColor, setFont, and setStrokeStyle (the latter lets you specify whether lines are to be solid or dotted).

Custom components can use the methods in Graphics to provide their desired visual representation. In Listing 2 we see that MComponent declares an abstract paint method, which a subclass implements to display itself on the screen. For example, a class that extends MComponent could display a box containing text by implementing paint as:

String text = "Hello World";
protected void paint( Graphics g )  {
g.setColor( 0x000000 /* black */ );
g.drawRect( getX(), getY(), getWidth(), getHeight() );
g.drawString( text, ...
Since MContainer extends Canvas, in order for components to be painted on the screen, a connection must be made between the container's paint method, which is called by the KVM, and the paint methods of the components within the container. Listing 1 shows how this occurs; when a component is added to the container, it's placed in a vector of components that the container's paint method iterates through, calling each component's paint method in turn.

In addition to painting itself when requested by the container, a component must also be able to initiate a screen refresh after an event that changes the component's appearance, which occurs when characters are entered into a text field as the result of key presses. MContainer inherits from the Canvas method repaint, which can be called. Since repaint is declared with protected scope, a component can call it via the component's container reference (recall that in Java, members with the protected access modifier have both package and protected scope).

In Listing 2 we see that MComponent contains the method repaint, which calls the container's repaint. Typically, after any change that affects the appearance of the component, the component should call its repaint method to ensure that the screen display is updated. As an example, first revisit the code fragment above, which contains a variable named text. The component containing this fragment could place digits in the text box by implementing MComponent's abstract keyPressed method as:

protected void keyPressed( int keyCode )  {
char keyChar = (char) keyCode;
if (Character.isDigit(keyChar))  {
text += keyChar;  //add to existing text
repaint();
Key Presses and Focus Management
The Canvas class provides the only entry point for processing key events; its keyPressed method is called by the KVM to handle keystrokes. Therefore, for custom components to respond to key events the Canvas must pass key events to the custom components.

In Listing 1 we see that MContainer's keyPressed method passes any key press to the focus manager that was created by MContainer's constructor. The function of a focus manager is to support user navigation between fields. I've adopted the convention used in navigating browser forms that moving right (the tab key in browsers) moves to the next component and moving left (shift-tab) moves to the previous component. On the handset shown in Figure 2, the move-right and move-left keys are the right and left sides of the four-way navigation key.

When any key is pressed and the KVM calls the container's keyPressed method, the container in turn passes the key code to the focus manager's keyPressed method. In Listing 3 we see that if the key pressed was the one to move right or left, the focus manager's keyPressed method will, via the changeFocus method, create MFocusEvents (see Listing 4) and call the processFocusEvent methods of both the component that loses focus and the component that gains focus.

In Listing 2 we see that in the component's processFocusEvent method an internal flag is set indicating whether the component has focus. One way this flag may be used by a component is to alter the way the component is displayed when it has focus. For example, via the following code (where "g" is a Graphics object), the component's paint method can display a black background when it has focus and a transparent background when it doesn't:

g.setColor( hasFocus()
? 0x000000   //black
: 0xFFFFFF);  //clear
g.fillRect( x, y,
getWidth(),getHeight());
The keyPressed method in Listing 3 shows that if the key pressed is neither the move-left nor the move-right key, the key code is passed to the keyPressed method of the component that has focus. MComponent declares this method as abstract, so each subclass must implement code for handling the key press. This returns our discussion to the keyPressed code fragment shown earlier, and completes our exploration into how components receive keystrokes.

Enhancements
This framework would benefit from a number of enhancements, including the following:

  1. Repaint only the affected component: Every repaint paints the entire screen, which is inefficient in the typical case in which a component responds to a key press and so only the display of that component needs to change. While I haven't observed any screen flicker from repainting the entire screen each time, it would be more efficient to repaint only the affected area of the display.
  2. An MPanel class that lets you group and reuse related components: For example, with the expense screen shown in Figure 2, I could use one panel to contain the dollars and cents fields and another to contain the day, month, and year fields.
  3. A layout abstraction so the developer could specify relative positions when adding a component to a container instead of using screen coordinates: In AWT and Swing this is accomplished via layout managers. Since some of the functionality that layout managers provide is related to the ability of users to move and resize windows on a desktop display, and because this ability is not currently present on a cellular phone, I suspect that implementing anything other than very simple layout managers for the cellular phone is probably overkill.
Enhancements that address these issues and provide additional functionality, including components such as scroll lists, radio buttons, and check boxes, will be provided in the next generation of this framework, which will be available to registered Nextel developers at http://developer.nextel.com in June.

Summary
The MIDP high-level API provides little developer control over the look and feel and doesn't support the development of custom visual components. The low-level API does, but doesn't provide an AWT-like level of abstraction for using those components. The classes I've described here provide the beginnings of that level of abstraction via the delegation of paint operations from the Canvas to the custom components, and the delegation of keystroke processing from the Canvas to a focus manager and then conditionally to the custom components.

Author Bio
Glen Cordrey is a senior architect at DigitalFocus (www.digitalfocus.com), which develops Internet applications in Herndon, Virginia. He has over 20 years of experience in application development, and has been developing with Java for the last 4 years. He can be contacted at: glen@oojava.com

	


Listing 1: MContainer provides contextual management of MComponents

import java.util.*;
import javax.microedition.lcdui.*;
abstract public class MContainer extends Canvas
{
  private Vector components = new Vector();
  private MFocusManager focusManager;


  protected MContainer ()
  {
    focusManager = new MFocusManager( this );
  } // constructor


  protected void add ( MComponent component,
                           int x,   // screen
                           int y  ) // coordinates
  {
    component.setX( x );
    component.setY( y );
    component.setContainer( this );
    components.addElement( component );
    if ( components.size() == 1 )
    { // first component on the screen
      component.setFocus( true );
    }
  } // add


  // gets component at this rank, where rank is
  // the order in which the component
  // was added to the container
  MComponent getComponent ( int rank )
  {
    Object obj = components.elementAt( rank );
    return ( obj == null ? null :
             (MComponent ) obj );
  } // getComponent


  // gets the number of components in the container
  protected int getComponentCount ()
  { return components.size(); }


  protected void keyPressed( int keyCode )
  { focusManager.keyPressed( keyCode ); }


  protected void paint( Graphics g )
  { // paint each component
    for ( Enumeration enum = components.elements();
          enum.hasMoreElements(); )
    {
      MComponent nextComponent =
        ( MComponent ) enum.nextElement();
      nextComponent.paint( g );
    }
  } // paint


}// MContainer



Listing 2: MComponent, the base class for all custom visual components

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


abstract public class MComponent
{
  final MContainer getContainer()
  { return this.container; }


  abstract public int getHeight();
  abstract public int getWidth();


  final public int getX()
  {return this.x;}


  final public int getY()
  {return this.y;}


  public boolean hasFocus( )
  { return this.hasFocus; }


  abstract protected void keyPressed(int keyCode);
  abstract protected void paint( Graphics g );


  // container in which this component is placed
  private MContainer container;
  private boolean hasFocus = false;
  private int x, y; // screen coordinates


  protected void
    processFocusEvent( MFocusEvent event )
  {
    hasFocus =
      ( event.getEventId() ==
               MFocusEvent.FOCUS_GAINED );
  } // processFocusEvent


  protected void repaint ()
  {
    if ( container != null )
    container.repaint();
  } // repaint


  final void setContainer( MContainer container )
  { this.container = container; }


  void setFocus ( boolean state )
  { hasFocus = state; }


  final void setX(int  value)
  {this.x = value;}


  final void setY(int  value)
  {this.y = value;}
}// MComponent

Listing 3: MFocusManager provides navigation between MComponents


import javax.microedition.lcdui.*;


public class MFocusManager
{
  // offsets to next and previous component
  private final static int NEXT = 1;
  private final static int PREVIOUS = -1;


  // Values of the keys to move right and left
  private int rightKey, leftKey;


  // Container of components to manage focus for
  private MContainer container;


  // The rank of the component with focus
  private int focusedComponentRank = 0;


  public MFocusManager ( MContainer container )
  {
    this.container = container;
    leftKey = container.getKeyCode(Canvas.LEFT);
    rightKey = container.getKeyCode(Canvas.RIGHT);
  } // constructor


  protected void
    changeFocus( int offset ) // NEXT or PREVIOUS
  {
    // notify currently focused component of change
    MFocusEvent event =
      createFocusEvent( focusedComponentRank,
                        MFocusEvent.FOCUS_LOST );
    container.
      getComponent( focusedComponentRank ).
      processFocusEvent( event );


    focusedComponentRank += offset;


    // notify newly focused component of change
    event =
      createFocusEvent( focusedComponentRank,
                              MFocusEvent.FOCUS_GAINED );
    container.
      getComponent( focusedComponentRank ).
      processFocusEvent( event );


    container.repaint(); // update display
  } // changeFocus


  protected MFocusEvent
    createFocusEvent( int componentRank,
                      int eventId )
  {
    return
      new MFocusEvent( container.
                       getComponent(componentRank),
                       eventId );
  } // createFocusEvent


  protected void keyPressed( int keyCode )
  {
    if ( keyCode == rightKey &&
                focusedComponentRank <
                container.getComponentCount() - 1 )
    { // not the last component
      changeFocus( NEXT );
    }
    else  if ( keyCode == leftKey &&
               focusedComponentRank != 0 )
    { // not the first component
      changeFocus( PREVIOUS );
    }
    else // pass the key stroke to the component
    {    // with focus
      container.
             getComponent( focusedComponentRank ).
             keyPressed( keyCode );
    }
  } // keyPressed


}// MFocusManager




Listing 4: MFocusEvents are delivered to MComponents 
to notify them when they gain and lose focus.

public class MFocusEvent
{
  public final static int FOCUS_GAINED = 1;
  public final static int
    FOCUS_LOST = -FOCUS_GAINED;


  private MComponent source;
  private int eventId; //FOCUS_GAINED or FOCUS_LOST


  MFocusEvent ( MComponent source, int eventId )
  {
    this.source = source;
    this.eventId = eventId;
  } // constructor


  public int getEventId ()
  { return this.eventId; }


  public MComponent getSource ()
  { return this.source; }


}// MFocusEvent

  
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: info@sys-con.com

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.