Sun Java Solaris Communities My SDN Account Join SDN
 
Wireless Tech Tips

J2ME Tech Tips: November 14, 2001

 

Tech Tips archive

November 14, 2001

WELCOME to the Java Developer Connection (JDC) Java 2 Platform, Micro Edition (J2ME) Tech Tips, for April 16, 2001. This issue covers:

The J2ME Tech Tips are written by Eric Giguere (http://www.ericgiguere.com), an engineer at iAnywhere Solutions, inc. Eric is the author of the book "Java 2 Micro Edition: Professional Developer's Guide" and co-author of the upcoming "Mobile Information Device Profile for Java 2 Micro Edition," both books in John Wiley & Sons' Professional Developer's Guide series.

Pixel

DEALING WITH DATES AND TIMES IN MIDP APPLICATIONS

Parsing and displaying dates and times is often complicated because of formatting and locale issues. Java 2 Platform, Standard Edition (J2SE) provides several classes to simplify date and time handling -- classes such as java.util.Calendar, java.util.Date, java.util.TimeZone, and java.text.DateFormat. By comparison, the Mobile Information Device Profile (MIDP) defines only subsets of the Calendar, Date and TimeZone classes, and does not include any form of DateFormat. How, then, can your MIDP applications properly handle dates and times?

The answer lies in the javax.microedition.lcdui.DateField class, part of the MIDP high-level user interface API. DateField is an interactive user interface component that displays a date, time, or both. It also allows you to edit the date and time. DateField extends the Item class. This means that DateField components can be placed on Form objects. So the first step in using a DateField is to create a Form and place the DateField on the form:

    Form f = new Form( "A Form" );
    f.append( df );

As with any Item, the DateField is displayed only when the form is made active by calling Display.setCurrent.

The DateField class defines two constructors:

    public DateField( String label, int mode );
    public DateField( String label, int mode,
                      java.util.TimeZone zone );

To properly display dates and times, a DateField instance needs to know which time zone to use. The two-argument constructor uses the device's default time zone. The three-argument constructor lets you specify an explicit time zone if the default is inappropriate. Note that you can't change the displayed time zone without creating a new instance of DateField.

The first two arguments are identical in both constructors. The first argument is the label to display alongside the field -- use null if there is no label. The second argument is the input mode of the field. There are three possible modes, these are declared as constants in the DateField class:

    public static final int DATE = 1;
    public static final int TIME = 2;
    public static final int DATE_TIME = 3;

The input mode controls what the field displays: a date only, a time only, or a combined date and a time. You can change the input mode at any time by calling the setInputMode method.

When you create a new DateField instance, you do not have to set a date or time. The following code, for example, displays an uninitialized date:

    Display display = ....; // initialized elsewhere
    Form f = new Form( "An Empty Date" );
    DateField df = new DateField( "Date:", 
                                  DateField.DATE );
    f.append( df );
    display.setCurrent( f );

To initialize the field to a particular date or time, call setDate and pass in a java.util.Date object initialized to the correct value:

    Calendar c = Calendar.getInstance();
    c.set( Calendar.MONTH, Calendar.OCTOBER );
    c.set( Calendar.DAY_OF_MONTH, 18 );
    c.set( Calendar.YEAR, 1996 );
    c.set( Calendar.HOUR_OF_DAY, 16 );
    c.set( Calendar.MINUTE, 39 );
    c.set( Calendar.SECOND, 45 );
    c.set( Calendar.MILLISECOND, 0 );
    
    Date moment = c.getTime();
    DateField df = new DateField( null, 
                                 DateField.DATE_TIME );
    df.setTime( moment );

A Date object represents a moment in time (in coordinated universal time, or UTC, to be exact) as the number of milliseconds since midnight, January 1, 1970. Use a Calendar instance to create a Date instance, as shown above.

Note that a DateField in TIME input mode requires the date portion to be set to January 1, 1970. Two useful routines for clearing out the date portion of a Date and for combining two Date objects into a single object are as follows:

    // Return a Date with the time intact but the date
    // set to January 1, 1970

    public static Date clearDate( Date d ){
        Calendar c = Calendar.getInstance();
        c.setTime( d );
        c.set( Calendar.MONTH, Calendar.JANUARY );
        c.set( Calendar.DAY_OF_MONTH, 1 );
        c.set( Calendar.YEAR, 1970 );
        return c.getTime();
    }

    // Combine a date and time into a single 
    // Date instance

    public static Date combineDateTime( 
                                Date date, Date time ){
        Calendar cd = Calendar.getInstance();
        Calendar ct = Calendar.getInstance();

        cd.setTime( date );
        ct.setTime( time );

        ct.set( Calendar.MONTH, 
                            cd.get( Calendar.MONTH ) );
        ct.set( Calendar.DAY_OF_MONTH, 
                     cd.get( Calendar.DAY_OF_MONTH ) );
        ct.set( Calendar.YEAR, 
                             cd.get( Calendar.YEAR ) );

        return ct.getTime();
    }

Always do your date manipulation using the Calendar class, not using the raw milliseconds value stored in a Date object.

After a DateField is displayed, the system will allow the user to select the object and edit the date, time or both, depending on the input mode. Whenever you need to obtain the new date/time, call the getDate method:

    DateField df = ....;
    Date editedDate = df.getDate();

Here is a simple MIDlet that lets you view and edit dates and times using all three input modes.

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

/**
 * Demonstration of time/date editing using the MIDP
 * DateField class.
 */

public class DateFieldTest extends MIDlet {

    private Display display;
    
    // Define our Command objects
  
    private Command exitCommand =
             new Command( "Exit", Command.EXIT, 1 ); 
    private Command okCommand =
             new Command( "OK", Command.OK, 1 );
    private Command cancelCommand =
             new Command( 
                         "Cancel", Command.CANCEL, 1 );

    public DateFieldTest(){
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    protected void pauseApp(){
    }
 
    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ // first time called...
            initMIDlet();
        }
    }

    private void initMIDlet(){
        display = Display.getDisplay( this );
        testList = new TestList();
        display.setCurrent( testList );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }
 
    // Return a Date with the time intact but the date
    // set to January 1, 1970

    public static Date clearDate( Date d ){
        Calendar c = Calendar.getInstance();
        c.setTime( d );
        c.set( Calendar.MONTH, Calendar.JANUARY );
        c.set( Calendar.DAY_OF_MONTH, 1 );
        c.set( Calendar.YEAR, 1970 );
        return c.getTime();
    }

    // Combine a date and time into a single 
    // Date instance

    public static Date combineDateTime( Date date, 
                                        Date time ){
        Calendar cd = Calendar.getInstance();
        Calendar ct = Calendar.getInstance();

        cd.setTime( date );
        ct.setTime( time );

        ct.set( Calendar.MONTH, 
              cd.get( Calendar.MONTH ) );
        ct.set( Calendar.DAY_OF_MONTH,  
                   cd.get( Calendar.DAY_OF_MONTH ) );
        ct.set( Calendar.YEAR, 
                   cd.get( Calendar.YEAR ) );

        return ct.getTime();
    }
    
    // The list of tests we can perform, arranged 
    // in threes so that ( index % 3 ) == one 
    // of DATE, TIME or DATE_TIME

    static final String[] testLabels = {
        "Current date",
        "Current time",
        "Current date/time",
        "Edit date",
        "Edit time",
        "Edit date/time",
    };

    private TestList testList;
    private Date     editDate;

    //
    // Displays the list of actions
    //

    class TestList extends List
                   implements CommandListener {
        public TestList(){
            super( "DateField Tests", IMPLICIT,
                   testLabels, null );
            addCommand( exitCommand );
            setCommandListener( this );
        }

        public void commandAction( Command c,
                                   Displayable d ){
            if( c == exitCommand ){
                exitMIDlet();
            } else if( c == List.SELECT_COMMAND ){
             
                // Figure out which date to display
                // and what the input mode is

                int     which = getSelectedIndex();
                String  label = getString( which );
                int     mode = ( which % 3 ) + 1;
                boolean save = ( which > 2 );

                display.setCurrent(
                   new Edit( save, label, mode ) );
            }
        }
    }

    //
    // Edit a date, time or date/time, optionally
    // saving the value.
    //

    class Edit extends Form
               implements CommandListener {

        public Edit( boolean save, String label,
                     int mode ){

            super( label );
            this.save = save;

            Date d = editDate;

            if( !save ){
                d = new Date();
            }

            dateField = new DateField( null, mode );
            append( dateField );

            if( d != null ){
                if( mode == DateField.TIME ){
                    d = clearDate( d );
                }

                dateField.setDate( d );
            }

            addCommand( okCommand );

            if( save ){
                addCommand( cancelCommand );
            }

            setCommandListener( this );
        }

        public void commandAction( Command c,
                                   Displayable d ){
            Alert alert = null;
            Date  date = dateField.getDate();

            if( 
              save && date != null && c == okCommand ){
                if( editDate != null ){
                   int mode = dateField.getInputMode();
                    if( mode == DateField.DATE ){
                        editDate = combineDateTime( 
                                      date, editDate );
                    } else if( 
                              mode == DateField.TIME ){
                        editDate = combineDateTime( 
                                      editDate, date );
                    } else {
                        editDate = date;
                    }
                } else {
                    editDate = date;
                }

                Calendar cal = Calendar.getInstance();
                cal.setTime( editDate );

                alert = new Alert( "New date/time" );
                alert.setString( 
                 "The saved date/time is now " + cal );
                alert.setTimeout( Alert.FOREVER );
            }

            if( alert != null ){
                display.setCurrent( alert, testList );
            } else {
                display.setCurrent( testList );
            }
        }

        private DateField dateField;
        private boolean   save;
    }
}

