Wednesday, July 2, 2008

Pimp My LWUIT Part 1: Gradient Galore

This is part one of hopefully a long series covering UI customizations in LWUIT, it is written mostly for the programmers among us and to a lesser extent for the designers (although they can probably grab ideas from here/make them beautiful). I will try to cover as many grounds of UI look as possible, from advanced rendering concepts to animations and customizations.

Most of the work will be in the look and feel level to show application wide changes rather than singular changes for one form/component.

Lets begin with gradients, you might recall them from the 90's... Todays gradients are more subtle but they are still around in full force, for the uninitiated a gradient is the slow transition from one color to another color. There are two major types of gradients: linear and radial. A linear gradient is like a strip while in a radial gradient the transition is circular.

Gradients are implemented in the LWUIT Graphics class which has two distinct methods to create gradients: fillLinearGradient & fillRadialGradient.
Both of these accept the coordinate of the gradients and both of these are very slow! Drawing a gradient is computationally intensive so unless your gradient is very small you should probably cache the result in an image object and use that instead of calling the fill gradient methods for every paint.

We discussed painters in the past, but just a small recap: Painters allow us to install custom rendering (drawing) logic for the background of components without actually deriving or changing the component source code. So essentially we can install a renderer onto any LWUIT component and draw anything we want within this renderer.
This is a simple linear gradient painter and the code that uses it:
/**
* A simple painter that can draw a linear gradient as the background of any component.
* The gradient is cached in a mutable image for performance.
*
* @author Shai Almog
*/

public class LinearGradientPainter implements Painter {
private int sourceColor;
private int destColor;
private boolean horizontal;
private Image cache;
public LinearGradientPainter(int sourceColor, int destColor, boolean horizontal) {
this.sourceColor = sourceColor;
this.destColor = destColor;
this.horizontal = horizontal;
}

public void paint(Graphics g, Rectangle rect) {
Dimension d = rect.getSize();
int x = rect.getX();
int y = rect.getY();
int height = d.getHeight();
int width = d.getWidth();
if(horizontal) {
if(cache == null || width != cache.getWidth()) {
cache = Image.createImage(width, 1);
cache.getGraphics().fillLinearGradient(sourceColor, destColor, 0, 0, width, 1, horizontal);
}
for(int iter = 0 ; iter < height ; iter++) {
g.drawImage(cache, x, y + iter);
}
} else {
if(cache == null || height != cache.getHeight()) {
cache = Image.createImage(1, height);
cache.getGraphics().fillLinearGradient(sourceColor, destColor, 0, 0, 1, height, horizontal);
}
for(int iter = 0 ; iter < width ; iter++) {
g.drawImage(cache, x + iter, y);
}
}
}
}


Form helloForm = new Form("Pimped MIDlet");
helloForm.getStyle().setBgPainter(new LinearGradientPainter(0, 0xffffff, false));

helloForm.addComponent(new Label("Hello World"));
helloForm.show();
The linear gradient is relatively efficient since it can hold one line/column in memory and just "tile" it across the screen. The same doesn't hold true for a radial gradient since the lines in a radial gradient are rather different. However the results for a radial gradient are far more stunning...
/**
* A simple painter that can draw a radial gradient as the background of any component.
* The gradient is cached in a mutable image for performance.
*
* @author Shai Almog
*/

public class RadialGradientPainter implements Painter {
private int sourceColor;
private int destColor;
private Image cache;
public RadialGradientPainter(int sourceColor, int destColor) {
this.sourceColor = sourceColor;
this.destColor = destColor;
}

public void paint(Graphics g, Rectangle rect) {
Dimension d = rect.getSize();
int x = rect.getX();
int y = rect.getY();
// we fill a larger area and make sure the area is square otherwise the radial
// effect won't look right (stretched) it will also end with a circle rather than
// a square area
int width = d.getWidth();
int height = d.getHeight();
int size = Math.max(width, height);
if(cache == null || size != cache.getWidth()) {
cache = Image.createImage(size, size);
Graphics g2 = cache.getGraphics();

g2.setColor(destColor);
g2.fillRect(0, 0, size, size);
g2.fillRadialGradient(sourceColor, destColor, (width - size) / 2, (height - size) / 2, size, size);
}
g.drawImage(cache, x, y);
}
}

Form helloForm = new Form("Pimped MIDlet");
helloForm.getStyle().setBgPainter(new RadialGradientPainter(0x999999, 0));
helloForm.addComponent(new Label("Hello World"));
helloForm.show();
Notice that we position the radial gradient in the center but we can place it anywhere and use any set of colors to create stunning effects. It is more memory intensive so use it with caution but when it works its just absolutely stunning... To create a single efficient painter for all Forms in the system rather than manually setting the style of every form we can do something rather trivial:
public class PimpLookAndFeel extends DefaultLookAndFeel {
private Painter formPainer = new RadialGradientPainter(0x999999, 0);
public void bind(Component c) {
if(c instanceof Form) {
if(c instanceof Dialog) {
((Dialog)c).getContentPane().getStyle().setBgPainter(formPainer);
} else {
c.getStyle().setBgPainter(formPainer);
}
}


}
}
Now all forms in the application will have this cool gradient effect and you don't even need to add an image to your JAR file. Notice that I don't allow the di alog to use this painter, if a painter is installed on a dialog it will paint over the background form... The dialog has a custom special painter that draws the form in the background normally we want to override the content pane of the dialog and not its internal painter.

