Monday, December 15, 2008

Custom Monospaced Font

If you want to use custom fonts the easiest one to start with is Monospaced.
As each character has the same width a simple math can define the correct point to draw.
Lets start with just numbers, from zero (Unicode value 48) to nine (Unicode value of 57).
Below is a PNG image of a very small font with three pixels width and five pixels height.




The following class can be used to draw numbers using this image.

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

public class MonospacedFont {

private Image image;
private char firstChar;
private int numChars;
private int charWidth;
public MonospacedFont(Image image, char firstChar, int numChars) {
if (image == null) {
throw new IllegalArgumentException("image == null");
}
// the first visible Unicode character is '!' (value 33)
if (firstChar <= 33) {
throw new IllegalArgumentException("firstChar <= 33");
}
// there must be at lease one character on the image
if (numChars <= 0) {
throw new IllegalArgumentException("numChars <= 0");
}
this.image = image;
this.firstChar = firstChar;
this.numChars = numChars;
this.charWidth = image.getWidth() / this.numChars;
}
public void drawString (Graphics g, String text, int x, int y) {
// store current Graphics clip area to restore later
int clipX = g.getClipX();
int clipY = g.getClipY();
int clipWidth = g.getClipWidth();
int clipHeight = g.getClipHeight();
char [] chars = text.toCharArray();
for (int i = 0; i < chars.length; i++) {
int charIndex = chars[i] - this.firstChar;
// current char exists on the image
if (charIndex >= 0 && charIndex <= this.numChars) {
g.setClip(x, y, this.charWidth, this.image.getHeight());
g.drawImage(image, x - (charIndex * this.charWidth), y,
Graphics.TOP | Graphics.LEFT);
x += this.charWidth;
}
}

// restore initial clip area
g.setClip(clipX, clipY, clipWidth, clipHeight);
}
}

Here is a sample code that uses this class.

Image img;
try {
img = Image.createImage("/monospaced_3_5.PNG");
MonospacedFont mf = new MonospacedFont(img, '0', 10);
mf.drawString(g, "9876543210", 40, 40);
} catch (IOException e) {
e.printStackTrace();
}

From here you can go on defining bigger images with more letters. 

Wednesday, December 10, 2008

Chess: A study case

Since June/2007 I have been studying how to develop small and adaptive Java ME applications. My study case is a simple chess board and is available for free at GetJar.
Current version has help text, good usability, unselect feature and support for touch screen devices. But still does not support check mate.
I have just uploaded a new version. A single 15k jar file that supports resolutions from 128x96 up to 240x320 and higher.
Feel free to give it a try, leave a comment or better: write a review.

Wednesday, December 3, 2008

Internationalization (or just I18N - count the letters between I and N)

When developing adaptive Java ME applications one thing to take into account is the language used.
There is a System.property where you can read the current language selected on the handset.

String locale = System.getProperty("microedition.locale");

The first two letters identify the language while the last two letters identify the country. For example, "en-US" represents English on United States of America, while "pt-BR" represents Portuguese on Brazil.
A good thing is to have all your GUI Strings loaded based on the current language.
Lets say you store all those Strings in a single array and initialize it like this:

String messages [] = null;

if (locale.startsWith("pt")) {
    messages = new String [] {
        "Novo Jogo",
        "Configurações",
        "Ajuda"
    };
} else { // default language is English
    messages = new String [] {
        "New Game",
        "Settings",
        "Help"
    };
}

Then you define some constants to identify each index.

static final int MSG_NEW_GAME = 0;
static final int MSG_SETTINGS = 1;
static final int MSG_HELP = 2;

And use them like this (where menuList is an instance of List):

menuList.append(messages[MSG_NEW_GAME], null);
menuList.append(messages[MSG_SETTINGS], null);
menuList.append(messages[MSG_HELPS], null);

With this you have support for two languages in your application. The more cases you have for messages initiation based on locale, the better for your end user.
But be careful with automated translations. These softwares usually take the words too literally... Prefer to have someone proficient with the language do the translations for you.

Monday, November 24, 2008

Adding touch support

No List implementation is complete without support for touch screen.
Since MIDP 1.0 Canvas has support for touch screen devices with pointer methods.
To make things easier we add an int attribute to hold the top pixel from where items are painted.
Every time an item is added or removed this attribute is changed.
Below are the changes on the code from previous post:


public class CustomImplicitList extends Canvas {
private int baseY;

public CustomImplicitList(String title) {
// ...
this.baseY = (title == null) ? this.getHeight() : this.getHeight() - font.getHeight();
this.baseY >>= 1;
}

public void insert(int index, String item) {
// ...
if (font.getSize() != Font.SIZE_SMALL && this.font.stringWidth(item) >= getWidth()) {
this.baseY = (title == null) ? this.getHeight() : this.getHeight() - font.getHeight();
this.baseY >>= 1;
} else {
this.baseY -= (font.getHeight() / 2);
}
}

public void delete(int index) {
// ...
this.baseY += (font.getHeight() / 2);
}

protected void paint(Graphics g) {
// ...
int y = this.baseY;
}

protected void pointerPressed(int x, int y) {
if (y >= this.baseY) {
int index = (y - this.baseY) / this.font.getHeight();

if (index < this.items.length) {
this.selectedIndex = index;
this.repaint();
}
}
}

protected void pointerReleased(int x, int y) {
if (y >= this.baseY) {
int index = (y - this.baseY) / this.font.getHeight();

if (index == this.selectedIndex) {
this.commandListener.commandAction(SELECT_COMMAND, this);
}
}
}

}

Sunday, November 16, 2008

Custom Implicit List

If you have a few items to choose from, you may not need an LCDUI List instance. Below is a class that implements some methods of List, but have a custom drawing as it extends Canvas.
The main differences is that append method does not receive an Image parameter and the constructor receives only title as parameter.
On the next post we will add support for touch screen devices.
Attention: MIP 2.0 added title to Displayable. Some KVM implementations will draw the title even if a subclass overrides setTitle method. This will result on a double title drawing.


import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;

/**
* @author Telmo Mota - telmo.mota@gmail.com
*/
public class CustomImplicitList extends Canvas {

/**
* Command used to notify commandListener when number five or game action
* fire is pressed.
*/
public static final Command SELECT_COMMAND = new Command("",
Command.SCREEN, 0);

private CommandListener commandListener;

private String title;

private String[] items = new String[0];

private int selectedIndex = 0;

private Font font = Font.getDefaultFont();

/**
* Creates a new, empty List, specifying its title
*
* @param title
* the screen's title
*/
public CustomImplicitList(String title) {
this.title = title;
font = Font.getFont(font.getFace(), font.getSize(), Font.SIZE_LARGE);
}

/**
* @param item
*/
public void append(String item) {
this.insert(items.length, item);
}

/**
* @param index to insert the new item
* @param item to be inserted
*/
public void insert(int index, String item) {
String[] tmp = new String[items.length + 1];

System.arraycopy(items, 0, tmp, 0, index);
tmp[index] = item;
System.arraycopy(items, index, tmp, index + 1, items.length - index);
items = tmp;

// if font is already small, does not need to change it
if (font.getSize() != Font.SIZE_SMALL && this.font.stringWidth(item) >= getWidth()) {
int size = font.getSize();

if (size == Font.SIZE_LARGE) {
size = Font.SIZE_MEDIUM;
} else if (size == Font.SIZE_MEDIUM) {
size = Font.SIZE_SMALL;
}
font = Font.getFont(font.getFace(), font.getSize(), size);
}

if (this.isShown()) {
this.repaint();
}
}

/**
* @param index to be removed from items
*/
public void delete(int index) {
String[] tmp = new String[items.length - 1];

System.arraycopy(items, 0, tmp, 0, index);
if (index + 1 < items.length) {
System.arraycopy(items, index + 1, tmp, index, tmp.length - index);
}
items = tmp;
}

/**
* @return current selected item
*/
public int getSelectedIndex() {
return selectedIndex;
}

/**
* @return number of items
*/
public int size() {
return this.items.length;
}

/*
* @see javax.microedition.lcdui.Displayable#setCommandListener(javax.microedition.lcdui.CommandListener)
*/
public void setCommandListener(CommandListener l) {
super.setCommandListener(l);
this.commandListener = l;
}

/*
* @see
* javax.microedition.lcdui.Canvas#paint(javax.microedition.lcdui.Graphics)
*/
protected void paint(Graphics g) {
int height = this.getHeight();

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

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

if (this.title != null) {
g.drawString(this.title, getWidth() / 2, 0, Graphics.HCENTER | Graphics.TOP);
height -= font.getHeight();
}

// vertically centered drawing
int y = (height - (this.items.length * font.getHeight())) / 2;

if (y < font.getHeight()) {
y = font.getHeight();
}

for (int i = 0; i < this.items.length; i++) {
g.drawString(this.items[i], getWidth() / 2, y, Graphics.HCENTER
| Graphics.TOP);
if (i == this.selectedIndex) {
g.drawRect(1, y, getWidth() - 3, font.getHeight());
}
y += font.getHeight();
// it was the last line that could be drawn
if (y + font.getHeight() > height) {
i = this.items.length;
}
}
}

/**
* Sets the selected state of an element. If elementNum is invalid, return
* silently.
*
* @param elementNum
* the index of the element, starting from zero
* @param selected
* the state of the element, where true means selected and false
* means not selected
*/
public void setSelectedIndex(int elementNum, boolean selected) {
if (elementNum < 0 || elementNum >= this.items.length) {
return;
}

if (selected) {
this.selectedIndex = elementNum;
} else {
this.selectedIndex = elementNum - 1;
if (this.selectedIndex < 0) {
this.selectedIndex = this.items.length - 1;
}
}
}

/**
* Gets the title of the Displayable. Returns null if there is no title.
*
* @return the title of the instance, or null if no title
*/
public String getTitle() {
return title;
}

/*
* @see javax.microedition.lcdui.Canvas#keyPressed(int)
*/
protected void keyPressed(int keyCode) {
int action = this.getGameAction(keyCode);

if (keyCode == Canvas.KEY_NUM8 || action == Canvas.DOWN) {
this.selectedIndex++;
if (this.selectedIndex >= this.items.length) {
this.selectedIndex = 0;
}
this.repaint();
} else if (keyCode == Canvas.KEY_NUM2 || action == Canvas.UP) {
this.selectedIndex--;
if (this.selectedIndex < 0) {
this.selectedIndex = this.items.length - 1;
}
this.repaint();
} else if (keyCode == Canvas.KEY_NUM5 || action == Canvas.FIRE) {
this.commandListener.commandAction(SELECT_COMMAND, this);
this.repaint();
}
}

}

Sunday, November 2, 2008

Remove debug messages

While developing an application it is common place to first use emulators. This environment gives access to stepping and watching variable values. Very useful, high development speed.
Then you need to run the application on actual devices. Some manufacturers implement On Device Debugging so, the same environment is available even when the MIDlet is running on an actual device.
For those that do not have ODD it is necessary to use some sort of logging mechanism, but all need the developer to add debugging code. Code that is not part of the application, that need to be removed before the final build, that is just wasting valuable space.
How can we be sure there is no debugging code?
One common practice is to define a public boolean constant that toggles debugging off:

public static final boolean DEBUG = true;

Every line of debugging code should be enclosed within an if block:

if (DEBUG) {
    // debug code
}

If you set the constant to false the Java compiler will not add the if block to the class file.

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.

Tuesday, September 30, 2008

Use LCDUI

There is a great number of different screen resolutions out there. Bellow you can see various sizes, from the oldest 96x54 to the new 240x320. And these are not all of them. A more complete list of handsets and their screen resolutions can be found at MIDP Benchmark.

So how can a Java ME application behave properly on so many different handsets? Use the standard UI classes: LCDUI.
Ok, LCDUI is not appealing. Programmers do not control the layout, or even the colors, of the elements. Vendors implement the look-and-feel as they like. But where Java ME is supported, there you will find Lists, Forms and Dialogs.
A good approach is to have only two Commands per screen. One of type OK (positive answer) and the other of type Cancel (negative answer). As manufactures do not mix these concepts you will end up having one command on each softkey.
Another benefit to use LCDUI is that it does not add to the application size.

Monday, September 29, 2008

Keep it flat

Jar files are Zip files with another extension and a special (and optional) folder named META-INF. For each file or folder there is a header associated. As headers take some space of the jar you better keep less files in it, but if you already took it to the bare minimum then keep all those files in the root directory.
For MIDlet distribution the jar file must have the META-INF folder so, for best space usage, your jar files should have only this folder. Your MIDlet and all other classes should be on default package. The same with resource files, keep all of them at file root.