Pixel

DEVELOPING CUSTOM COMPONENTS FOR THE MOBILE INFORMATION DEVICE PROFILE

A question that novice Mobile Information Device Profile (MIDP) programmers often ask is "How can I extend the MIDP user interface classes to define my own custom components?" You might want to extend the TextField class, for example, to support more kinds of constraints or alternate forms of input. Or you might want to define a completely new kind of user interface (UI) component.

Unlike the Abstract Windowing Toolkit (AWT) in J2SE, however, the MIDP user interface classes are not written with extensibility in mind. In particular, the classes that make up the high-level user interface API are not extensible. For example, the Item class, which serves as the base class for all components that can be placed on a Form, does not expose any public or protected constructors. More importantly, none of the high-level UI classes expose the low-level painting or input events required to write custom components. The only class suitable for extension is the Canvas class. This class is part of the low-level user interface API which does expose the necessary information. Unfortunately, you can't use components from both the low-level and high-level APIs at the same time. In other words, you can't place a canvas on a form, or place a form item on a canvas. While a canvas is extensible, you pay for this flexibility with increased responsibility. The canvas is responsible for painting the entire screen and for responding to all input events, except for the selection and triggering of Command objects. After you decide to write your own component class, there's no going back -- it's really an all-or-none proposition.

Be sure to explore the alternatives before you decide to write your own component class. Do you truly need a new component, or can you get by with the predefined MIDP classes? The high-level API defines a set of abstract user interface components, components that are adapted by a device manufacturer to suit the display and input characteristics of the device. Any components you write do not share this adaptability, so careful design and testing are required for them to work well across all devices. The components also increase the size of your application.

