Download the source
Displaying a list of information is a common user interface task. In the Mobile Information Device Profile (MIDP), the classes List and
ChoiceGroup make these chores fairly easy. These components do limit your options, however, because they directly manage the data they display. In this tech tip we develop a replacement for them called a "virtual list," that uses callbacks to obtain the data to display, in
effect delegating the management of the data to the application. For simplicity, the list will display only non-wrapping string data.
Creating this virtual list gives you a useful example of how to write a custom MIDP 2.0 form item and how to deal with some important design
issues.
We start by extending CustomItem to create VirtualList:
import javax.microedition.lcdui.*;
public class VirtualList extends CustomItem {
public VirtualList( String name, Display display ){
super( name );
_display = display;
}
...
}
|
Like all form items, a virtual list has an optional label. It also needs a reference to the MIDlet's Display object to obtain the color mappings it needs to draw the component.
Next, let's deal with the size of the list. As the Tech Tip "Using
Custom Items in MIDP 2.0" describes, a custom form item must implement the methods getMinContentHeight(), getMinContentWidth(),
getPrefContentHeight(), and getPrefContentWidth() to specify the minimum and preferred dimensions of the component's content area for layout purposes. Because we're displaying strings only, we should base these dimensions on the font used to draw the strings. We add getFont() and
setFont() methods to get and set the font:
public Font getFont() {
return _font;
}
public void setFont( Font f ) {
_font = f;
invalidate();
}
|
Note the call to invalidate() after setting the font: The component's dimensions are font-based, so we need to force an update of the form's layout, not just a repaint of the component.
We also need to know how many lines of data to display in the list, so we add getVisibleCount() and setVisibleCount() methods. Another approach would be to let the list grab as much screen real estate as it can.
All we need to calculate the dimensions are some sample strings. Ideally, these should come from the application, just like the data, and
be representative of the strings that the list will display. We define a nested interface with callback methods:
public interface Callback {
String getMinimalString( VirtualList list );
String getPreferredString( VirtualList list );
String getString( VirtualList list, int index );
}
public Callback getCallback() {
return _callback;
}
public void setCallback( Callback c ) {
_callback = c;
}
|
See the code listing at the end of this tech tip for the actual dimension calculations.
With the callback interface defined, we can add a method that, given a list index, returns the string to display:
public String getString( int index ){
String s = null;
if( _callback != null ){
s = _callback.getString( this );
}
return s != null ? s : "";
}
|
There's almost enough information to paint the list. We just need to know the total count of data elements in the list, so we add
getTotalCount() and setTotalCount() methods. We also add
getSelectedIndex() and setSelectedIndex(), to indicate which line in the list is selected. For painting details, see the paint() method in the listing.
The only task left is to deal with user action, which we do by supporting internal traversal of the component, as described in the tech tip
"Custom Item Traversal in MIDP 2.0." Knowing which interaction modes the user's device supports is important: Remember that many handsets have only two arrow keys. In this case, "left" should be equivalent to "up," and "right" should be equivalent to "down." Where movement in two dimensions is supported, consider assigning them
different semantics. The list control uses the "left" and "right" directions as shorthand for quickly moving the focus to another component.
Here's the complete code for the virtual list:
import javax.microedition.lcdui.*;
// A custom component for MIDP 2.0 that implements
// a "virtual" list where management of the data
// is delegated to the application via a callback.
public class VirtualList extends CustomItem {
// The callback interface used by the list to
// obtain the string it must display.
public interface Callback {
String getMinimalString( VirtualList list );
String getPreferredString( VirtualList list );
String getString( VirtualList list, int index );
}
// The offset between items.
private static final int ITEM_OFFSET = 1;
// Shorthand forms of the color definitions
private static final int BACKCOLOR_SEL =
Display.COLOR_HIGHLIGHTED_BACKGROUND;
private static final int BACKCOLOR_UNSEL =
Display.COLOR_BACKGROUND;
private static final int FORECOLOR_SEL =
Display.COLOR_HIGHLIGHTED_FOREGROUND;
private static final int FORECOLOR_UNSEL =
Display.COLOR_FOREGROUND;
// The constructor. The display object is needed
// to obtain the color mappings.
public VirtualList( String label, Display display ){
super( label );
_display = display;
}
// Returns the callback.
public Callback getCallback(){
return _callback;
}
// Returns the font set by the application.
// If null, then the list uses the default system font.
public Font getFont(){
return _font;
}
// Returns a guaranteed non-null font object.
protected Font getFontForDrawing(){
return _font != null ? _font :
Font.getDefaultFont();
}
// Returns the height of a line, which is basically
// the font height plus an offset.
protected int getLineHeight(){
return getFontForDrawing().getHeight()
+ ITEM_OFFSET * 2;
}
// Returns the minimal height of the content area.
public int getMinContentHeight(){
int mh = _minHeight;
int lh = getLineHeight();
if( mh < lh ){
mh = lh;
}
return mh;
}
// Returns the minimal width of the content area.
public int getMinContentWidth(){
int mw = _minWidth;
if( mw < 0 ){
Font font = getFontForDrawing();
String typical = getMinimalString();
mw = font.stringWidth( typical );
}
return mw;
}
// Returns the typical "minimal" string used
// to calculate a minimal line width.
public String getMinimalString(){
String s = null;
if( _callback != null ){
s = _callback.getMinimalString( this );
}
return s != null ? s : "M";
}
// Returns the preferred height of the content area.
public int getPrefContentHeight( int width ){
int ph = _prefHeight;
if( ph < 0 ){
ph = getLineHeight() * _visible;
}
return ph;
}
// Returns the preferred width of the content area.
public int getPrefContentWidth( int height ){
int pw = _prefWidth;
if( pw < 0 ){
Font font = getFontForDrawing();
String typical = getPreferredString();
pw = font.stringWidth( typical );
}
return pw;
}
// Returns the typical "preferred" string used
// to calculate a preferred size.
public String getPreferredString(){
String s = null;
if( _callback != null ){
s = _callback.getPreferredString( this );
}
return s != null ? s : "MMMMM";
}
// Returns the index of the selected item.
public int getSelectedIndex(){
return _selected;
}
// Returns the string to display for the given index.
public String getString( int index ){
String s = null;
if( _callback != null ){
s = _callback.getString( this, index );
}
return s != null ? s : "";
}
// Returns the total number of elements in the list.
public int getTotalCount(){
return _count;
}
// Returns the number of elements to display.
public int getVisibleCount(){
return _visible;
}
// Makes sure the selected item is visible.
protected void makeSelectedVisible(){
if( _selected >= 0 ){
if( _selected < _top ){
_top = _selected;
} else if( _selected - _top + 1 > _visible ){
_top = _selected - _visible + 1;
}
}
}
// Draws the item's content area, whose dimensions
// are given by the width and height parameters.
protected void paint( Graphics g, int width, int height ){
int lh = getLineHeight();
int lines = height / lh;
int curr = _top;
int last = _top + lines;
int backSel = _display.getColor( BACKCOLOR_SEL );
int backUnsel = _display.getColor( BACKCOLOR_UNSEL );
int foreSel = _display.getColor( FORECOLOR_SEL );
int foreUnsel = _display.getColor( FORECOLOR_UNSEL );
int y = 0;
while( curr < _count && curr < last ){
String s = getString( curr );
boolean isSel = ( curr == _selected );
g.setColor( isSel ? backSel : backUnsel );
g.fillRect( 0, y, width, lh );
g.setColor( isSel ? foreSel : foreUnsel );
g.drawString( s, 0, y+1, g.TOP | g.LEFT );
y += lh;
++curr;
}
if( y < height ){
g.setColor( backUnsel );
g.fillRect( 0, y, width, height - y );
}
}
// Sets the callback.
public void setCallback( Callback callback ){
_callback = callback;
}
// Sets the minimum content height.
public void setMinContentHeight( int mh ){
_minHeight = mh;
}
// Sets the minimum content width.
public void setMinContentWidth( int mw ){
_minWidth = mw;
}
// Sets the preferred content height.
public void setPrefContentHeight( int ph ){
_prefHeight = ph;
}
// Sets the preferred content width.
public void setPrefContentWidth( int pw ){
_prefWidth = pw;
}
// Sets the index of the selected element.
public void setSelectedIndex( int index ){
if( index >= 0 && index < _count ){
_selected = index;
makeSelectedVisible();
} else {
_selected = -1;
}
repaint();
}
// Sets the total number of elements in the list.
public void setTotalCount( int count ){
_count = count;
invalidate();
}
// Sets the number of elements to display.
public void setVisibleCount( int count ){
_visible = ( count > 0 ? count : 1 );
invalidate();
}
// Handle traversal of the component.
public boolean traverse( int dir, int vw, int vh,
int[] vrect ){
if( !_inTraversal ){
_inTraversal = true;
if( _selected < 0 && _count > 0 ){
if( dir == Canvas.DOWN ||
dir == Canvas.RIGHT ){
_selected = 0;
} else if( dir == Canvas.UP ||
dir == Canvas.LEFT ){
_selected = _count - 1;
}
makeSelectedVisible();
repaint();
notifyStateChanged();
}
return true;
}
int modes = getInteractionModes();
boolean horiz = ( modes & TRAVERSE_HORIZONTAL ) != 0;
boolean vert = ( modes & TRAVERSE_VERTICAL ) != 0;
boolean notify = false;
if( dir == Canvas.DOWN ||
( dir == Canvas.RIGHT && !vert ) ){
if( _selected < _count - 1 ){
++_selected;
notify = true;
} else {
return false;
}
} else if( dir == Canvas.UP ||
( dir == Canvas.LEFT && !vert ) ){
if( _selected > 0 ){
--_selected;
notify = true;
} else {
return false;
}
} else if( dir != NONE ){
return false;
}
makeSelectedVisible();
repaint();
if( notify ){
notifyStateChanged();
}
return true;
}
// Called when user has traversed out.
public void traverseOut(){
_inTraversal = false;
}
private Callback _callback;
private int _count; // the number of elements
private Display _display;
private Font _font;
private boolean _inTraversal;
private int _minHeight = -1;
private int _minWidth = -1;
private int _prefHeight = -1;
private int _prefWidth = -1;
private int _selected = -1;
private int _top; // the top element
private int _visible = 1;
}
|
We could improve this class in several ways: It could cache the data for
the visible items. It could draw a border around the content area. It
could support multiple selection of items. It could implement MIDP's
Choice interface. It could draw images as well as strings, or even leave
the drawing of individual list elements entirely up to the application.
About the Author: Eric Giguere is a software developer for iAnywhere Solutions, a subsidiary of Sybase, where he works on Java technologies for handheld and wireless computing. He holds BMath and MMath degrees in Computer Science from the University of Waterloo and has written extensively on computing topics.