October 22, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

MIDP Programming with J2ME

  • December 26, 2002
  • By Sams Publishing
  • Send Email »
  • More Articles »

Foreground and Background Notifications

For several reasons, the Canvas may move into the background—for example, if the display is set to another displayable object or if the device displays a system dialog. In these cases, the Canvas is notified by the hideNotify() method. When the Canvas becomes visible (again), the corresponding counterpart, showNotify(), is called.

Javagochi Example

Now that you are familiar with the Canvas object and the basic drawing methods of the Graphics class, you are ready to develop a small interactive application, the Javagochi.

As you can see in the following code, the MIDlet implementation of Javagochi is already finished, but the Face class is missing:

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

public class Javagochi extends MIDlet {
  
  static final int IDEAL_WEIGHT = 100;
  Display display = Display.getDisplay (this);
  Face face = new Face (this);
  int weight = IDEAL_WEIGHT;
  Timer consumption;
  int score;

Before you begin development, let us first say a few words about the Javagochi itself. A Javagochi has a weight that is initialized with its IDEAL_WEIGHT. It also owns an instance of Display, Face, and Consumption, which will be explained later. Finally, it stores a score value for the care the owner spends on the Javagochi.

The happiness of the Javagochi is determined by the deviation of its current weight from the ideal weight, ranging from 10 to 0:

public int getHappiness () {
  return 20 - (weight > IDEAL_WEIGHT 
         ? 10 * weight / IDEAL_WEIGHT 
         : 10 * IDEAL_WEIGHT / weight);
  if (happiness < 0) happiness = 0;
  if (happiness > 10) happiness = 10;
}

This formula also demonstrates how to circumvent problems with the absence of floating point arithmetic. In order to avoid loss of significant fractions, the values are scaled up before division.

Like all other known life forms, the Javagochi can die. Javagochies only die from sadness when their happiness level reaches zero:

public boolean isDead () {
  return getHappiness <= 0;
}

The only other action a Javagochi can perform besides dying is to transform energy to matter and back. Since a weight change may change the Javagochi's look, a repaint is requested in the transform() method:

public void transform (int amount) {
  if (!isDead ()) {
    weight += amount;
    face.repaint ();
  }
}

When the Javagochi MIDlet is started, it displays itself and starts a consumption Timer that keeps track of the power the Javagochi needs for living:

public void startApp () {
  display.setCurrent (face);
  consumption = new Consumption (this).start ();
}

When the MIDlet is paused, the Javagochi goes to sleep by telling the consumption thread to terminate itself. The destroyApp() method does nothing because the life cycle will enter sleep anyway, and no further cleanup is needed:

  public void pauseApp () {
    consumption.leave = true;
  }

  public void destroyApp (boolean forced) {
  }
}

The consumption Thread is a separate class that monitors the power the Javagochi needs for living. In the run() method, every 0.5 seconds the score is updated depending on the Javagochi's happiness and the small amount of body mass that is transformed back to life energy:

public class Consumption extends Thread {

  Javagochi javagochi;
  boolean leave = false;

  public Consumption (Javagochi javagochi) {
    this.javagochi = javagochi;
  }

  public void run () {
    while (!leave) {
      try {
        sleep (500);
      }
      catch (InterruptedException e) {break;}
      javagochi.score += 10 - javagochi.deviation;
      javagochi.transform (-5);
    }
  }
}

Now that you know how a Javagochi works, it is your job to give the Javagochi an appropriate appearance by implementing the missing Face class.

Scaling and Fitting

In many cases, it is a good idea to scale displayed graphics depending on the actual screen size. Otherwise, the display will look nice on one particular device type, but won't fit the screen on devices with a lower screen resolution or become unnecessarily small on devices with higher screen resolutions.

We will now show how scaling works for the Javagochi example. A picture of a Javagochi is shown in Figure 3.19. You will start by drawing the shape of the face, a simple ellipse. In this case, the ellipse will reflect the Javagochi's weight. If the Javagochi is at its ideal weight, the ellipse becomes a circle.

Figure 3.19 A happy Javagochi at its ideal weight.

In order to leave some space for the Javagochi to grow, the diameter of the ideal circle is half the minimum of the screen width and height. Thus, the height of the Javagochi is calculated using the following formula:

int height = Math.min (getHeight (), getWidth ()) / 2;

Based on the current weight, the ideal weight, and the calculated height, which is also the diameter of the "ideal" Javagochi, you can now calculate the width of the Javagochi:

int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;

Other applications may of course have other dependencies from the actual screen size, but this example should be sufficient to show the general idea.

The Javagochi's skin color is dependent on its happiness. If the Javagochi feels well, its skin has a bright yellow color. With decreasing happiness, the Javagochi becomes pale. This is reflected by the following setColor() command:

setColor (255, 255, 28 * javagochi.happiness);

Using the given width and height, you can now implement your first version of the Javagochi's Face class:

import javax.microedition.lcdui.*;
class Face extends Canvas implements CommandListener {
  Javagochi javagochi;

  Face (Javagochi javagochi) {
    this.javagochi = javagochi;
  }
  
  public void paint (Graphics g) {
    g.setColor (255, 255, 255);
    g.fillRect (0, 0, getWidth (), getHeight ());
    
    int height = Math.min (getHeight (), getWidth ()) / 2;
    int width = height * javagochi.weight / javagochi.IDEAL_WEIGHT;
  
    g.translate (getWidth () / 2, getHeight () / 2);
  
    g.setColor (255, 255, 255 - javagochi.getHappiness () * 25);
    g.fillArc (- width / 2, - height / 2, width, height, 0, 360);

    g.setColor (0, 0, 0);
    g.drawArc (- width / 2, - height / 2, width, height, 0, 360);
  } 
} 

In order to simplify the centered display of the Javagochi, you set the origin of the coordinate system to the center of the screen using the translate() method. The outline of the Javagochi's face is then drawn using the drawArc() method.

Unfortunately, the outline of the Javagochi looks a bit boring, so you will add a simple face now. In order to avoid duplicated code, you put the drawing of the eyes in a separate method. The drawEye() method takes the Graphics object, the coordinates of the eye, and a size parameter:

void drawEye (Graphics g, int x, int y, int size) {
  if (javagochi.isDead ()) {
    graphics.drawLine (x - size/2, y, x + size/2, y);
    graphics.drawLine (x, y - size/2, x, y + size/2);
  }
  else 
    graphics.drawArc (x-size/2, y-size/2, size, size, 0, 360); 
}

Now you can insert the rest of the drawing code into the paint() method, just after drawArc(). You will start with the eyes by calling the drawEye() method defined previously. By using fractions of the current width and height of the Javagochi, the eyes are positioned and sized correctly:

drawEye (g, - width / 6, - height / 5, height / 15 + 1);
drawEye (g, width / 6, - height / 5, height / 15 + 1);

Now you draw the mouth, depending on the current happiness of the Javagochi. Again, you use fractions of the Javagochi size for positioning and sizing:

switch (javagochi.getHappiness () / 3) {
case 0:
case 1: g.drawArc (-width/6, height/7, width/3, height/6, 0, 180); break;
case 2: g.drawLine (-width/6, height/7, width/6, height/7); break;
default: g.drawArc (-width/6, height/7, width/3, height/6, 0, -180); 
}

Simple Interaction

When you run the first version of the Javagochi application, the Javagochi starts out happy, but dies quickly from starvation. Obviously, you need a way to transfer energy from the device's battery to the Javagochi. One possibility would be to add a corresponding command.

However, in the "High-Level API" section you learned that commands may be delegated to a sub-menu. When the Javagochi urgently needs feeding, you would like to be able to react quickly.

So you just use the key event corresponding to the game action FIRE for feeding the Javagochi:

public void keyPressed (int keyCode) {
  if (getGameAction (keyCode) == FIRE)
    javagochi.transform (10);
}

Now you can save the Javagochi from starvation using the FIRE game key.

Canvas and Text Input

As mentioned in the introduction to interaction, it is not possible to receive composed key events using the low-level API. But what can you do if you need this kind of input, such as for a text input trainer?

Let's just assume simple feeding is not enough for your Javagochi. Depending on its current state, it needs special vitamins, denoted by letters ranging from A to Z. On phones providing keys 0 through 9 only, this is a problem. The only solution is to emulate the key input mechanism in software. On cellular phones, there are also three to four letters printed on the number keys. In text input mode, pressing a number makes the first letter appear. If the same number is pressed again in a limited period of time, the second letter appears instead of the first one. This way you can cycle through all the letters on a number key. When no key is pressed for about three quarters of a second, or another key is pressed, the letter currently displayed is confirmed as input key.

For emulation of this mechanism, you define the letters on the keys 2 through 9 in a String array inside the Face class:

public static final String[] keys = {"abc", "def", "ghi", "jkl", 
                   "mno", "pqrs", "tuv", "wxyz"};

You also need a timer to measure the time until confirmation of the current key. The timer is stored in keyTimer. The variables keyMajor and keyMinor contain the index in the keys array and the index inside the corresponding string. The variable needed stores the vitamin currently needed by the Javagochi:

Timer keyTimer;
int keyMajor = -1;
int keyMinor;
char needed = 'a';

What do you do if a numeric key is pressed? If you already have a timer running, you cancel it since a key was pressed. Then, you subtract the code of the 2 key from the current key code in order to calculate the index in your key array. If the given event does not represent a numeric key between 2 and 9, you set keyMajor to the special value –1, denoting that no valid character is being entered. Otherwise, you check whether the key is identical to the last key. If so, keyMinor is incremented in order to cycle through the letters assigned to a single numeric key. If another key is pressed, keyMajor is changed accordingly and keyMinor is set back to 0. A new timer is scheduled for half a second later:

public synchronized void keyPressed (int keyCode) {

  if (keyTimer != null) keyTimer.cancel ();

  int index = keyCode - KEY_NUM2; 

  if (index < 0 || index > keys.length) 
    keyMajor = -1;
  else {
    if (index != keyMajor) {
      keyMinor = 0;
      keyMajor = index;
    }
    else {
      keyMinor++;
      if (keyMinor >= keys [keyMajor].length ()) 
        keyMinor = 0;
    } 
  
    keyTimer = new Timer ();
    keyTimer.schedule (new KeyConfirmer (this), 500);
  }
  repaint ();
}

Now you need to implement a timer task that confirms the letter if no other key is pressed for half a second. In that case, the KeyConfirmer class just calls keyConfirmed() in the original Face class:

import java.util.*;

public class KeyConfirmer extends TimerTask {
  
  Face face;

  public KeyConfirmer (Face face) {
    this.face = face;
  }

  public void run () {
    face.keyConfirmed ();
  }
}

Back in the Face class, you can now implement the functionality performed when the letter is finally confirmed. You just compare the letter to the vitamin needed by the Javagochi. If the right vitamin is fed, the weight of the Javagochi is increased 10 units by calling transform():

  synchronized void keyConfirmed () {
    if (keyMajor != -1) {
      
      if (keys [keyMajor].charAt (keyMinor) == needed) {
        javagochi.score += javagochi.getHappiness ();

        if (!javagochi.isDead ())
          needed = (char) ('a' 
            + ((System.currentTimeMillis () / 10) % 26));
        
        javagochi.transform (10);
      }

      keyMajor = -1;
      repaint ();
    }
  }
}

Finally, you add some status information about the current score and selected key to the Face.paint() method. Just insert the following code at the end of the previous implementation of paint():

String keySelect = "";
if (keyMajor != -1) {
  String all = keys [keyMajor];
  keySelect = all.substring (0, keyMinor) + "[" + all.charAt (keyMinor) 
                + "]" + all.substring (keyMinor+1);
}    

g.drawString ("Feed: " + needed + " " + keySelect, 0, 
       getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER); 
g.drawString ("Score: "+javagochi.score, 0, 
       -getHeight ()/2, Graphics.TOP|Graphics.HCENTER);     

Figure 3.20 shows the Javagochi being fed with vitamins. The complete source code is contained in Listing 3.2.

Figure 3.20 A Javagochi being fed with vitamins.

Listing 3.2 Javagochi.java—The Complete Javagochi Sample Source Code

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


class Consumption extends TimerTask {

  Javagochi javagochi;

  public Consumption (Javagochi javagochi) {
    this.javagochi = javagochi;
  }


  public void run () {
    javagochi.transform (-1 - javagochi.score/100 );
  }
}


class KeyConfirmer extends TimerTask {
  
  Face face;

  public KeyConfirmer (Face face) {
    this.face = face;
  }

  public void run () {
    face.keyConfirmed ();
  }
}


class Face extends Canvas {

  public static final String[] keys = {"abc", "def", "ghi", "jkl", 
                     "mno", "pqrs", "tuv", "wxyz"};

  Javagochi javagochi;
  Timer keyTimer;

  int keyMajor = -1;
  int keyMinor;
  char needed = 'a';

  Face (Javagochi javagochi) {
    this.javagochi = javagochi;
  }
  
