Sun Java Solaris Communities My SDN Account Join SDN
 
Article

Writing a Custom Counter Component

 
By Eric Giguere, October 2004  

Download the source

On occasion you may want your MIDP application to display a counter, for example to display the number of seconds elapsed since some operation started. It's fairly simple to write a custom component that automatically updates and displays such a counter. With a bit of careful coding you can even make it work with canvases, custom application-drawn screens derived from the Canvas class, and on forms, using the MIDP 2.0 CustomItem class.

The key to making a dual-purpose component is to separate the drawing of the component from its management. The reason is that there is no concept of a component when working with a canvas as a whole; the application itself must draw and manage the entire canvas. A custom item, on the other hand, is a true component; the system delegates drawing to the component but handles most of the management itself. To handle this difference, you define a callback interface that your component uses to delegate management tasks to the appropriate code.

Here's the core of a sample counter component, the CounterArea class:

import javax.microedition.lcdui.*;

// The counter class, which can be used on a canvas
// or wrapped within a custom item.

public class CounterArea {

    public static final int DEFAULT_RATE = 500;
    public static final int MIN_RATE = 100;

    // The callback interface by which we notify
    // the counter owner of certain events.

    public interface Callback {
        void invalidateCounter( CounterArea counter );
        void repaintCounter( CounterArea counter );
        void resizeCounter( CounterArea counter );
    }

    public CounterArea(){
    }

    public CounterArea( int width, int height ){
        _width = width;
        _height = height;
    }

    public int getBackColor(){
        return _backColor;
    }

    public Callback getCallback(){
        return _callback;
    }

    public Font getFont(){
        return _font;
    }

    public Font getFontForDrawing(){
        return _font != null ? _font :
                               Font.getDefaultFont();
    }

    public int getHeight(){
        if( _height < 0 ){
            _height = getMinHeight();
        }

        return _height;
    }

    public int getMinHeight(){
        return getFontForDrawing().getHeight();
    }

    public int getMinWidth(){
        Font f = getFontForDrawing();
        return f.stringWidth( Integer.toString( _value ) );
    }

    public int getRate(){
        return _rate;
    }

    public int getTextColor(){
        return _textColor;
    }

    public int getValue(){
        return _value;
    }

    public int getWidth(){
        if( _width < 0 ){
            _width = getMinWidth();
        }

        return _width;
    }

    private void invalidate(){
        if( _callback != null ){
            _callback.invalidateCounter( this );
        }
    }

    public boolean isCounting(){
        return _timer != null;
    }

    public void paint( Graphics g ){
        String s = Integer.toString( _value );
        Font   f = getFontForDrawing();
        int    w = f.stringWidth( s );
        int    h = f.getHeight();

        int   aw = getWidth();
        int   ah = getHeight();

        g.setColor( _backColor );
        g.fillRect( _left, _top, aw, ah );

        g.setColor( _textColor );
        g.drawString( s, _left + aw - w,
                      _top + ( ah - h ) / 2,
                      g.TOP | g.LEFT );

        if( w > aw || h > ah ){
            resize();
        }
    }

    private void repaint(){
        if( _callback != null ){
            _callback.repaintCounter( this );
        }
    }

    private void resize(){
        if( _callback != null ){
            _callback.resizeCounter( this );
        }
    }

    private synchronized boolean increment( Runnable source ){
        if( source != _timer ) return false;

        ++_value;

        repaint();
        return true;
    }

    public void setBackColor( int color ){
        _backColor = color;
        invalidate();
    }

    public void setCallback( Callback callback ){
        _callback = callback;
    }

    public void setLeft( int left ){
        _left = left;
    }

    public void setFont( Font f ){
        _font = f;
        invalidate();
    }

    public void setHeight( int h ){
        _height = h;
    }

    public void setRate( int rate ){
        _rate = ( rate < MIN_RATE ? MIN_RATE : rate );
    }

    public void setTextColor( int color ){
        _textColor = color;
        invalidate();
    }

    public void setTop( int top ){
        _top = top;
    }

    public synchronized void setValue( int value ){
        _value = value;
    }

    public void setWidth( int w ){
        _width = w;
    }

    public synchronized void start(){
        _timer = new CounterTimer();
        new Thread( _timer ).start();
    }

    public synchronized void stop(){
        _timer = null;
    }