There are many possible implementations for custom components, but, in general, you need at least two classes: one class to define a "container" or "manager" for the components, and another to define the base class for those components. Here is an implementation of a possible base class:

import javax.microedition.lcdui.*;

// A root class for Canvas-based components.
// Because Area extends Canvas, you can actually
// use a component directly as a Canvas, although
// it's recommended you place it on Manager.

public abstract class Area extends Canvas {
    protected int      x;
    protected int      y;
    protected int      w;
    protected int      h; 
    protected Font     font; 
    protected Manager  parent; 
    protected int      backcolor = -1;
    protected int      forecolor = -1;

    protected Area( int x, int y, int w, int h ){
        this( x, y, w, h, null );
    }

    protected Area( int x, int y,
                    int w, int h, Font f ){
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.font = font;
    }

    // Erase the background using backcolor

    protected void eraseBackground( Graphics g ){
        g.setColor( getBackColor() );

        if( parent == null ){
            g.fillRect( 0, 0, getCanvasWidth(),
                        getCanvasHeight() );
        } else {
            g.fillRect( 0, 0, w, h );
        }
    }

    public final int getBackColor(){
        if( backcolor == -1 ){
            if( parent != null ){
                return parent.getBackColor();
            }

            backcolor = 0xFFFFFF;
        }

        return backcolor;
    }

    protected final int getCanvasHeight(){
        return super.getHeight();
    }

    protected final int getCanvasWidth(){
        return super.getWidth();
    }

    public final int getHeight(){ return h; }
    public final int getWidth(){ return w; }
    public final int getX(){ return x; }
    public final int getY(){ return y; }

    public final Font getFont(){
        if( font == null ){
            if( parent != null ){
                return parent.getFont();
            }

            font = Font.getDefaultFont();
        }

        return font;
    }

    public final int getForeColor(){
        if( forecolor == -1 ){
            if( parent != null ){
                return parent.getForeColor();
            }
            
            forecolor = 0;
        }

        return forecolor;
    }

    public Manager getParent(){ return parent; }

    public void keyPressed( int keyCode ){
    }

    public void keyReleased( int keyCode ){
    }

    public void keyRepeated( int keyCode ){
    }

    protected void moveFocus( boolean forward ){
        if( parent != null ){
            parent.moveFocus( forward );
        }
    }

