Thursday, December 17, 2009

Single Jar theory

This month there was a Sun Tech Days event at São Paulo, Brazil.
I could not be there, but I am thankful they shared the slides.

The only presentation about Java ME was JavaME: Building Cool Interfaces with the Lightweight UI Toolkit (PDF document). I think there are also other cool things on Java ME, but the event is a lot smaller than Java ONE and there was more interesting stuff on Java Standard and Enterprise. A Java ME event? Lets hope there will be a new Java Mobile, Media & Embedded Developer Days - Latin America.

Back to the presentation. At slide 26 there is this interesting suggestion about Easy deployment with One JAR.
I think this is the right way of thinking, but the presentation is about LWUIT being the answer.

I advocate the Adaptive User Interface for some time now and agree it is hard to achieve with only LCDUI and Canvas.
But I do not like the LWUIT overhead. To me 50K is still too much (slide 78).

My applications are small ones (below 15k) and I will keep them this way.
They focus on one feature and try to do it right: with good user experience.

Will I ever use LWUIT? Maybe. I might need an specially difficult UI - very hard to implement from scratch - and end up using this toolkit.
But I will first try doing it with Canvas (slide 19). If I can not do it this way I will still have learned something from it.

Related Topics:

Tuesday, December 15, 2009

Blog audience

Since 26 April 2009 I am following this blog statistics with Google Analytics and it had 566 visiting users (855 pageviews).
There are days days when I do not have a single visit, but it usually goes from two to six a day.
The most interesting part of the statistics for me is the Map Overlay, where we can see from which part of the wordl the visits came from.
Below is the world map with all 54 countries that have visited this blog and the number of visits for each.

The top 10 countries are:


  • Brazil: 255

  • United States: 56

  • India: 32

  • Portugual: 22

  • Poland: 21

  • Germany: 18

  • Sweden: 10

  • United Kingdom: 9

  • Turkey: 8

  • Russia: 8


It is natural to me to see so many visits from Brazil as I am Brazilian and have metioned this blog where I lecture and also at Brazil JUGs.
If you have a blog or a site you should really use some analytics tool.
It is very satisfaying to see all these different visits on the world map.

Related topics:

Monday, December 14, 2009

Books 1.3

Last week I have posted all changes I made for Books version 1.3, they are listed below on Related Topics.
I also updated my blog entry on Books.
But the first place I notified the world about this version was on
this tweet update.

Related Topics:

Friday, December 11, 2009

Reading UTF-8 streams

If you have a byte array with Unicode characters encoded with UTF-8 you can create a String from it with the following constructors:

String(byte[] bytes, int off, int len, String enc) 
String(byte[] bytes, String enc)


This is useful when reading from local files. But what can you use if you are reading an UTF-8 stream?

You can use class java.io.InputStreamReader with the following constructor:

InputStreamReader(InputStream is, String enc)


After you have the InputStreamReader instance you can create a char array buffer and use the following method:

public int read(char[] cbuf, int off, int len)


For example:

InputStreamReader in = new InputStreamReader(
inputConnection.openInputStream(), "UTF-8");
char [] buff = new char[1024];
int len = in.read(buff, 0, buff.length);

while (len > 0) {
// use buff characters, like
// String s = new String(buff, 0, len)

len = in.read(buff, 0, buff.length);
}


But I have used only in.read(buff) and did not have a problem with it.

This also applies for Java Standard and Enterprise editions.

Thursday, December 10, 2009

Showing error messages

Exceptions are a powerful feature of Java.
Applications may recover from expected error situations and present meaningful messages to the user.

But when you catch an unexpected exception? What should you present to the user?

I propose to show a generic error message prefix requesting user feedback like:

"There was an unexpected situation, please send an email to a@b.c with the following details: "

The details are the Exception class name and, if available, the message.
Below is a method to handle exceptions this way, it should be placed in a class that extends MIDlet:


public void showExceptionAlert (Exception e) {
StringBuffer msg = new StringBuffer(getErrorMessage());
Display d = Display.getDisplay(this);

msg.append(e.getClass().getName());
if (e.getMessage() != null) {
msg.append("\n").append(e.getMessage());
}

d.setCurrent(
new Alert(getErrorTitle(), msg.toString(),
null /*alertImage*/, AlertType.ERROR),
d.getCurrent());
}


