Friday, October 24, 2008

Vertically centered text

Images can be easily drawn at the center of the screen because Graphics.drawImage supports both anchors: HCENTER and VCENTER. To drawString we can use only HCENTER. If VCENTER is passed as argument an IllegalArgumentException is raised.
So how do we draw vertically centered text?
The maximum number of lines that fits the screen is obtained with a division:

int maxLinesPerScreen = getHeight() / Font.getDefaultFont().getHeight();

On a previous post we presented a way to split an String depending on the screen width. The result was a String array where each position represents a line that fits the screen. The programmer can choose to draw the text at any horizontal alignment.
But we can show them vertically centered too. We just need to calculate the pixel were the first
line is drawn 

int remainingLines = this.message.length - this.messageFirstLineShown;
int y = (remainingLines < maxLinesPerScreen) ?
  (getHeight() - (remainingLines * Font.getDefaultFont().getHeight())) / 2 : 
  (getHeight() - (maxLinesPerScreen * Font.getDefaultFont().getHeight())) / 2;
for (int i = this.messageFirstLineShown; i < this.message.length; i++) {
  g.drawString(this.message[i], getWidth() / 2, y, Graphics.HCENTER | Graphics.TOP);
  y += g.getFont().getHeight();
  // it was the last line that could be drawn
  if (y + Font.getDefaultFont().getHeight() > getHeight()) {
i = this.message.length; // stop the loop
    }
}

Sunday, October 19, 2008

Work with proportions

Lets say you have some data to present, for example, the amount of money per month you spend with food. How do we create an application that shows this data as columns?
First thing is to know how much space the application has to use. This can be achieved with a Canvas child class calling the methods getWidth() and getHeight().
The number of columns displayed at a time depends on the screen width and the font used to draw the month name. To make things easier I suggest using a Font with FACE_MONOSPACE and let the column be the same width of the month short name. You may vary style and size, but this will not change the result greatly.
As the columns should not touch each other we will add an one pixel gap between them.

