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.

0 comments:

Post a Comment