Now we can also install linear gradients on the menu/title bar and provide a more 3D feel for the application as such:
/**
* Look and feel implementing some of the abilities for elaborate UI's.
*


* @author Shai Almog
*/

public class PimpLookAndFeel extends DefaultLookAndFeel {
private Painter formPainer = new RadialGradientPainter(0x999999, 0);
public void bind(Component c) {
if(c instanceof Form) {
if(c instanceof Dialog) {
((Dialog)c).getContentPane().getStyle().setBgPainter(formPainer);
} else {
c.getStyle().setBgPainter(formPainer);
}
Form f = (Form)c;
f.getTitleStyle().setBgPainter(new LinearGradientPainter(0xffffff, 0xaaaaaa, false));

// this is available only in the latest LWUIT drop not yet released currently
// use setSoftButtonStyle
f.getSoftButtonStyle().setBgPainter(new LinearGradientPainter(0xaaaaaa, 0xffffff, false));

}
}
}
Notice that I reversed the direction of the gradient in the softbutton area to create a direction for the UI.

The main problem I have right now is with the radial gradient being wasteful for the dialog which is smaller, I would also like to theme a menu differently from a dialog but that is for a future post... For now I will just convert the dialog to use a linear gradient instead:
if(c instanceof Dialog) {
((Dialog)c).getContentPane().getStyle().setBgPainter(new LinearGradientPainter(0xcccccc, 0, true));
} else {
c.getStyle().setBgPainter(formPainer);
}
This is it for this installment of pimp my LWUIT, next time I will continue where we left off and delve into some other advanced customization options.

14 comments:

  1. The gradient, though working, does not fill the entire form. It seems there's a component at the center of the form that hides the gradient. I am using LWUIT 20080814 version.

    ReplyDelete
  2. a. Which gradient? (radial/linear).
    b. How are you using it?
    c. Did you test it with the latest LWUIT SVN code?
    d. Do you have an image showing the issue?

    ReplyDelete
  3. a. Both, linear and radial.

    b. On the startApp method of the MIDlet, after calling Display.init(this);, I called an initialize method that contains the code below:

    Form helloForm = new Form("Pimped MIDlet");
    helloForm.getStyle().setBgPainter(new LinearGradientPainter(0, 0xffffff, false));
    helloForm.addComponent(new Label("Hello World"));
    helloForm.show();


    The same happened when I replaced the line having the LinearGradientPainter with RadialGradientPainter:

    helloForm.getStyle().setBgPainter(new RadialGradientPainter(0x999999, 0));

    c. I only tested this on your released library, I haven't try downloading from your subversion server.

    d. Yes, I have a screenshot, but don't know how to post it here.

    Thanks!

    ReplyDelete
  4. Label is opaque by default you need to set its transparency (in the style) to 0 e.g.:label.getStyle().setBgTransparency(0);

    If this doesn't help place the image in flickr, picasa etc. and place a link here.

    ReplyDelete
  5. I also tried it without a label:

    Form helloForm = new Form("Pimped MIDlet");
    helloForm.getStyle().setBgPainter(new LinearGradientPainter(0, 0xffffff, false));
    //helloForm.addComponent(new Label("Hello World!"));
    helloForm.show();


    see http://farm4.static.flickr.com/3153/2918476998_f19ccf7f48_o.jpg

    This also happened on my Nokia N80 phone. The "gradiented" form is covered by an unknown component.

    ReplyDelete
  6. Ok, I found the solution:

    helloForm.getContentPane().getStyle().setBgTransparency(0);

    This should be called after helloForm is created. Thanks!

    ReplyDelete
  7. I can't get the gradient to work.

    cache.getGraphics().fillLinearGradient(sourceColor, destColor, 0, 0, width, 1, horizontal);

    the above code generates a compile time error:
    C:\workspace\vclikChat\src\util\LinearGradientPainter.java:44: cannot find symbol
    symbol : method fillLinearGradient(int,int,int,int,int,int,boolean)
    location: class javax.microedition.lcdui.Graphics
    cache.getGraphics().fillLinearGradient(sourceColor, destColor, 0, 0, width, 1, horizontal);

    I have the right LWUIT jar file in my project. For some reason the code is looking up javax.microedition.lcdui.Graphics for the fillLinearGradient method.

    ReplyDelete
  8. Your import statement is incorrect. com.sun.lwuit.Graphics

    ReplyDelete
  9. Shai, this is great! Thanks for all the excellent examples.

    Karl

    ReplyDelete
  10. Hmmm I'm getting bars from top to bottom, it is a gradient, but each new color forms a bar :o

    So to make it simple:
    white
    light gray
    gray
    dark gray
    black

    it isn't fluid, but in pieces.
    Hope you understand what I mean :p
    If not: here's a picture showing the problem:
    http://img440.imageshack.us/img440/7076/gradientbarszw5.jpg

    ReplyDelete
  11. Got the feeling I need a newer lwuit library or something, can you point me in a direction ? :)

    ReplyDelete
  12. You are probably using the WTK which defaults to 4096 colors on the device. Try using a newer simulator or edit the device properties for a minimum of 65k colors (as is common on today's phones).

    ReplyDelete
  13. Hmmmm can't really find how to change this, I'm using Netbeans IDE 6.5 with the defaultColorPhone emulator.

    ReplyDelete
  14. Got it working now with the Nokia S60 JDK emulator, but if u can set some color option to 65k with the defaultColorPhone emulator I'd love to know how :)

    Thanks in advance !

    ReplyDelete