    // If the area is acting like a Canvas, call
    // the real paint routine

    protected void paint( Graphics g ){
        eraseBackground( g );
        g.setColor( getForeColor() );
        g.setFont( getFont() );
        paintArea( g, true );
    }

    // The manager calls this paint routine on each of 
    // its children

    public final void paint( Graphics g,
                             boolean hasFocus ){
        int  cx = g.getClipX();
        int  cy = g.getClipY();
        int  ch = g.getClipHeight();
        int  cw = g.getClipWidth();
        Font f  = g.getFont();
        int  col = g.getColor();

        eraseBackground( g );

        g.setClip( x, y, w, h );
        g.setFont( getFont() );
        g.setColor( getForeColor() );

        paintArea( g, hasFocus );

        g.setClip( cx, cy, cw, ch );
        g.setFont( f );
        g.setColor( col );
    }

    // Subclass implements to do actual painting

    protected abstract void paintArea( 
                        Graphics g, boolean hasFocus );

    // Repaint the area of the given child

    public void repaintArea( Area child, boolean now ){
        if( parent != null ){
            parent.repaintArea( child, now );
        } else {
            repaint( child.getX(),
                     child.getY(),
                     child.getWidth(),
                     child.getHeight() );
    
            if( now ){
                serviceRepaints();
            }
        }
    }

    public void setBackColor( int col ){
         backcolor = col;
    }

    public void setForeColor( int col ){
        forecolor = col;
    }

    protected void setParent( Manager parent ){
        this.parent = parent;
    }
}

Although the root class is fairly simple, it does handle the basics. It stores the position, dimensions, colors, and fonts of the component. The Area class is meant to be subclassed. Here's an example of a simple push button component that extends Area:

import javax.microedition.lcdui.*;

public class PushButton extends Area {

    // Define the listener interface

    public interface Listener {
        void buttonPressed( PushButton which );
    }

    private String   label;
    private boolean  selected;
    private Listener listener;

    public PushButton( String label,
                       int x, int y ){
        this( label, x, y, 0, 0, null );
    }

    public PushButton( String label,
                       int x, int y,
                       Font f ){
        this( label, x, y, 0, 0, f );
    }  
    public PushButton( String label,
                       int x, int y,
                       int w, int h ){
       this( label, x, y, w, h, null );
    }

    public PushButton( String label,
                       int x, int y,
                       int w, int h,
                       Font f ){
        super( x, y, w, h, f );
        
        if( label == null ) label = "";

        int tw = calcMinWidth( label, getFont() );
        int th = calcMinHeight( label, getFont() );
        
        if( tw > w ) this.w = tw;
        if( th > h ) this.h = th;
        
        this.label = label;
    }

    public static int calcMinWidth( 
                                 String text, Font f ){
        return f.stringWidth( text ) + 8;
    }

    public static int calcMinHeight( 
                                 String text, Font f ){
        return f.getHeight() + 8;
        }

    public String getLabel(){ return label; }

    protected void paintArea( 
                        Graphics g, boolean hasFocus ){
        g.setStrokeStyle( g.SOLID );
        g.drawRect( x, y, w-1, h-1 );
        
        if( selected ){
            g.setColor( getForeColor() );
            g.fillRect( x, y, w-1, h-1 );
            g.setColor( getBackColor() );
        } else if( hasFocus ){
            g.setStrokeStyle( g.DOTTED );
            g.drawRect( x+2, y+2, w-5, h-5 );
            g.setStrokeStyle( g.SOLID );
        }
        
        g.drawString( 
                     label, x+4, y+4, g.TOP | g.LEFT );
        }

    public void keyPressed( int keyCode ){
        int action = getGameAction( keyCode );
        switch( action ){
            case UP:
            case LEFT:
                moveFocus( false );
                break;
            case DOWN:
            case RIGHT:
                moveFocus( true );
                break;
            case FIRE:
                selected = true;
                repaintArea( this, true );
                break;
        }
    }

    public void keyReleased( int keyCode ){
        int action = getGameAction( keyCode );
        switch( action ){
            case FIRE:
                selected = false;
                repaintArea( this, true );
                
                if( listener != null ){
                    listener.buttonPressed( this );
                }
                break;
        }
    }
    
    public void setListener( Listener listener ){
        this.listener = listener;
    }
}  


Most of the logic in PushButton has to do with the painting of the component. The push button can draw itself in three states: unselected with focus, unselected without focus, and selected. It responds to key events in order to change its selected state or move the focus away from it to another component. (The term focus, as used here, refers to the concept of input focus, that is, the particular component on screen to which keyboard/keypad input is directed.)

