Download the source code
Version 1.0 of the Mobile Information Device Profile (MIDP) gives you a basic set of components that support most of the user interface (UI) your applications need. If your requirements are even slightly complex, however, you typically must subclass Canvas and reinvent the wheel.
MIDP 2.0 changes all that. Now you can create custom components that give you fine-grained control over user interaction, yet fit into the existing form framework and conform to the device's native look-and-feel.
In this article we'll investigate these new customizing capabilities by building a simple outliner MIDlet. An outliner is a tool for organizing ideas, keeping lists, or even making project plans -- a useful application to have on a mobile device. The Outliner MIDlet will enable users to build a nicely hierarchical outline of form items. They'll be able to add and remove, indent and outdent, and collapse and expand items in a way that was impossible before the advent of MIDP 2.0.
Forms: A Recap
In case you're unfamiliar with building user interfaces with MIDP, let's recap the basics.
MIDP 1.0 provides some stock UI components, including ChoiceGroup,
DateField, Gauge, ImageItem, StringItem,
and TextField. These classes all extend a common base class called
Item. Much like their AWT counterparts, items are abstractions
we use to control underlying native UI widgets. Because native implementations
can and do vary widely from one device to the next, the public interface of
Item gives you very little control over the appearance and behavior
of the underlying widgets.
Forms exist to arrange items in rows that best fit the screen dimensions and the capabilities of the device they're running on. In theory at least, the MIDP implementation easily and seamlessly adapts your application to the device hardware; the downside is that your influence over how the user interface looks and feels is limited.
What's New?
MIDP 2.0 enhances Form to give you better control over the layout of items, and provides a new class, CustomItem, that empowers you to create your own form items. Outliner uses all of these capabilities to provide users with the following features:
- The application displays multiple lines of text, indented by varying amounts
to represent a hierarchical structure visually.
Form's enhanced
layout capabilities make this representation possible.
- The user can collapse any line of an outline to hide lines that are beneath
it in the hierarchy. A visual indicator shows whether a given line is expanded
or collapsed. You can override
CustomItem's paint()
method to draw such an indicator however you like.
- The user can also rearrange lines into any order desired. Moving a line
moves all its subordinate lines as well. Now that commands can be applied specifically
to an item, menus can be context-sensitive. The commands to move up, move
down, expand, and collapse appear only when they are applicable to the current
state of the item.
None of these features would have been possible in MIDP 1.0. Let's take a look at how al this magic is done.
Creating the Outline Item Class
The Outliner class itself is just a run-of-the-mill MIDlet. The heart of the functionality is in a subclass of CustomItem called OutlineItem. This class does all of the things you would need to do to implement your own CustomItem, so you should take a good look at it in the source code. The constructor is a good place to start:
/**
* Creates an OutlineItem with the specified initial indent and text.
*/
public OutlineItem( int inIndent, String inText )
{
// we don't want a system-supplied label
super( null );
indent = inIndent;
text = inText;
hiddenChildren = null;
// define layout
setLayout( LAYOUT_EXPAND | LAYOUT_TOP | LAYOUT_NEWLINE_AFTER );
// add the commands that always apply
addCommand( editCommand );
addCommand( insertCommand );
}
|
An OutlineItem is created by calling the constructor and passing in the text to be displayed and the number of times the item should be indented.
Within the constructor, the first task is to call the superclass's constructor. A MIDP Item represents not only the UI widget itself but also a label that identifies it to the user. For example, a TextField is a box containing text; its label usually appears to its left, as words describing what's in the box, like Name or Password. An outline item has no use for a label, so our constructor passes null as the required argument to the superclass constructor.
After initializing the object's state with the parameters passed to the constructor,
the next steps are to configure the item's layout directives and add some commands
directly to the item. I'll explain the layout directives and item-specific commands
a bit later.
CustomItem has five abstract methods we must implement. Of these, paint() is the one that gives you control over the appearance of your item. The arguments to paint() include the width and height of your item as determined by the form's layout logic, and a graphics object, translated so that its origin is at the top left corner of the item. Here's OutlineItem's implementation of paint():
public void paint( Graphics g, int w, int h )
{
// clear all with background color
g.setColor( DISPLAY.getColor( DISPLAY.COLOR_BACKGROUND ) );
g.fillRect( 0, 0, w, h );
// now use foreground color for drawing
g.setColor( DISPLAY.getColor( DISPLAY.COLOR_FOREGROUND ) );
if ( isCollapsed() )
{
// draw a filled circle to represent hidden items
g.fillArc( indent * INDENT_MARGIN + 2, 2,
FONT_HEIGHT-7, FONT_HEIGHT-7, 0, 360 );
}
else
{
// no hidden items so draw an empty circle
g.drawArc( indent * INDENT_MARGIN + 2, 2,
FONT_HEIGHT-7, FONT_HEIGHT-7, 0, 360 );
}
// draw the text
g.drawString( text,
indent * INDENT_MARGIN + FONT_HEIGHT, 0, g.TOP | g.LEFT );
}
|
We discover the device's default background and foreground colors by calling
the Display's getColor() method twice, passing it the appropriate constant each time, COLOR_BACKGROUND, then COLOR_FOREGROUND. Whether we elect to use the default
colors or specify our own, OutlineItem.paint() fills a rectangle
with the background color, then switches to the foreground color to draw the
rest of the content.
Note that the MIDP specification requires you to cover every pixel in your item's display area when you paint. Some implementations may clear the area covered by your item before calling paint(), but others may not. You risk losing portability if you don't first fill the rectangle with the background color.
OutlineItem then draws a circle offset to the right by eight pixels per level of indent, filling the circle if the item is in the collapsed state. The circle's width and height are both determined by the height of the font so the circle will scale appropriately on devices with varying fonts and sizes. Because the size of the circle never exceeds the font height, the text is offset to the right by the indent margin plus the font height.
Note that long strings may spill off the screen because OutlineItem doesn't take line lengths into consideration. I cheerfully leave you
the chore of implementing text-wrapping as an exercise.
The remaining abstract methods let the CustomItem base class ask our subclass to calculate the appropriate minimum and preferred sizes of the item. OutlineItem's implementations are straightforward:
public int getMinContentHeight()
{
return FONT_HEIGHT;
}
public int getMinContentWidth()
{
return indent * INDENT_MARGIN + FONT_HEIGHT;
}
public int getPrefContentWidth( int height )
{
return indent * INDENT_MARGIN
+ FONT.stringWidth( text ) + FONT_HEIGHT;
}
public int getPrefContentHeight( int width )
{
return FONT_HEIGHT;
}
|
You can think of these methods as callbacks used by CustomItem's implementation of the accessors for the minimum and preferred sizes. Unless you override them, your item's getMinimumWidth() method will return the result of the getMinContentWidth() method, and its getMinimumHeight() method will return the result from getMinContentHeight().
The item's preferred width and height are determined in almost the same way,
except that the application can modify the preferred dimensions. Once an item's
setPreferredWidth() or setPreferredHeight() method
is called, the corresponding dimension is said to be locked. A call to
get the preferred size of that dimension will always return the locked value.
Both dimensions are unlocked when an item is created, and you can unlock a locked
dimension by setting its size to -1.
The getPrefContentWidth() and getPrefContentHeight()
methods will be called only when their respective dimensions are unlocked. They
should return sizes that best display the current contents of your item, with
minimal line-wrapping and no clipping.
OutlineItem does no wrapping, so the minimum and preferred heights both equal the height of the current font. The preferred width is the width of the current text in the current font plus room for the expansion indicator plus the size of the current indent. The minimum width is simply the indicator width plus the size of the indent.
Form Layout
To produce a layout, a form needs not only the minimum and preferred sizes of each item, but also each item's layout directives: one-bit flags that specify alignment and breaks between rows. An item's layout directives are combined and packed into a single integer. If you don't specify any layout directives, you get the default MIDP 1.0-compatible layout, in which items are placed in rows, one after another. To specify a different layout, use the bitwise OR operator to combine a number of predefined layout directives into an integer, and pass it to setLayout().
The Item class defines the layout directives as constants:
LAYOUT_LEFT
LAYOUT_RIGHT
LAYOUT_CENTER
LAYOUT_SHRINK
LAYOUT_EXPAND
|
LAYOUT_TOP
LAYOUT_BOTTOM
LAYOUT_VCENTER
LAYOUT_VSHRINK
LAYOUT_VEXPAND
|
LAYOUT_NEWLINE_BEFORE
LAYOUT_NEWLINE_AFTER
|
Form's layout algorithm is somewhat complex. It's explained in
great detail in the class documentation; I'll only summarize by saying that
the algorithm follows a "springs-and-struts" pattern that works somewhat
like a cross between AWT's SpringLayout
and GridBagLayout
layout managers.
For the horizonal and vertical dimensions, each item has an alignment, and
a preference for shrinking or expanding to fit available space on a given row.
An item can also specify that it appear at the beginning or end of a row by
requesting that the row break immediately before or after.
To ensure portability, you should specify no more layout directives than you
need. The default alignment options will vary across implementations and between
locales, usually for languages that are not read from left to right. You're not required to specify a layout, but doing so gives
the item a default layout that may help it achieve a desirable consistency in
appearance or behavior.
OutlineItem sets its own layout directives in its constructor, with
this call:
// define layout
setLayout( LAYOUT_EXPAND | LAYOUT_TOP | LAYOUT_NEWLINE_AFTER ); |
The layout directives include
LAYOUT_EXPAND, LAYOUT_TOP, and LAYOUT_NEWLINE_AFTER.
A horizontal alignment option is not needed because the item fills all available
horizontal space. Because neither LAYOUT_VSHRINK nor LAYOUT_VEXPAND
is specified, the form gives the item its preferred height and aligns it to
the top of the available vertical space in the row. The item gets a
row break after its position in the form, so each item appears on its own row.
Because Outliner's form contains only OutlineItems,
this combination of layout directives puts every item in its own row, and makes each as wide
as the form and as tall as its preferred height.
Traversing Forms
Up to now we've focused on the custom item's appearance -- its look. Now we'll consider its behavior -- its feel in response to user input.
MIDP forms have a built-in notion of navigation called traversal. This is similar to the concept of shifting input focus in a desktop application. In both desktop and mobile environments, at any given moment a single UI component has the focus, which means that all user input actions are directed to that component.
For example, if a text field has the focus, some key presses cause characters to appear in the field at the field's insertion point. In a typical desktop application, the arrow keys move the insertion point within the text field and the tab key transfers the focus to the next component. A mobile device may not have a full keyboard. Indeed, it may not even have four directional arrow keys. If it does, the right and left keys may move the insertion point and the up and down keys may transfer the focus. With only two arrow keys, the up and down keys may serve double duty: moving the insertion point, and shifting the focus if the insertion point reaches the beginning or end of the field.
Because devices vary widely, MIDP provides a mechanism for custom items to support traversal in a way that's consistent and portable. This mechanism is embodied in a method of CustomItem:
protected boolean traverse(
int dir,
int viewportWidth,
int viewportHeight,
int[] visRect_inout )
|
Our custom item's traverse() method is called when the user presses a navigational key that causes our item to receive the focus, typically one of the arrow keys. If the method returns true, it is called again the next time the user presses a navigational key, and so on until the method returns false.
The first argument to traverse() is the key press that caused focus to transfer to our item. The value will be one of the directional game actions defined in the Canvas class: Canvas.UP, Canvas.DOWN, Canvas.LEFT, and Canvas.RIGHT, or the value CustomItem.NONE. If the value is NONE, some platform-specific event like a resizing of the form caused your item to gain the focus.
The rest of the arguments describe the dimensions of the screen and the portion of your item that's visible on it. Some items, particularly those that display large amounts of text, are larger than the screen and they must be able to scroll their visible content in response to traversal events. The documentation for traverse() explains these arguments in detail, but you don't need to worry about them now.
Your implementation should return true whenever you want the item to retain the focus in response to the user's key press. The implementation in CustomItem always returns false, producing a behavior like StringItem's: Any navigational key press shifts the focus to a different item. This behavior is appropriate for most simple subclasses of CustomItem.
Items that are more interactive may need to override traverse() to customize
the behavior of the navigational keys. A good example is the Gauge
item, which in some implementations causes the right and left keys to increment
and decrement the value in the component. When the value reaches its maximum
or minimum, traverse() returns false to allow that
key press to shift the focus to the next component in the corresponding direction.
Some implementations of TextField work similarly, moving the insertion
point to the left or right and shifting focus only when the insertion point
is at the beginning or end of the field.
For OutlineItem, the up and down direction keys shift the focus
to another component as usual. Because there is no insertion point to worry
about -- all editing takes place on a separate screen --
OutlineItem intercepts right and left key presses to indent and outdent
the text. On devices without horizontal direction keys, the outliner MIDlet shows extra menu items for indenting and outdenting text, as I'll explain a bit later.
Here's the implementation:
/**
* Used to indent and outdent the item when possible.
*/
protected boolean traverse(
int dir,
int viewportWidth,
int viewportHeight,
int[] visRect_inout )
{
// this flag distinguishes between
// traversing INTO this item and
// traversing WITHIN this item:
if ( traversingItem != this )
{
// traversing INTO: mark self and return true
traversingItem = this;
return true;
}
// handle traversing WITHIN this item
switch ( dir )
{
case Canvas.RIGHT:
if ( isIndentable() )
{
indent();
}
repaint();
return true;
case Canvas.LEFT:
if ( isOutdentable() )
{
outdent();
}
repaint();
return true;
case NONE:
// do nothing: just a form layout reflow
return true;
default:
// break out
}
return false;
}
|
Note that traverse() is called when focus traverses into your item and, as long as your implementation returns true, each time the focus traverses within your item. You will want to distinguish between these cases in your code. OutlineItem keeps a static reference to determine whether the item is gaining focus with the present call to traverse(). If so, it returns true so that it can receive the next directional key.
On subsequent calls to traverse(), OutlineItem looks
at which key was pressed. If the right or left key was pressed, it changes the
indentation level. In the NONE case, OutlineItem does
nothing but return true, to retain the focus. The remaining two
cases, UP and DOWN, return false to allow
the focus to traverse to the next or previous item.
Because changing the level of indent also changes the visual appearance of the item, traverse() calls repaint() to tell the form to redraw itself. Because OutlineItem does no word-wrapping, the preferred dimensions of the item don't change. If they did change, traverse() would call invalidate() instead of repaint(), to ask the form to rearrange its layout.
Tailoring User Interaction
For added flexibility, MIDP 2.0 lets you associate commands with individual tems on a form. When an item has focus, the item's commands are combined with the form's commands to produce a context-sensitive menu. Responding to an item's command requires no more effort than responding to the form's command; it's
just a different interface. Outliner implements the ItemCommandListener
interface and adds itself as the listener for each of the items on the form.
Adding commands to an item and removing them work as you would expect: you just call addCommand() and removeCommand(). Because each OutlineItem tracks its own state -- whether it is expanded or collapsed and whether it can indent, outdent, or move up or down -- each custom item manages its own list of applicable commands. Each time an OutlineItem's state changes, the item calls updateCommands() on itself.
private void updateCommands()
{
if ( !hasPointerPress() )
{
removeCommand( expandCommand );
removeCommand( collapseCommand );
// fall back on commands to expand/collapse
if ( isCollapsed() )
addCommand( expandCommand );
else
addCommand( collapseCommand );
}
if ( !hasHorizontalTraversal() )
{
removeCommand( indentCommand );
removeCommand( outdentCommand );
// fall back on commands to indent/outdent
if ( isIndentable() )
addCommand( indentCommand );
if ( isOutdentable() )
addCommand( outdentCommand );
}
removeCommand( upCommand );
removeCommand( downCommand );
if ( canMoveUp() )
addCommand( upCommand );
if ( canMoveDown() )
addCommand( downCommand );
removeCommand( deleteCommand );
if ( getIndex() > 0 )
{
// if not root
addCommand( deleteCommand );
}
} |
There are so many commands that it makes good sense to hide those that don't
apply to the item in its current state and position. To keep the logic simple we remove all commands from the item, then selectively re-insert them.
It's nice to know that it doesn't hurt to remove a command that an item doesn't
have, and that you won't get a duplicate if you add the same command twice. Another bit of welcome news: Rebuilding the command list as often as we do has no perceptible impact
on the application's responsiveness.
Recall that OutlineItem uses the horizontal navigation keys to change the indentation level. Commands for indenting and outdenting are necessary on devices that don't have horizontal navigation keys, but redundant on devices that do. Fortunately, there's a way to make these commands visible only where they're needed.
CustomItem has a getInteractionModes() method we can call to find out what UI capabilities a device offers. The method returns a bitmask against which you can test the interface-capability constants that CustomItem defines: KEY_PRESS, KEY_RELEASE, KEY_REPEAT, POINTER_PRESS, POINTER_DRAG, POINTER_RELASE, TRAVERSE_HORIZONTAL, and TRAVERSE_VERTICAL. The updateCommands() method calls the methods hasHorizontalTraversal() and hasPointerPress() to take advantage of this facility:
private boolean hasPointerPress()
{
return ( getInteractionModes() & POINTER_PRESS ) != 0;
}
private boolean hasHorizontalTraversal()
{
return ( getInteractionModes() & TRAVERSE_HORIZONTAL ) != 0;
}
|
OutlineItem hides the indent and outdent commands if the device has horizontal navigation keys -- or if it supports a stylus or mouse. CustomItem sports methods for responding to taps with a pointing device, just as Canvas does. OutlineItem overrides pointerPressed() to toggle the expansion state if the user taps on the expansion indicator.
protected void pointerPressed( int x, int y )
{
// if in widget area
if ( x < FONT_HEIGHT )
{
if ( isCollapsed() )
expand();
else
collapse();
}
}
|
The coordinates passed to this method are relative to the top and left of the item's display area, so a simple comparison against the X coordinate reveals whether the pointer press was near the expansion indicator. Popular new MIDP 2.0 devices like the Sony Ericsson P900 and the Palm Tungsten series accept stylus entry, so it's a good idea to implement support for pointer interaction in your applications.
Empowering you to write applications that can be run on a variety of devices with differing capabilities and form factors was one of MIDP's original design goals. MIDP 2.0 makes it even easier, so take full advantage of the opportunity.
Summary
You can now write custom components with the kinds of visualization and validation that were not possible with MIDP 1.0. Form's enhanced layout capabilities give you more control over your presentation, and CustomItem lets you tailor behavior to the look and feel of the native device without requiring you to write and maintain device-specific code. MIDP 2.0 opens the door to near limitless customization of user interface in form-based mobile applications.
Acknowledgements
I'd like to thank Roger Riggs for suggesting improvements to the code and Brian Christeson for innumerable improvements to the text. And not least I'd like to thank my family for affording me the time to write.
About the Author
Michael Powers is Principal of mpowers LLC, a software consultancy for desktop and wireless platforms, and he has been working with Java technology in its various incarnations since its inception. His award-winning Piranha Pricecheck MIDlet is taking the world by storm, and is a free download from mpowers.net.
|