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.