String [] months = new String [] {
    "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
    "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
Font monospacedFont = Font.getFont(
    Font.FACE_MONOSPACE, Font.STYLE_PLAIN,
    Font.SIZE_MEDIUM);
int columnWidth = monospacedFont.stringWidth(months[0]);
// 1 == gap between columns
int numColumns = (getWidth() - 1) / (columnWidth + 1);

Next thing we need to know is the highest value in the data set and how many pixels we will use to represent this special column.

// 1 == gap to the top of the screen
// 2 == space to draw month name and value
int maxColumnHeight = getHeight() - (2 * monospacedFont.getHeight()) - 1;
int highestValue = 0;

for (int i = 0; i < data.length; i++) {
if (data[i] > highestValue) {
highestValue = data[i];
}
}

Now we do the painting:

int x = 1; // 1 == gap to the left side of the screen
int y = getHeight() - monospacedFont.getHeight();

// clear the screen
g.setColor(0xffffff); // white
g.fillRect(0, 0, getWidth(), getHeight());

g.setFont(monospacedFont);
g.setColor(0); // black

for (int i = 0; i < numColumns; i++) {
  int columnHeight = (data[i] * maxColumnHeight) / highestValue;
  g.fillRect(x, y - columnHeight, columnWidth, columnHeight);
  // month name at the bottom
  g.drawString(months[i], x, y, Graphics.LEFT | Graphics.TOP);
  // data value on top of the column
  g.drawString(String.valueOf(data[i]), x, y - columnHeight, Graphics.LEFT | Graphics.BOTTOM);
  x = x + columnWidth + 1;
}

Below are two screen shots with different resolutions:








Friday, October 17, 2008

The smallest MIDlet

A Java ME application must have at least on class that extends MIDlet. If the application has custom interface there must be at least one class that extends Canvas. In this case, how short can be the MIDlet?

import javax.microedition.lcdui.Display;
import javax.microedition.midlet.MIDlet;

public class C extends MIDlet {

  MyCanvas cCanvas;

  public C() {
    this.cCanvas = new MyCanvas(this);
  }

  protected void startApp() {
    if (Display.getDisplay(this).getCurrent() == null) {
      Display.getDisplay(this).setCurrent(this.cCanvas);
    }
  }

  protected void pauseApp() { }

  protected void destroyApp(boolean arg0) { }
}

We use the default package because packages are directories inside the jar file and we want to keep the jar flat.
The class name is just one letter because the obfuscator is configured to not change the name of classes that extends MIDlet. So, we take the smallest name possible.
The only attribute is of the type of the class that extends Canvas. The instance is created at MIDlet constructor because initialization during declaration creates bigger class files.
At startApp we avoid the use of a local variable of type Display, this also reduces the final class size.
We removed the throws clause of startApp and destroyApp because the import entry of MIDletStateChangeException is not needed, reducing the class size.
MyCanvas class receives the MIDlet as parameter at constructor to call notifyDestroyed when the user selects an Exit command. This is the only class that implements CommandListener.
With this minimum MIDlet all application logic is kept inside MyCanvas.

Monday, October 13, 2008

A better Alert

Alert is a good screen for short messages like: "Settings saved", "Restored default values", "Network error", etc. A TextBox can only be used for user input. If you want to show a help text, with multiple lines, there is another way. On a previous post I showed a way to use Font.stringWidth, now we will see one more.
Assume you have the following attributes:

String [] message;
int messageFirstLineShown;
Command nextCommand;

First you will have to break your message in lines that fit the screen.

char[] chars = ("My long text that will not fit the screen with just one line.\n"
    + "Second line.\nThird line").toCharArray();
StringBuffer buffer = new StringBuffer();
Vector vLines = new Vector();

for (int i = 0; i < chars.length; i++) {
  if (chars[i] == ' ' || chars[i] == '.') {
  String s = buffer.toString();
  // filled a line
  if (Font.getDefaultFont().stringWidth(s) > getWidth()) {
  vLines.addElement(s.substring(0, s.lastIndexOf(' ')));
  buffer.setLength(0);
  buffer.append(s.substring(s.lastIndexOf(' ') + 1));
  }
  buffer.append(chars[i]);
  } else if (chars[i] == '\n') {
  vLines.addElement(buffer.toString());
  buffer.setLength(0);
  } else {
  buffer.append(chars[i]);
  }
}
vLines.addElement(buffer.toString());
this.message = new String[vLines.size()];
vLines.copyInto(this.message);
this.messageFirstLineShown = 0;

When the time comes when you need to paint the message, use the following code:

int y = 0;
for (int i = this.messageFirstLineShown; i < message.length; i++) {
    g.drawString(this.message[i], 0, y, Graphics.LEFT | Graphics.TOP);
    y += g.getFont().getHeight();
}

If you have more lines in message than the screen can show, you will need to add a nextCommand = Command("Next", Command.OK, 1)  to increase the value of messageFirstLineShown.
To know if this is the case check if ((getHeight() / Font.getDefaultFont().getHeight()) > message.length) and at commandAction add the following treatment:

int maxLines = this.getHeight() / Font.getDefaultFont().getHeight();
int remainingLines = this.message.length - this.messageFirstLineShown;

this.messageFirstLineShown += maxLines;
if (remainingLines < maxLines) {
    this.removeCommand(nextCommand);
}

Wednesday, October 8, 2008

Fewer classes

Each class definition creates a corresponding class file, even nested classes. The fewer classes you have the smaller your jar file will be. A Java ME application may have only one class, the MIDlet, and this single class can implement CommandListener and be responsible for all application GUI.
If you are developing a game or an application with custom user interface, you will need another class that extends Canvas, but this is the bare minimum.
Having all your application code in one or two classes is not Object Oriented programming. It is not easy to develop like this if you use a simple text editor, but it can be done with current IDEs like Eclipse or NetBeans.
Keep in mind that, for achieving small jar files, you will need to use development techniques not related to OO.

Tuesday, October 7, 2008

Centered text

When you want to draw some centered text, lets say, "Press any key" on your Splash, you can use

void paint (javax.microedition.lcdui.Graphics g) {
  g.drawString("Press any key", getWidth() / 2, getHeight(), Graphics.HCENTER | Graphics.BOTTOM);
}

But what if the default font is so small you can not read easily. Or worse, your text width is bigger than the screen width? To be sure you can use Font.stringWidth. Inside your paint method do something like:

int [] sizes = new int [] {Font.SIZE_LARGE, Font.SIZE_MEDIUM, Font.SIZE_SMALL};
int i = 0;
Font defaultFont = g.getFont();
// keep the same face and style, just want the biggest size that fits the screen
Font newFont = Font.getFont(defaultFont.getFace(), defaultFont.getStyle(), sizes[i]);
String m = "single line of text that fit the screen";

while (newFont.stringWidth(m) > getWidth()) {
i++;
newFont = Font.getFont(defaultFont.getFace(), defaultFont.getStyle(), sizes[i]);
}
g.setFont(newFont);

Sunday, October 5, 2008

Optimize your images

Java ME applications may use images in the PNG (Portable Network Graphics) format, version 1.0, but at least the critical chunks must be present. As ancillary chuncks are not necessary you may use a tool to remove them.
One such tool is PNGGauntlet. It is strongly recommended that you use it, as you may up to 60% compression of an image file.

Thursday, October 2, 2008

Showing a Splash

When customizing the User Interface with Canvas you can obtain the screen resolution with getWidth and getHight methods. Lets say you want to show a Splash screen to the user and have an image with your logo. How would you use it?
Well, a single image will not fit all screens. But two will do.
The smaller logo image should be less than 96x54, as this is the smallest screen resolution. This image can be used up to the resolution of 128x128 without problems. With bigger resolutions it will look tiny, though.
The bigger logo image should be a bit bigger than 128x128 and can be used up to 240x320.
The code bellow gives as example of how to implement this.

import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.Graphics;

class Splash extends javax.microedition.lcdui.Canvas {

  Image logo;

  Splash () {
    if (getWidth() <= 128) {
      // sl stands for Small Logo and does not need to have a file extension
      // this will use less space on the jar file
      logo = Image.createImage("/sl");
    } else {
      // bl stands for Big Logo
      logo = Image.createImage("/bl");
    }
  }

  protected void paint (Graphics g) {
    // With these anchors your logo image will be drawn on the center of the screen.
    g.drawImage(logo, getWidth()/2, getHeight()/2, Graphics.HCENTER | Graphics.VCENTER);
  }
}

Wednesday, October 1, 2008

Obfuscate your classes

An obfuscator can shrink and optimize your classes. Unused classes, fields, methods and attributes may be removed. The remaining code may be renamed to short meaningless names.
I use Proguard as an Ant task since 2003 and have good results with it.
Below are the settings I often use on my projects:
usemixedcaseclassnames="off", if class names reach z the next name will be aa instead of A.
defaultpackage="", all obfuscated classes are moved to the default package.
overloadaggressively="true", multiple fields and methods can get the same names, as long as their arguments and return types are different.
optimizationpasses="3", number of optimization passes to be performed. This may vary from one application to another. Check the final jar size, when it does not shrink any further, stop incresing this value.
printmapping="./mapping.txt", print the mapping from old names to new names for classes and class members that have been renamed. Useful when exceptions happen.
printusage="./deadcode.txt", list dead code of the input class files. Gives you tips of code that is not needed.
The only classes I keep are those that extends javax.microedition.midlet.MIDlet. So, for best jar size, my MIDlets are placed on default package. This way all classes are found at jar root.