getErrorMessage() and will load the prefix getErrorTitle() the Alert title based on current locale (i18n).

Related topics:

Wednesday, December 9, 2009

Full Screen Repaint

If for any reason you have to override getWidth or getHeight methods in a class that extends Canvas, the calls to repaint() might not update the whole display.

And this is an expected behaviour. Check below documentation from API:

public final void repaint()

Requests a repaint for the entire Canvas. The effect is identical to

repaint(0, 0, getWidth(), getHeight());


If you want to always update the whole display when calling repaint() add the following method to your class:


public void repaint() {
super.repaint(0, 0, super.getWidth(), super.getHeight());
}

Tuesday, December 8, 2009

Color Distance

We can represent a color in Java ME using integer literals. The four bytes represent different aspects of the color:

  • Transparency: also known as Alpha channel. Indicates if the color should we drawn as solid or translucid

  • Red: the red component

  • Green: the green component

  • Blue: the blue component



In a previous post I pointed how to get the handset theme color dinamically with Display.getColor.
But I came to a situation where I could not use COLOR_HIGHLIGHTED_BACKGROUND with COLOR_HIGHLIGHTED_FOREGROUND.
The colors were too "close". Like dark gray over black.

My work around was to add a color distance verification. Lets imagine that the three color components are the axis of a 3D system.
To calculate the distance between two colors (two points) I just need to apply the below method:


public static int colorDistance (int c1, int c2) {
int r1 = (c1 & 0xff0000) >> 16;
int g1 = (c1 & 0x00ff00) >> 8;
int b1 = c1 & 0x0000ff;
int r2 = (c2 & 0xff0000) >> 16;
int g2 = (c2 & 0x00ff00) >> 8;
int b2 = c2 & 0x0000ff;

// http://en.wikipedia.org/wiki/Euclidean_metric
return sqrtRound(((r2 - r1) * (r2 - r1)) +
((g2 - g1) * (g2 - g1)) +
((b2 - b1) * (b2 - b1)));
}


I chose that if color distance if below or equal to 40 I should use black color for foreground and white color for background.

Related topics:

Monday, December 7, 2009

Square Root Rounding

Since CLDC 1.1 Java ME applications can use floating point operations like sin, cos, tan, sqrt, etc. All these methods are available at java.lang.Math class.

Some applications needs square root floating point precision and will have to use double variables. Other applications need only an approximation and this is what I present here.

Lets first take a look at how I create my square pyramid. To start we draw two colums with the base number and its square:








BaseSquare
00
11
24
39
416


Now we place all other numbers between the squares.








BaseSquare and other numbers
0
      values <= 0   
1
          1  2         
2
       3  4  5  6      
3
    7  8  9 10 11 12   
4
13 14 15 16 17 18 19 20


The square root approximation of these numbers is the same as the square root of the center value. For example, sqrtRound(7) == 3, sqrtRound(6) == 2.

Below is the source code:

public static int sqrtRound (int value) {
if (value <= 0) {
return 0;
}

int i = 1;
int j = 2;

// search for range where value is between two square values
while ((i * i) <= value && value > (j * j)) {
i++;
j++;
}

int d = (j * j) - (i * i) + 1;

if ((value - (i * i)) < (d / 2)) { // round down
return i;
} else { // round up
return j;
}
}

Sunday, October 25, 2009

Books version 1.2

This week I shared version 1.2 of Books, so I updated the blog entry listing all features and related JSRs.

Monday, October 12, 2009

Alphabet: my third study case

With this application you can help your children learn the alphabet. Three letters at a time. When the child correctly presses the key for each letter three other letters are presented. There is a counter to stimulate the child to achieve higher scores. You can change letter colors based on the handset theme, but the application also have predefined colors.


The application is available for downloa here.


Related topics:


Wednesday, September 16, 2009

Adding a simple progress bar

A progress bar is a visual representation of a Real Number between zero and one or a percentage between 0% and 100%.

Below is a method for drawing a simple progress bar:


