|
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.
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;
}
}
|
 |
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.
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.
|