Friday, December 10, 2010

Touch Menu

Cros-posted at Technè - Blog de Tecnologia do C.E.S.A.R.

Continuing with the details on how to develop touch enabled Java ME applications, today we will see a way of using such screens as input: if the user touchs an area it will trigger an action, like a menu.

This menu will also adapt to screen orientation.

First lets create a class to represent a menu entry. It knows how to paint the entry and stores a Command to be delivered when the user touchs the area:

public class TouchItem {

Command command;

public TouchItem(Command command) {
this.command = command;
}

// for this to work it is necessary that
// Graphics origin to be translated, so it
// is on the top left corner of the item,
// and that the clipping area has the
// width and height of the item
public void paint (Graphics g) {
g.drawString(command.getLabel(),
g.getClipWidth() / 2, g.getClipHeight() / 2,
Graphics.HCENTER | Graphics.BASELINE);
g.drawRect(0, 0, g.getClipWidth(), g.getClipHeight());
}
}

Then we create a class responsible for painting all menu entries. These first two classes can also be used to implement board games like Chess, for example.
public class TouchGrid {

private final TouchItem items[][];
private int itemWidth, itemHeight;
private int x, y;
private int nextLine, nextColumn;

public TouchGrid(int x, int y, int width, int height, int columns, int lines) {
this.items = new TouchItem[columns][lines];
setRectangle(x, y, width, height);
}

public void setRectangle (int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.itemWidth = width / this.items.length;
this.itemHeight = height / this.items[0].length;
}

// fill all columns in a line before moving to
// the next line. Calling this method four times
// will end up with the following menu:
// [ 1, 2 ]
// [ 3, 4 ]
// on a grid with two columns and two lines
public void add (TouchItem item) {
this.items[nextColumn][nextLine] = item;
nextColumn = (nextColumn + 1) % items.length;
if (nextColumn == 0) {
nextLine = (nextLine + 1) % items[0].length;
}
}

public void paint (Graphics g) {
Clip clip = new Clip(g);
int itemX = x;
for (int i = 0; i < items.length; i++) {
int itemY = y;
for (int j = 0; j < items[i].length; j++) {
if (items[i][j] != null) {
// needed by TouchItem to
// paint itself properly
g.setClip(itemX, itemY, itemWidth, itemHeight);
g.translate(itemX, itemY);
items[i][j].paint(g);
// reset Graphics
g.translate(-g.getTranslateX(), -g.getTranslateY());
clip.applyTo(g);
}
itemY += itemHeight;
}
itemX += itemWidth;
}
}

// here we map the point pressed to an item
public TouchItem getTouchItemAt (int x, int y) {
if (x < this.x || y < this.y) {
return null;
}

int i = (x - this.x) / this.itemWidth;
int j = (y - this.y) / this.itemHeight;

if (i >= items.length || j >= items[0].length) {
return null;
}

return items[i][j];
}
}

At paint method above we used below class to store the clip information and restore it:
public class Clip {

private int x, y;
private int width, height;

public Clip (Graphics g) {
x = g.getClipX();
y = g.getClipY();
width = g.getClipWidth();
height = g.getClipHeight();
}

public void applyTo (Graphics g) {
g.setClip(x, y, width, height);
}
}

We need a class that extends Canvas to paint the grid. It will also initialize the grid with the menu options:
public class TouchMenuCanvas extends Canvas {

private TouchGrid touchGrid;
private CommandListener commandListener;

public TouchMenuCanvas () {
final int columns = 2;
final int lines = 3;
int margin = getWidth() / 25;
// 2 = left and right margins
int width = getWidth() - (2 * margin);
// 2 = top and bottom margins
int height = getHeight() - (2 * margin);
touchGrid = new TouchGrid(margin, margin, width, height, columns, lines);
for (int i = 0; i < lines * columns; i++) {
Command c = new Command("item " + (i+1), Command.SCREEN, 1);
touchGrid.add(new TouchItem(c));
}
}

// when orientation changes we need to update
// grid rectangle
protected void sizeChanged(int width, int height) {
int margin = width / 25;
width = width - (2 * margin);
height = height - (2 * margin);
touchGrid.setRectangle(margin, margin, width, height);
}

// we need to override this method because there
// is no getCommandListener on API
public void setCommandListener(CommandListener listener) {
super.setCommandListener(listener);
commandListener = listener;
}

protected void paint(Graphics g) {
g.setColor(0xffffff); // white
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0); // black
this.touchGrid.paint(g);
}