    private int       _backColor = 0x00FFFFFF;
    private Callback  _callback;
    private Font      _font;
    private int       _height = -1;
    private int       _index;
    private int       _left;
    private int       _rate = DEFAULT_RATE;
    private int       _textColor = 0x00000000;
    private Runnable  _timer;
    private int       _top;
    private int       _width = -1;
    private int       _value;

    //-------------------------------------------------

    // A very simple timer that sleeps and wakes
    // up at regular intervals.

    private class CounterTimer implements Runnable {
        public void run(){
            Thread t = Thread.currentThread();

            while( true ){
                try {
                    t.sleep( _rate );
                }
                catch( InterruptedException e ){
                }

                if( !increment( this ) ) break;
            }
        }
    }
}

The counter paints its value centered horizontally and vertically within its drawing area and attempts to resize that area if its dimensions are too small. It also defines a public nested interface, CounterArea.Callback, to delegate the management tasks of invalidating, repainting, and resizing the component. A private inner class, CounterTimer, updates the counter value in the background.

The CounterArea class can be used directly on a canvas, but you can also wrap it with a custom item. The wrapper class, CounterItem, looks like this:

import javax.microedition.lcdui.*;

// A custom component for MIDP 2.0 that wraps
// a CounterArea instance.

public class CounterItem extends CustomItem
                         implements CounterArea.Callback {

    public CounterItem(){
        super( null );

        _area = new CounterArea();
        _area.setCallback( this );
    }

    public Font getFont(){
        return _area.getFont();
    }

    public int getMinContentHeight(){
        return _area.getMinHeight();
    }

    public int getMinContentWidth(){
        return _area.getMinWidth();
    }

    public int getPrefContentHeight( int width ){
        return getMinContentHeight();
    }

    public int getPrefContentWidth( int height ){
        return getMinContentWidth();
    }

    public int getValue(){
        return _area.getValue();
    }

    protected void hideNotify(){
        _area.stop();
    }

    public void invalidateCounter( CounterArea counter )
        if( counter == _area ){
            invalidate();
        }
    }

    protected void paint( Graphics g, int width, int height ){
        _area.paint( g );
    }

    public void repaintCounter( CounterArea counter ){
        if( counter == _area ){
            repaint();
        }
    }

    public void resizeCounter( CounterArea counter ){
        if( counter == _area ){
            invalidate();
        }
    }

    protected void sizeChanged( int w, int h ){
        _area.setWidth( w );
        _area.setHeight( h );
    }

    public void setFont( Font f ){
        _area.setFont( f );
    }

    public void setValue( int value ){
        _area.setValue( value );
    }

    protected void showNotify(){
        _area.start();
    }

    public boolean traverse( int dir, int vw, int vh,
                             int[] vrect ){
        return false;
    }

    private CounterArea _area;
}

The wrapper does very little beyond acting as a sink for the management callback and for automatically starting and stopping the counter whenever the item is displayed or hidden. You might wish to change this behavior, of course.

Finally, here's a simple test MIDlet that demonstrates the use of the counter:

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

// A simple MIDlet to test the custom counter
// component.

public class CounterItemTest extends MIDlet
                 implements CommandListener {

    private Display display;

    public static final Command exitCommand =
                         new Command( "Exit",
                                      Command.EXIT, 1 ); 

    public CounterItemTest(){
    }

    public void commandAction( Command c,
                               Displayable d ){
        if( c == exitCommand ){
            exitMIDlet();
        }
    }

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

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public Display getDisplay(){ return display; }

    protected void initMIDlet(){
        Form f = new Form( "CounterItem Test" );
        f.addCommand( exitCommand );
        f.setCommandListener( this );

        CounterItem counter = new CounterItem();
        counter.setLayout( Item.LAYOUT_CENTER |
                      Item.LAYOUT_NEWLINE_BEFORE |
                      Item.LAYOUT_NEWLINE_AFTER );

        f.append( counter );

        getDisplay().setCurrent( f );
    }

    protected void pauseApp(){
    }

    protected void startApp()
                      throws MIDletStateChangeException {
        if( display == null ){ 
            display = Display.getDisplay( this );
            initMIDlet();
        }
    }
}

You can improve the counter in various ways, of course, such as by having it display elapsed time instead of a series of integers. Have fun exploring the possibilities.