/**
* @param g Graphics
* @param x
* @param y
* @param w width
* @param h height
* @param p part between zero and total
* @param t total
*/
void fillProgressBar (Graphics g, int x, int y, int w, int h, int p, int t) {
g.drawRect(x, y, w, h);
// p will receive the pixel width of the proportion: part / total
p = (p * w) / t;
g.fillRect(x, y, p, h);
}

Wednesday, September 9, 2009

Application Sindication

It has been only a week since I shared Books and it got more than one thousand downloads. Of course I am happy with this number, but I was wondering how did these people find out about it? Of course this blog does not have so many readers. I write to a very specific nich.

So I did some search and navigation on GetJar and found out RSS Feeds for application updates. They are organized by platforms and manufacturers.

I began following some of these feeds and found out they are a very interesting way to discover new applications for my cell phone.

Related topics:

Wednesday, September 2, 2009

Books: a new study case

Updated on 31/May/2010. New features on version 1.4

I had some TXT books and wanted to read them on my cell phone.
So I made this application and shared it.

Features:


  • Fullscreen landscape reading with biggest font available.

  • Recent files list, so you do not need to browse for files every time you open the application.

  • Resume reading from the same line you were (with progress bar since version 1.1) - no key pressing just to find the point you were before.

  • Press and hold to fast forward or rewind with increasing speed (since 1.1).

  • Languages: English, Español, Français, Português (since 1.2).

  • Read UTF-8 encoded files (since 1.3).

  • Color verification to guarantee scroll bar is always visible (since 1.3).

  • Presenting error dialog when an unexpected situation occurs (since 1.3).

  • Checking file size before trying to open it (since 1.4).

  • Line number presented on footer during read (since 1.4).

  • Better touch support: go to previous pages (since 1.4).



You can open files using JSR 75 - FileConnection.
When you enter "Open File" screen your cell phone might ask you to grant read access to the application.
With Sony Ericsson I had to grant it only once, at the first execution.
With Nokia it was necessary to grant every folder the application was trying to read.
If some unexpected situation occurs an error dialog will be presented with details.
You can send me these details by email.

The content is shown in landscape/fullscreen mode thanks to Sprite transforms, so MIDP 2 is required.

To use the maximum available space just for text I did not set title nor Commands on main Canvas and treated keyPressed to identify softkeys.

Each file read is kept on a "Recents" screen.
A List where the top most items are the most recent files opened by the application.
I used a simple RecordStore to keep this list of files and where in each file we were reading.

Give it a try and, if you like it, write a nice review. ;)

Related topics:

Sunday, August 23, 2009

Fullscreen Landscape

Most handset screens are portrait: higher than wider. What if you want to show content in landscape mode? Since MIDP 2.0 we can use an Sprite to rotate an image in 90 degrees.


First thing we need is an Image of the correct size. Following code could be inside the constructor of a class that extends Canvas - considering an Sprite and an Image attributes:



int width = Math.max(super.getWidth(), super.getHeight());
int height = Math.min(super.getWidth(), super.getHeight());
screen = Image.createImage(width, height);
sprite = new Sprite(screen);
if (super.getWidth() < super.getHeight()) { // portrait screen
sprite.setTransform(Sprite.TRANS_ROT90);
sprite.setPosition(0, 0);
}

When painting your content use the mutable Image Graphics, then update the sprite with the image like bellow.



protected void paint(Graphics g1) {
Graphics g = screen.getGraphics();
// ... do your drawing
this.sprite.setImage(screen, screen.getWidth(), screen.getHeight());
sprite.paint(g1);
}

How can you use the biggest possible area on the handset display?



  1. Do not setTitle on your Canvas

  2. Do not addCommand to your Canvas

  3. Call setFullScreenMode(true) before calling super.getWidth() and super.getHeight()

Monday, August 17, 2009

Open File Dialog

Ok, this is not a Dialog... It is an Implicit List. As we do not have File Dialogs on LCDUI I decided to share my version with you. It did work with Sony Ericson and Motorola iDEN handsets and I use it in my Books application.

import java.io.IOException;
import java.util.Enumeration;
import java.util.Stack;

import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import javax.microedition.io.file.FileSystemRegistry;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.Ticker;