The only thing missing now is the manager class. Its purpose is to track the other components and to pass along paint and input events as appropriate.

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

// A subclass of Area that can act as
// the parent for other components.

public class Manager extends Area {
    protected Vector children = new Vector();
    protected Area   focus = null;

    public Manager(){
        super( 0, 0, 0, 0, null );
        w = getCanvasWidth();
        h = getCanvasHeight();
    }
    
    public void add( Area child ){
        if( !children.contains( child ) ){
            children.addElement( child );
            child.setParent( this );
            repaintArea( child, false );
        }
    }

    protected Area getFocus(){
        if( focus == null && children.size() > 0 ){
            focus = (Area) children.elementAt( 0 );
        }

        return focus;
    }
    
    public void keyPressed( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyPressed( keyCode );
        }
    }
    
    public void keyReleased( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyReleased( keyCode );
        }
    }
    
    public void keyRepeated( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyRepeated( keyCode );
        }
    }
    
    // Called to move the focus to the next
    // or previous component

    protected void moveFocus( boolean forward ){
        Area oldFocus = getFocus();
        if( oldFocus != null ){
            int i = children.indexOf( oldFocus );
            int last = children.size() - 1;
            if( forward ){
                if( ++i > last ) i = 0;
            } else {
                if( --i < 0 ) i = last;
            }
            
            focus = (Area) children.elementAt( i );
            repaintArea( oldFocus, false );
            repaintArea( focus, true );
        }
    }

    public void remove( Area child ){
        if( children.removeElement( child ) ){
            child.setParent( null );
            repaintArea( child, false );
        }
    }

    protected void paint( Graphics g ){
        if( focus == null ) getFocus();

        eraseBackground( g );
        g.setColor( getForeColor() );

        int n = children.size();
        for( int i = n-1; i >= 0; --i ){
             try {
                Area area = 
                    (Area) children.elementAt( i );
                area.paint( g, ( focus == area ) );
            }
            catch( Exception e ){
            }
        }
    }

    protected void paintArea( 
                        Graphics g, boolean hasFocus ){
    }
}

Finally, here's a simple MIDlet that uses the Manager and PushButton classes to display three push buttons:

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

// A simple example that shows how to use
// custom-drawn components.  Creates a
// Manager that has three PushButtons on it.

public class Tester extends MIDlet
                    implements CommandListener,
                               PushButton.Listener {

    private Display display;
    private Command exitCommand =
                         new Command( "Exit",
                                     Command.EXIT, 1 ); 

    public Tester(){
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    protected void pauseApp(){
    }

    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ // first time called...
            initMIDlet();
        }
    }

    private void initMIDlet(){
        display = Display.getDisplay( this );

        Manager m = new Manager();

        PushButton pb = new PushButton( 
                                     "First", 0, 0 );
        pb.setListener( this );
        m.add( pb );

        pb = new PushButton( "Second", 20, 20 );
        pb.setListener( this );
        m.add( pb );

        pb = new PushButton( "Third", 0, 60, 50, 0 );
        pb.setListener( this );
        m.add( pb );

        m.addCommand( exitCommand );
        m.setCommandListener( this );

        display.setCurrent( m );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public void commandAction( Command c,
                               Displayable d ){
        exitMIDlet();
    }
    
    public void buttonPressed( PushButton which ){
        String label = which.getLabel();

        Alert a = new Alert( "Pressed!",
            "You pressed the " + label + " button.",
            null, null );
            
        display.setCurrent( a, which.getParent() );
    }
}

If you're familiar with AWT or Swing programming in J2SE, the code in the initMIDlet method should look very familiar. Run this sample and you'll see three push buttons on screen, two of which are overlapping. Use the UP, DOWN, LEFT and RIGHT keys to move the input focus from one button to another, and press and release the FIRE key to display an alert.

Use the code above as the basis for your own custom component coding. There are many improvements you can make. For example, you could add support for pointer events. Or you can optimize the painting and support the hiding of components. Start with this small bit of code and only add the functionality that you need.

Pixel

IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/

- FEEDBACK

Comments? Send your feedback on the J2ME Tech Tips to:

jdc-webmaster@sun.com

- SUBSCRIBE/UNSUBSCRIBE

- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- To use our one-click unsubscribe facility, see the link at the end of this email:

- ARCHIVES

You'll find the J2ME Tech Tips archives at: http://java.sun.com/jdc/J2METechTips/index.html

- COPYRIGHT

Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:

http://java.sun.com/jdc/copyright.html

J2ME Tech Tips November 14, 2001

Sun, Sun Microsystems, Java, Java Developer Connection, J2ME, and J2SE are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.