  public void paint (Graphics g) {
    g.setColor (255, 255, 255);
    g.fillRect (0, 0, getWidth (), getHeight ());

    int height = Math.min (getHeight (), getWidth ()) / 2;
    int width = height * javagochi.weight 
      / javagochi.IDEAL_WEIGHT;
  
    g.translate (getWidth () / 2, getHeight () / 2);
  
    g.setColor (255, 255, 255 - javagochi.getHappiness () * 25);
    g.fillArc (- width / 2, - height / 2, width, height, 0, 360);

    g.setColor (0, 0, 0);
    g.drawArc (- width / 2, - height / 2, width, height, 0, 360);

    g.drawString ("Score: "+javagochi.score, 0, -getHeight ()/2, 
           Graphics.TOP|Graphics.HCENTER);

    String keySelect = "";
    if (keyMajor != -1) {
      String all = keys [keyMajor];
      keySelect = all.substring 
              (0, keyMinor) + "[" + all.charAt (keyMinor) 
               + "]" + all.substring (keyMinor+1);
    }

    g.drawString ("Feed: " + needed + " " + keySelect, 
           0, getHeight ()/2, Graphics.BOTTOM|Graphics.HCENTER); 

    drawEye (g, - width / 6, - height / 5, height / 15 + 1);
    drawEye (g, width / 6, - height / 5, height / 15 + 1);

    switch (javagochi.getHappiness () / 3) {
    case 0:
    case 1:
      g.drawArc (-width/6, height/7, width/3, height/6, 0, 180);
      break;
    case 2:      
      g.drawLine (-width/6, height/7, width/6, height/7);
      break;
    default:
      g.drawArc (-width/6, height/7, width/3, height/6, 0, -180); 
    }
  } 

  void drawEye (Graphics graphics, int x0, int y0, int w) {
    if (javagochi.isDead ()) {
      graphics.drawLine (x0 - w/2, y0, x0 + w/2, y0);
      graphics.drawLine (x0, y0 - w/2, x0, y0 + w/2);
    }
    else 
      graphics.fillArc (x0-w/2, y0-w/2, w, w, 0, 360); 
  }

  public synchronized void keyPressed (int keyCode) {

    int index = keyCode - KEY_NUM2; 

    if (keyTimer != null) keyTimer.cancel ();

    if (index < 0 || index > keys.length) 
      keyMajor = -1;
    else {
      if (index != keyMajor) {
        keyMinor = 0;
        keyMajor = index;
      }
      else {
        keyMinor++;
        if (keyMinor >= keys [keyMajor].length ()) 
          keyMinor = 0;
      } 
    
      keyTimer = new Timer ();
      keyTimer.schedule (new KeyConfirmer (this), 500);
    }
    repaint ();
  }

  synchronized void keyConfirmed () {
    if (keyMajor != -1) {
      
      if (keys [keyMajor].charAt (keyMinor) == needed) {
        javagochi.score += javagochi.getHappiness ();

        if (!javagochi.isDead ())
          needed = (char) ('a' 
            + ((System.currentTimeMillis () / 10) % 26));
        
        javagochi.transform (10);
      }

      keyMajor = -1;
      repaint ();
    }
  }
}


public class Javagochi extends MIDlet {
  
  static final int IDEAL_WEIGHT = 100;

  Display display;
  Face face = new Face (this);
  int weight = IDEAL_WEIGHT;
  Timer consumption;
  int score;

  public int getHappiness () {
    int happiness = 20 - (weight > IDEAL_WEIGHT 
           ? 10 * weight / IDEAL_WEIGHT 
           : 10 * IDEAL_WEIGHT / weight);
    if (happiness < 0) happiness = 0;
    else if (happiness > 10) happiness = 10;
    return happiness;
  }

  public boolean isDead () {
    return getHappiness () == 0;
  }
  
  public void transform (int amount) {
    if (!isDead ()) {
      weight += amount;
      face.repaint ();
    }
  }

  public void startApp () {
    display = Display.getDisplay (this);
    display.setCurrent (face);
    consumption = new Timer ();
    consumption.scheduleAtFixedRate (new Consumption (this), 500, 500);
  }

  public void pauseApp () {
    consumption.cancel ();
  }

  public void destroyApp (boolean forced) {
  }
}




Page 7 of 8



Comment and Contribute

 


(Maximum characters: 1200). You have characters left.

 

 


Sitemap | Contact Us

Rocket Fuel