public class OpenFileDialog extends List
implements CommandListener
{
public static final String PREFIX = "file:///";
private static final String UP = "[ .. ]";
private static final String DIR = " + ";
private Stack stack = new Stack();
private OpenFileListener listener;
private CommandListener commandListener;
private String extension;

public OpenFileDialog (OpenFileListener listener,
String title, String extension)
{
super(title, List.IMPLICIT);
super.setCommandListener(this);
this.listener = listener;
this.extension = extension;
addRoots();
}

public void setCommandListener(CommandListener l) {
this.commandListener = l;
}

public void commandAction(Command c, Displayable d) {
if (c == List.SELECT_COMMAND) {
this.changeSelection();
} else {
if (this.commandListener != null) {
this.commandListener.commandAction(c, d);
}
}
}

private String getSelectedText () {
int index = getSelectedIndex();
if (index < 0 || index >= this.size()) {
return "";
}
return this.getString(index);
}

private void changeSelection () {
String target = this.getSelectedText();
String parent = null;
boolean goingUp = false;

if (UP.equals(target)) {
goingUp = true;
stack.pop();
if (stack.isEmpty() == false) {
target = (String) stack.peek();
} else {
super.deleteAll();
addRoots();
this.setTicker(null);
return;
}
} else {
if (stack.isEmpty() == false) {
parent = stack.peek().toString();
}
}

try {
if (target.startsWith(DIR)) {
target = target.substring(3);
}
if (parent != null) {
target = parent + target;
}
this.setTicker(new Ticker(target));
FileConnection fc = (FileConnection)
Connector.open(PREFIX + target, Connector.READ);
if (fc.isDirectory()) {
super.deleteAll();
if (goingUp == false) {
stack.push(target);
}
super.append(UP, null);

Enumeration entries = fc.list();

while (entries.hasMoreElements()) {
String entry = (String) entries.nextElement();
FileConnection fc2 = (FileConnection)
Connector.open(PREFIX + target + entry,
Connector.READ);
if (fc2.isDirectory()) {
super.append(DIR + entry, null);
} else if (entry.toLowerCase().endsWith(extension)) {
super.append(entry, null);
}
fc2.close();
}
fc.close();
} else {
this.listener.fileSelected(fc);
}
} catch (IOException e) {
e.printStackTrace();
}

}

private void addRoots() {
Enumeration roots = FileSystemRegistry.listRoots();
while (roots.hasMoreElements()) {
super.append(DIR + roots.nextElement().toString(), null);
}
}

}

Related topics:

Monday, July 6, 2009

Know your audience

Which handsets are actually downloading your Java ME application? How many users report that your applications runs fine with their handsets? The answers are available to GetJar developers at Download Stats - Compatibility report, but I had a problem with it...


Since 2009 May, 14 my chess board has two versions available, one for MIDP 1.0 handsets and another for MIDP 2.0. From this day onwards I evaluate weekly downloads with handset details. At June, 7 I noticed that the most active handset at MIDP 1.0 version (Nokia 3110c - with 11% of all downloads and 53% of new downloads since May, 14) has droped to zero downloads, but appeared at MIDP 2.0 version and now it is one of the top 10 downloaders.



  1. Nokia N73

  2. Nokia 6300

  3. Sony-Ericsson K800i

  4. Nokia N70-1

  5. Nokia 5800 XpressMusic

  6. Nokia 6600

  7. Nokia E71

  8. Nokia 5310 XpressMusic

  9. Nokia 3110c

  10. Sony-Ericsson W200i

According to the official site this device is really MIDP 2.0, so it was an error from GetJar to have it download MIDP 1.0 content to begin with. So what? Other 43 handsets are still downloading MIDP 1.0 version, below are the top 10 downloaders since May, 14.



  1. Nokia 3100

  2. Nokia 3120

  3. Samsung SGH D900i

  4. Nokia 3200

  5. Nokia N-Gage QD

  6. Nokia 6610i

  7. Sony-Ericson T610

  8. Samsung SGH U700

  9. Nokia N-Gage

  10. Nokia 7210


Related topics:

Tuesday, June 30, 2009

Adding a scrollbar

Sometimes you have more data to show than available space. One way to make this clear to the user is presenting a scroll bar.
Lets update our custom alert to paint a scroll bar when necessary.
Below is the source code of my simple scroll bar component:

import javax.microedition.lcdui.Graphics;

public class Scrollbar {

private int x, y, width, height;
private int total; // how much you have to present
private int position; // what you are presenting
private int backgroundColor = 0xffffff;
private int foregroundColor;
private int indicatorHeight;

/**
* @param part how much can be presented to the user at a time
*/
public Scrollbar(int x, int y, int w, int h,
int part, int total)
{
this.x = x; this.y = y; this.width = w; this.height = h;
this.total = total;
// the indicator height is calculated as a proportion of h
indicatorHeight = (part * h) / total;
// sanity test
if (indicatorHeight >= height) {
indicatorHeight = height / 2;
}
}

public void setPosition(int position) {
this.position = position;
}

public void setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
}

public void setForegroundColor(int foregroundColor) {
this.foregroundColor = foregroundColor;
}

public void paint(Graphics g) {
int indicatorY = (this.position * this.height) / this.total;

// sanity test, can not draw below the height
if (indicatorY + this.indicatorHeight > height) {
indicatorY = height - this.indicatorHeight;
}

g.setColor(this.backgroundColor);
g.fillRect(x, y, width, height);
g.setColor(this.foregroundColor);
g.fillRect(x, indicatorY, width, indicatorHeight);
}
}

Now lets use the scroll bar. Define an Scrollbar attribute and create an instance right after you have initiated your message array:

// how many lines the user can see at a time
int maxLines = this.getHeight() / Font.getDefaultFont().getHeight();

if (maxLines < this.message.length) {
// 10 = scroll bar width. Remember to also subtract this value
// when initiating message array
this.scrollbar = new Scrollbar(getWidth() - 10, 0, 10, getHeight(),
maxLines, this.message.length)
}

At commandAction right after you update messageFirstLineShown:

if (this.scrollbar != null) {
this.scrollbar.setPosition(this.messageFirstLineShown);
}

At paint method:

if (this.scrollbar != null) {
this.scrollbar.paint(g);
}

Now, when the user press Next command he will see the scroll bar indicator move downwards.
This is a simple implementation with a simple usage. Feel free to change any part of it on your project.

Related topics:

Saturday, June 20, 2009

Key Repetition with TimerTask

It is not friendly to force your user to repeat key presses. My CustomImplicitList had this fault. If you wanted to move the selection using up/down keys or 2/8 numbers you had to press the key once for every change. To avoid this I made a simple change, adding the following method:

protected void keyRepeated(int keyCode) {
this.keyPressed(keyCode);
}

Now the user can press and hold a key to keep changing the selection, right? Wrong! Because not all Java ME Virtual Machines call keyRepeated. You can know it by calling Canvas.hasRepeatEvents(). So, how do we grant key repetition to these devices?

I could use a Thread and flag attributes to solve this problem, but I prefer creating an inner class at CustomImplicitList:

class KeyRepeatTask extends TimerTask {
public void run() {
if (isShown() && keyCode != Integer.MIN_VALUE) {
keyPressed(keyCode);
}
}
}

Two new attributes will help me control the key repetition:

private Timer keyRepeatTimer;
private int keyCode = Integer.MIN_VALUE;

To start the key repetition I added the following lines to the very start of keyPressed method:

if (this.hasRepeatEvents() == false && this.keyCode == Integer.MIN_VALUE) {
this.keyCode = keyCode;
this.keyRepeatTimer = new Timer();
// keep repeating the task at each 700 milliseconds
this.keyRepeatTimer.scheduleAtFixedRate(new KeyRepeatTask(), new Date(), 700);
}

To stop the key repetition I added the following method:

protected void keyReleased(int keyCode) {
if (this.keyRepeatTimer != null) {
this.keyCode = Integer.MIN_VALUE;
this.keyRepeatTimer.cancel();
this.keyRepeatTimer = null;
}
}

How did I test this? First I removed the implementation of keyRepeated and added hasRepeatEvents always returning false. Then I executed my use case on emulators and personal handsets. Of course this is not enough to guarantee the same behavior on all Java ME enabled handsets, as this is highly dependendent on the correct implementation of hasRepeatEvents but is a safe start.

If you find out this code does not work on your handset, please let me know.