protected void pointerPressed(int x, int y) {
TouchItem item = this.touchGrid.getTouchItemAt(x, y);
if (item != null) {
commandListener.commandAction(item.command, this);
}
}
}

The last piece is a MIDlet to show the Canvas:
public class TouchMenuMIDlet extends MIDlet implements CommandListener {

TouchMenuCanvas menuCanvas;

public TouchMenuMIDlet() {
menuCanvas = new TouchMenuCanvas();
menuCanvas.setCommandListener(this);
menuCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
}

public void commandAction(Command c, Displayable d) {
if (c.getCommandType() == Command.EXIT) {
this.notifyDestroyed();
} else {
System.out.println(c.getLabel());
}
}

protected void destroyApp(boolean unconditional) {
}

protected void pauseApp() {
}

protected void startApp() throws MIDletStateChangeException {
Display.getDisplay(this).setCurrent(menuCanvas);
}

}

Below is the application running on DefaultCldcPhone1 of Java Platform Micro Edition SDK 3.0 on both orientations.








We hope this helps. See you next time.

Thursday, December 9, 2010

Touch Screen support

Cros-posted at Technè - Blog de Tecnologia do C.E.S.A.R.

More and more cell phones have touch screens. And when we hear about them the first thing that we think are the most expensive smartphones. Big screens and amazing computer power.

But there are also feature phones with such screens. And amongst them there is a lot supporting Java ME. So, how do you enable an application to get the user's "point"?

Since MIDP 1.0 Canvas already had the following callback methods: pointerPressed, pointerDragged and pointerReleased. And for information there were also hasPointerEvents and hasPointerMotionEvents.

TIP: SDK 3.0 emulator already have touch support, but if you are using WTK 2.5 go to wtklib\devices\DefaultColorPhone\DefaultColorPhone.properties and change the value of touch_screen to true.

To better understand these pointer methods here is a simple example. A Canvas where we can draw freely. It bevahes like Microsoft Paint pencil.

public class TouchCanvas extends Canvas {
// starting point of the line drawn at paint method
int x1 = -1, y1 = -1;
// line final point
int x2, y2;
// flag used to know when to clear the screen
boolean clear = true;

protected void pointerPressed(int x, int y) {
x1 = x;
y1 = y;
}
protected void pointerReleased(int x, int y) {
x1 = y1 = -1;
}
protected void pointerDragged(int x, int y) {
x2 = x;
y2 = y;
repaint();
}

protected void paint(Graphics g) {
if (clear) {
g.setColor(0xffffff); // white
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0); // black
clear = false;
}

if (x1 >= 0 && y1 >= 0) {
g.drawLine(x1, y1, x2, y2);
x1 = x2;
y1 = y2;
}
}
}

The MIDlet below shows the Canvas and give the user a option to clear the screen.

public class TouchMIDlet extends MIDlet implements CommandListener {

TouchCanvas touchCanvas = new TouchCanvas();

public TouchMIDlet() {
touchCanvas.addCommand(new Command("Clear", Command.OK, 1));
touchCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
touchCanvas.setCommandListener(this);
}

protected void destroyApp(boolean unconditional) { }

protected void pauseApp() { }

protected void startApp() {
Display.getDisplay(this).setCurrent(touchCanvas);
}

public void commandAction(Command c, Displayable d) {
if (c.getCommandType() == Command.EXIT) {
notifyDestroyed();
} else {
touchCanvas.clear = true;
touchCanvas.repaint();
}
}
}