Related topics:

Sunday, June 14, 2009

MIDP 1.0 long tail

I don't know of any new handset that comes with a MIDP 1.0 Java Virtual Machine. New handsets comes with MIDP 2.0 or 2.1 and will come, eventually, with 3.0.
But I know there is a market for these old handsets and began following the daily downloads of my Chess board. Below is an updated graphic of these downloads:


We can see a trend with a smaller number of downloads for each week. Some day this curve will reach zero and not move anymore, but when?
Since April, 19 MIDP 1.0 handsets were responsible for 1069 new downloads, correspondig to 17.8% of the total. For me it is enough to keep evolving this version. Because old users may come back and download an updated application.

Related topics:

Tuesday, May 26, 2009

Bordered text

How about adding a fancy bordered text to your customized UI? Simple, draw the text with the border color four times around the anchor point with just one pixel difference. Then draw the text with the desired color at the anchor point. Did not get it? How about the source code below?

public void drawBorderedText(Graphics g, String text,
  int x, int y, int anchor, int textColor, int borderColor)
{
  // draw border
  g.setColor(borderColor);
  g.drawString(text, x - 1, y - 1, anchor);
  g.drawString(text, x - 1, y + 1, anchor);
  g.drawString(text, x + 1, y - 1, anchor);
  g.drawString(text, x + 1, y + 1, anchor);
  // draw text
  g.setColor(textColor);
  g.drawString(text, x, y, anchor);
}

Monday, May 25, 2009

My News view

I share what I consider interesting news at this site. If you want to easily follow them please use this RSS feed

These are the same news available at this blog, section "Also read...".

Saturday, May 16, 2009

Development branches

How can I evolve two different versions of the same application? Using the same source code? For example, if one version uses MIDP 2.0 but the other uses MIDP 1.0? The answer is preprocessing.

Since October/2003 I use Antenna: an Ant plugin that allows a developer to easily build, preverify and package a Java ME application, but it also has a pre-processor built in. To use it you need to add special line comments in your source code and call the wtkpreprocess task before calling the build task.
Below is an example of such line comments:

//#ifdef midp20
list.setBackgroundColor(display.getColor(Display.COLOR_BACKGROUND));
//#endif

ifdef means If Defined.
My build.xml Ant script will have the following lines:

<wtkpreprocess src="./src" dest="./prep" symbols="midp20"/>
<wtkbuild src="./prep" dest="./bin"/>

With this the preprocessor will match the informed symbol "midp20" with the ifdef value and keep the original code unchanged.
To differ between the two versions I will need two targets in build.xml. One for MIDP 2.0 and the other for MIDP 1.0. The first will use the sample above, but the target for MIDP 1.0 will use:

<wtkpreprocess src="./src" dest="./prep"/>
<wtkbuild src="./prep" dest="./bin"/>

Without any symbols the preprocessor will change the source code and add comment lines between ifdef and endif. The changed code at prep folder will look like this:

//#ifdef midp20
//list.setBackgroundColor(display.getColor(Display.COLOR_BACKGROUND));
//#endif

You can also use preprocessing with NetBeans Mobility Pack and Eclipse MTJ (EclipseME has moved to this project).

Related Topics:

Thursday, April 30, 2009

MIDP 1.0 still matters

Is it worth to keep a MIDP 1.0 version of your product? I found out that the answer is yes and will share the reasons.
The latest verstion of my chess board - 1.7 - requires MIDP 2.0 and minimum screen resolution of 128x96. The previous version - 1.6 - required the same minimum screen resolution, but MIDP 1.0 instead.
I could delete 1.6 and focus only on newer handsets, but got curious about how many downloads this older version would get against the new one.
GetJar has download stats reports: daily, weekly and monthly. I got daily reports for 1.6 and 1.7 since April, 12 and was surprised to see that MIDP 1.0 downloads were 20% of the total - 389 out of 1889.
Below is a graphic with the daily download evolution of both versions:

Sunday, April 19, 2009

Adding colors

Since MIDP 2.0 it is possible to discover some colors defined by the handset user.
There is a new method Display.getColor(int), the parameter may be one of six public constants from class Display: COLOR_BACKGROUND, COLOR_BORDER, COLOR_FOREGROUND, COLOR_HIGHLIGHTED_BACKGROUND, COLOR_HIGHLIGHTED_FOREGROUND, COLOR_HIGHLIGHTED_BORDER. The return is a valid color to be used with Graphics.setColor(int).
These colors are the same from the handset main screens - the current theme.
To use these colors lets define five more attributes at our CustomImplicitList:
private int backgroundColor = 0xffffff;
private int foregroundColor;
private int backgroundhHighlightColor = 0xffffff;
private int foregroundHighlightColor;
private int borderHighlightColor;
And add setter method for each one of them.
The value used to initiate backgroundColor and backgroundhHighlightColor represents the white color.
Lets say there are two variables: CustomImplicitList list and a Display display.
list.setBackgroundColor(display.getColor(Display.COLOR_BACKGROUND));
list.setForegroundColor(display.getColor(Display.COLOR_FOREGROUND));
list.setBackgroundhHighlightColor(display.getColor(Display.COLOR_HIGHLIGHTED_BACKGROUND));
list.setForegroundHighlightColor(display.getColor(Display.COLOR_HIGHLIGHTED_FOREGROUND));
list.setBorderHighlightColor(display.getColor(Display.COLOR_HIGHLIGHTED_BORDER));
We are not using COLOR_BORDER because the border of our CustomImplicitList is used only at the selected element.
The last part is to change the paint method of CustomImplicitList:
g.setColor(this.backgroundColor);
g.fillRect(0, 0, getWidth(), getHeight());
// ...
for (int i = 0; i < this.items.length; i++) {
if (i == this.selectedIndex) {
g.setColor(this.backgroundhHighlightColor);
g.fillRect(1, y, getWidth() - 3, font.getHeight());
g.setColor(this.borderHighlightColor);
g.drawRect(1, y, getWidth() - 3, font.getHeight());
g.setColor(this.foregroundHighlightColor);
} else {
g.setColor(this.foregroundColor);
}
g.drawString(this.items[i], getWidth() / 2, y, Graphics.HCENTER Graphics.TOP);
y += font.getHeight();
// it was the last line that could be drawn
if (y + font.getHeight() > height) {
i = this.items.length;
}
}

Thursday, March 26, 2009

Painting custom commands

Commands should be the last elements drawn on the screen. This way the user will always know his options for that screen.
We are using the height of commands area as the default font height, so we need to override getHeight method:
private boolean hasCommands () {
   return (this.leftCommand != null) || (this.rightCommand != null);
}
public int getHeight () {
   int height = super.getHeight();
   
   if (hasCommands()) 
      height -= Font.getDefaultFont().getHeight();

   return height;
}
A simple way to implement command drawing is:
protected paint (Graphics g) {
   // draw other elements

   if (hasCommands()) {
      int commandHeight = Font.getDefaultFont().getHeight();

      // clear command area
      g.setColor(0xffffff); // white
      g.fillRect(0, super.getHeight() - commandHeight, getWidth(), commandHeight);
      g.setColor(0); // black
      g.setFont(Font.getDefaultFont());
      if (this.leftCommand != null) {
         g.drawString(this.leftCommand.getLabel(), 0, super.getHeight(), Graphics.LEFT | Graphics.BOTTOM);
      }
      if (this.rightCommand != null) {
         g.drawString(this.rightCommand.getLabel(), getWidth(), super.getHeight(), Graphics.RIGHT | Graphics.BOTTOM);
      }
   }
}
At LWUIT there is a simple implementation as this at DefaultLookAndFeel class.
A better approach is to give visual feedback on keyPressed calls, for example, changing the font color before painting a command.
Lets define two new attributes and add keyPressed method:
private boolean leftCommandPressed;
private boolean rightCommandPressed;
protected void keyPressed (int keyCode) {
   if (keyCode == this.leftCommandKey && this.leftCommand != null) {
      this.leftCommandPressed = true;
      this.repaint();
   } else if (keyCode == this.rightCommandKey && this.rightCommand != null) {
      this.rightCommandPressed = true;
      this.repaint();
   }
}
Now we change paint method to set command color according to these new attributes.
protected paint (Graphics g) {
   if (hasCommands()) {
      if (this.leftCommand != null) {
         if (this.leftCommandPressed) {
            g.setColor(0x080808); // gray
         } else {
            g.setColor(0); // black
         }
         g.drawString(this.leftCommand.getLabel(), 0, super.getHeight(), Graphics.LEFT | Graphics.BOTTOM);
      }
      if (this.rightCommand != null) {
         if (this.rightCommandPressed) {
            g.setColor(0x080808); // gray
         } else {
            g.setColor(0); // black
         }
         g.drawString(this.rightCommand.getLabel(), getWidth(), super.getHeight(), Graphics.RIGHT | Graphics.BOTTOM);
      }
   }
}
And change keyReleased to reset the attributes:
protected void keyReleased (int keyCode) {
   if (this.commandListener != null){
      if (keyCode == this.leftCommandKey && this.leftCommand != null) {
         this.leftCommandPressed = false;
         this.commandListener.commandAction(this.leftCommand, this);
      } else if (keyCode == this.rightCommandKey && this.rightCommand != null) {
         this.rightCommandPressed = false;
         this.commandListener.commandAction(this.rightCommand, this);
      }
   }
}