For a more complex sample lets change two classes from Bar Chart to add touch dragging. First we add the following method to BarChart class:
public int getBarWidth (Font font) {
return font.stringWidth(widestName);
}

Now we add the following code to BarChartCanvas:
private int lastX, lastY;
protected void pointerPressed(int x, int y) {
lastX = x;
lastY = y;
}
protected void pointerDragged(int x, int y) {
int w = barChart.getBarWidth(Font.getDefaultFont());
// dragged a distance bigger than the bars width
if (lastX - x >= w || lastY - y >= w) {
barChart.nextBar();
repaint();
lastX = x;
} if (x - lastX >= w || y - lastY >= w) {
barChart.previousBar();
repaint();
lastX = x;
}
}

The smaller the bars be smoother the movement will be. This code also works on landscape mode (SDK 3.0 Emulator: View .. Orientation .. 270).

Another common action is the long press. As we do not have a callback method for it we need to count the time the user is pressing and not dragging or releasing. Below is a way to change our TouchCanvas:

private Timer timer;
protected void showNotify() {
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
// long press x and y
int lpx = -1, lpy = -1;
public void run() {
if (x1 >= 0) {
if (lpx == x1 && lpy == y1) {
lpx = lpy = -1;
System.out.println("long press");
} else {
lpx = x1;
lpy = y1;
}
}
}
},
// starting now and checking at every second
0, 1000);
}
protected void hideNotify() {
timer.cancel();
timer = null;
}

We hope this helps. See you next time.

Related topics:

Tuesday, December 7, 2010

Bar Chart

Cros-posted at Technè - Blog de Tecnologia do C.E.S.A.R.

When developing a mobile application one must consider screen size fragmentation. If Apple have only two screen sizes for iPhone until now, this is the exception. All other manufacturers vary the screen sizes a lot.

To make developers life easier each technology provides widgets and ways to combine them. But there are times when you need an specific look and fell. What to do? Low level graphics. 2D, 3D, does not matter.

Even if you only have one target screen size it is a good practice to code using proportions. Avoid absolute values of x, y, width and height. Here we show how to do this with a BarChart and Java ME.

It is good to point that there already are components that do this. Our main point here is to present good low level visual programming.

First thing is to have a class for our data.


class ChartEntry {
String name;
float value;
public BarChartEntry(String name, float value) {
this.name = name;
this.value = value;
}
}

And another class to draw the data. Values on top of the bars and labels at the bottom. But if an entry has negative value we do the opposite and draw the value at the bottom.

public class BarChart {
private Vector entries = new Vector();
private float maxValue;
private float minValue;
private String widestName = "";
private int firstBar;

// Every time an entry is added we check for the limits
public void addEntry(BarChartEntry entry) {
entries.addElement(entry);
if (maxValue < entry.value) {
maxValue = entry.value;
}
if (minValue > entry.value) {
minValue = entry.value;
}
if (widestName.length() < entry.name.length()) {
widestName = entry.name;
}
}

public void nextBar () {
this.setFirstBar(this.firstBar +1);
}
public void previousBar () {
this.setFirstBar(this.firstBar -1);
}
private void setFirstBar(int firstBar) {
if (firstBar >= 0 && firstBar < this.entries.size())
this.firstBar = firstBar;
}

// similar to Graphics.drawRect.
// x and y is the top left corner of a rectangle
// w is for width and h is for height
public void draw(Graphics g, int x, int y, int w, int h) {
Font font = g.getFont();
// 2 = value and label strings drawn at top and bottom
int availableHeight = h - (2 * font.getHeight());
int barWidth = font.stringWidth(widestName);
// empty space between bars
int barMargin = w / 25;
int maxBars = (int) Math.floor(w / (barWidth + barMargin));
float valuesRange = maxValue - minValue;
int barOrigin = y + h - font.getHeight();
if (minValue < 0) {
// move origin line up
barOrigin = barOrigin
+ (int) ((availableHeight * minValue) / valuesRange);
}

g.drawLine(x, barOrigin, x + w, barOrigin);

// draw chart entries
BarChartEntry drawEntries [] = new BarChartEntry[this.entries.size()];
int barX = x;
int barCount = 0;
this.entries.copyInto(drawEntries);
for (int i = 0; i < maxBars && this.firstBar + i < drawEntries.length; i++) {
BarChartEntry chartEntry = drawEntries[this.firstBar + i];
// we get current bar height with a simple rule of three
// availableHeight -> valuesRange
// barHeight -> chartEntry.value
int barHeight = (int) ((availableHeight * chartEntry.value) / valuesRange);
int textCenterX = barX + barMargin + (barWidth / 2);
if (barHeight > 0) {
g.drawString(chartEntry.name, textCenterX, barOrigin,
Graphics.HCENTER | Graphics.TOP);
g.fillRect(barX + barMargin, barOrigin - barHeight, barWidth,
barHeight);
g.drawString(String.valueOf(chartEntry.value), textCenterX,
barOrigin - barHeight, Graphics.HCENTER
| Graphics.BOTTOM);
} else {
g.drawString(chartEntry.name, textCenterX, barOrigin,
Graphics.HCENTER | Graphics.BOTTOM);
g.fillRect(barX + barMargin, barOrigin, barWidth, -barHeight);
g.drawString(String.valueOf(chartEntry.value), textCenterX,
barOrigin - barHeight, Graphics.HCENTER | Graphics.TOP);
}
barX = barX + barMargin + barWidth;
barCount++;
}
}
}

Below is a Canvas that shows a bar chart and changes the first bar shown according to pressed keys.

public class BarChartCanvas extends Canvas {

BarChart barChart = new BarChart();

public BarChartCanvas() {
barChart.addEntry(new BarChartEntry("J", 8.0f));
barChart.addEntry(new BarChartEntry("FE", 10.0f));
barChart.addEntry(new BarChartEntry("MAR", 6.0f));
barChart.addEntry(new BarChartEntry("ABRI", 9.0f));
barChart.addEntry(new BarChartEntry("MAIO", -2.0f));
barChart.addEntry(new BarChartEntry("JUNHO", -4.0f));
barChart.addEntry(new BarChartEntry("JULHO", 6.0f));
barChart.addEntry(new BarChartEntry("AGOSTO", 9.0f));
}

protected void paint(Graphics g) {
g.setColor(0xffffff);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0);
barChart.draw(g, 0, 0, getWidth(), getHeight());
}

protected void keyPressed(int keyCode) {
switch (keyCode) {
case Canvas.KEY_NUM2:
case Canvas.KEY_NUM4:
barChart.previousBar();
break;
case Canvas.KEY_NUM6:
case Canvas.KEY_NUM8:
barChart.nextBar();
break;
default:
switch (super.getGameAction(keyCode)) {
case Canvas.LEFT:
case Canvas.UP:
barChart.previousBar();
break;
case Canvas.RIGHT:
case Canvas.DOWN:
barChart.nextBar();
break;
}
break;
}
this.repaint();
}
}

And the MIDlet used to show this Canvas.

public class BarChartMIDlet extends MIDlet implements
CommandListener {

BarChartCanvas chartCanvas = new BarChartCanvas();

public BarChartMIDlet() {
chartCanvas.addCommand(new Command("Exit", Command.EXIT, 1));
chartCanvas.setCommandListener(this);
}

protected void destroyApp(boolean unconditional) { }

protected void pauseApp() { }

protected void startApp() {
Display.getDisplay(this).setCurrent(chartCanvas);
}

public void commandAction(Command c, Displayable d) {
this.notifyDestroyed();
}

}

Below are screens of this application running on DefaultCldcPhone1 emulator of Java Platform Micro Edition SDK 3.0.






As expected in landscape mode we can draw one more bar. We can change the screen orientation with menu View .. Orientation.
We hope this helps.