Sunday, March 1, 2009

Custom commands

We presented ways to customize title and content with Canvas, but now it is time to also treat commands.
For our code lets define the following attributes:
private int leftCommandKey, rightCommandKey;
A good example of adaptive code to identify command keys can be found at LWUIT class com.sun.lwuit.Implementation, method setKnownSoftKeyCodes.
Below is an adapted snippet to treat Nokia and Motorola commands:


try {
Class.forName("com.nokia.mid.ui.FullCanvas");
this.leftCommandKey = -6;
this.rightCommandKey = -7;
return;
} catch (ClassNotFoundException _ex) { }
try {
Class.forName("com.motorola.phonebook.PhoneBookRecord");
this.leftCommandKey = -21;
this.rightCommandKey = -22;
return;
} catch (ClassNotFoundException _ex) { }

As there are more manufacturers you may keep adding try/catch clauses for specific classes.
One way to avoid this lot of try/catch blocks is the final loop in the method:


boolean leftInit = false;
boolean rightInit = false;
for(int i = -127; i <= 0 && !leftInit && !rightInit; i++) {
try {
if (getKeyName(i).indexOf("1") >= 0) {
this.leftCommandKey = i;
leftInit = true;
}
if (getKeyName(i).indexOf("2") >= 0) {
this.rightCommandKey = i;
rightInit = true;
}
} catch (Exception ex) { }
}

This only works because Implementation class extends Canvas and can use getKeyName method, so your class must extend Canvas too.
From getKeyName javadoc:
"Gets an informative key string for a key. The string returned will resemble the text physically printed on the key. This string is suitable for displaying to the user. For example, on a device with function keys F1 through F4, calling this method on the keycode for the F1 key will return the string "F1". A typical use for this string will be to compose help text such as "Press F1 to proceed."
This method will return a non-empty string for every valid key code.
There is no direct mapping from game actions to key names. To get the string name for game action GAME_A, the application must call
getKeyName(getKeyCode(GAME_A))"
From the loop above we can deduce that commands are SOFT keys and that number 1 is the left and number 2 is the right.
We will try to keep the same behavior and call CommandListener.commandAction when one of these keys is released, but first we need to know which Command will be used for each soft key.
Commands are set with Displayable.addCommand and the listener is set with Displayable.setCommandListener.
As there is no getter method for commands or listener it is necessary to override them.


private Command leftCommand, rightCommand;
public void addCommand (Command cmd) {
if (this.leftCommand == null) {
this.leftCommand = cmd;
} else if (rightCommand == null) {
this.rightCommand = cmd;
}
}
private CommandListener commandListener;
public void setCommandListener (CommandListener l) {
this.commandListener = l;
}

The final piece of code:


protected void keyReleased (int keyCode) {
if (this.commandListener != null){
if (keyCode == this.leftCommandKey && this.leftCommand != null) {
this.commandListener.commandAction(this.leftCommand, this);
} else if (keyCode == this.leftCommandKey && this.rightCommand != null) {
this.commandListener.commandAction(this.rightCommand, this);
}
}
}


Related topics: