Wednesday, July 30, 2008

As You Like It: Layouts any way you want (how to build a layout manager)


Layout managers in LWUIT are a remarkably powerful tool, I won't go into all the elaborate ways in which you can modify the layout in LWUIT since this is covered rather well in the tutorial and developer guide. Instead I will try to show something that is a bit under documented, mostly because its almost exactly like its Swing/AWT equivalent: building a layout manager.

A layout manager contains all the logic for positioning LWUIT components, it essentially traverses a LWUIT container and positions components absolutely based on internal logic. When we build our own component we need to take padding into consideration, when we build the layout we need to take margin into consideration. Building a layout manger involves two simple methods: layoutContainer & getPreferredSize.

layoutContainer is invoked whenever LWUIT decides the container needs rearranging, LWUIT tries to avoid calling this method and only invokes it at the last possible moment. Since this method is generally very expensive (imagine the recursion with nested layouts...), LWUIT just marks a flag indicating layout is "dirty" when something important changes and tries to avoid "reflows".

getPreferredSize allows the layout to determine the size desired for the container, this might be a difficult call to make for some layout managers that try to provide both flexibility and simplicity. Most of flow layout bugs stem from the fact that this method is just impossible to implement for flow layout. The size of the final layout won't necessarily match the requested size (it probably won't) but the requested size is taken into consideration, especially when scrolling and also when sizing parent containers.

This is a layout manager that just arranges components in a center column aligned to the middle:
/**
* Layout manager that arranges components in a center column
*
* @author Shai Almog
*/

public class CenterLayout extends Layout {
public void layoutContainer(Container parent) {
int components = parent.getComponentCount();
Style parentStyle = parent.getStyle();
int centerPos = parent.getLayoutWidth() / 2 + parentStyle.getMargin(Component.LEFT);
int y = parentStyle.getMargin(Component.TOP);
for(int iter = 0 ; iter < components ; iter++) {
Component current = parent.getComponentAt(iter);
Dimension d = current.getPreferredSize();
current.setSize(d);
current.setX(centerPos - d.getWidth() / 2);
Style currentStyle = current.getStyle();
y += currentStyle.getMargin(Component.TOP);
current.setY(y);
y += d.getHeight() + currentStyle.getMargin(Component.BOTTOM);
}
}

public Dimension getPreferredSize(Container parent) {
int components = parent.getComponentCount();
Style parentStyle = parent.getStyle();
int height = parentStyle.getMargin(Component.TOP) + parentStyle.getMargin(Component.BOTTOM);
int marginX = parentStyle.getMargin(Component.RIGHT) + parentStyle.getMargin(Component.LEFT);
int width = marginX;
for(int iter = 0 ; iter < components ; iter++) {
Component current = parent.getComponentAt(iter);
Dimension d = current.getPreferredSize();
Style currentStyle = current.getStyle();
width = Math.max(d.getWidth() + marginX + currentStyle.getMargin(Component.RIGHT) +
currentStyle.getMargin(Component.LEFT), width);
height += currentStyle.getMargin(Component.TOP) + d.getHeight() +
currentStyle.getMargin(Component.BOTTOM);
}
Dimension size = new Dimension(width, height);
return size;
}
}
Here is a simple example of using it:
Form f = new Form("Centered");
f.setLayout(new CenterLayout());
for(int iter = 1 ; iter < 20 ; iter++) {
f.addComponent(new Button("Button: " + iter));
}
f.addComponent(new Button("Really Wide Button Text!!!"));
f.show();

Monday, July 28, 2008

emoze LWUIT Application Now Up On YouTube

Hopefully your guys remember emoze which I have mentioned here before. They are finally out the door with one of the first commercial LWUIT deployments and a remarkably heavy push email application. It runs on the very constrained S40 devices shown here in the Nokia emulator but working very well on the device itself.

Sunday, July 27, 2008

Pimp My LWUIT Part V: Shadowing Presence


Shadows are an unbelievable piece of "eye candy", when building a "proper" shadow in desktop OS's one often uses black with an opacity gradient that is then blurred. This creates the remarkable shadows that windows in modern operating systems enjoy. These are REALLY expensive to recreate without serious hardware acceleration, so we won't...
However, a simple translucent black background provides a shadow effect that looks pretty cool in a button and provides depth for a UI. Before we install the shadow we will also change the color scheme of the application since the shadow just won't look well with the old black theme of the "Pimp" series...

Regardless of some of the changes made to the theme notice the gorgeous drop shadow surrounding the gradient menu which gives it depth beyond the default look. This is done by extending the gradient border introduced previously to also draw a shadow with the border.
public class DropShadowRoundedBorderLinearGradient extends Border {
private int sourceColor;
private int destColor;
private boolean horizontal;
private Image cache;
private int arcWidth;
private int arcHeight;
private int borderColor;
private boolean noShadow;

public DropShadowRoundedBorderLinearGradient(int sourceColor, int destColor, boolean horizontal, int borderColor, int arcWidth, int arcHeight) {
this.sourceColor = sourceColor;
this.destColor = destColor;
this.horizontal = horizontal;
this.arcHeight = arcHeight;
this.arcWidth = arcWidth;
this.borderColor = borderColor;
}

public Border createPressedVersion() {
DropShadowRoundedBorderLinearGradient d = new DropShadowRoundedBorderLinearGradient(sourceColor, destColor, horizontal, borderColor, arcWidth, arcHeight);
d.noShadow = true;
return d;
}

public boolean isBackgroundPainter() {
return true;
}

public void paintBorderBackground(Graphics g, Component c) {
int x = c.getX();
int y = c.getY();
int height = c.getHeight();
int width = c.getWidth();
if(cache == null || width != cache.getWidth() || height != cache.getHeight()) {
cache = Image.createImage(width, height);
Graphics current = cache.getGraphics();
int shadow = 0x60000000;
if(noShadow) {
current.setColor(0);
current.fillRoundRect(4, 4, width - 5, height - 5, arcWidth, arcHeight);
} else {
current.setColor(0xcccccc);
current.fillRoundRect(4, 4, width - 5, height - 5, arcWidth, arcHeight);
current.setColor(0);
current.fillRoundRect(0, 0, width - 5, height - 5, arcWidth, arcHeight);
}
int[] rgb = cache.getRGB();
current.fillLinearGradient(sourceColor, destColor, 0, 0, width, height, horizontal);
int[] rgb2 = cache.getRGB();
int white = rgb[0];
int gray = rgb[rgb.length - width - width / 2];
if(noShadow) {
gray = 0xffffff;
}
for(int iter = 0 ; iter < rgb.length ; iter++) {
if(rgb[iter] == white) {
rgb2[iter] = 0;
continue;
}
if(rgb[iter] == gray) {
rgb2[iter] = shadow;
}
}
cache = Image.createImage(rgb2, width, height);
}
g.drawImage(cache, x, y);
}

public void paint(Graphics g, Component c) {
int oldColor = g.getColor();
g.setColor(borderColor);
if(noShadow) {
g.drawRoundRect(c.getX() + 4, c.getY() + 4, c.getWidth() - 5, c.getHeight() - 5, arcWidth, arcHeight);
} else {
g.drawRoundRect(c.getX(), c.getY(), c.getWidth() - 5, c.getHeight() - 5, arcWidth, arcHeight);
}
g.setColor(oldColor);
}
}

Tuesday, July 22, 2008

Pimp My LWUIT Part IV: Where's My ScrollBar?


On part A of the pimp my LWUIT scrollbar saga I showed you how to create a hot looking scrollbar, this time we will bring fades and animations into the fold in a remarkably CPU intensive version of the scrollbar.
I really don't like it when the scrollbar takes up screen real-estate, the alternative is to create a really narrow and ugly scrollbar... but there are other options...
We can just paint the scroll arrows in the menu (but thats really a regression in functionality) or we can just hide the scrollbar. The problem with hiding is that we would need to still indicate that a scrollbar exists, so the solution I picked was to fade it out and keep a very translucent version of the scroll.

The way to achieve this is using an image to draw the scrollbar, I change can the opacity of the image when necessary to indicate scrolling. One of the cool features of this approach is that the scrollbar takes up no space at all and can still be gorgeous/remarkably visual.

Its a tough approach to layer on top of LWUIT since any component within LWUIT can render itself at any given time, we hope to offer a more convenient means of implementing something like this in a future drop.

To support the fade effect we bind an animation to the scroll bar and fade it out using a motion object. The main performance hurdle here is the repaint invoked when focus changes, otherwise there is no other way to detect changes painting on top of the buttons.
public class PimpLookAndFeel extends DefaultLookAndFeel {
private static final Image SCROLL_DOWN;
private static final Image SCROLL_UP;
private ScrollBarAnimation scrollAnimation;
private int opacity = 255;
private int imageOpacity;
private Image scrollBarImage;

static {
Image sd = null;
Image su = null;
try {
sd = Image.createImage("/scrollbar-button-south.png");
su = Image.createImage("/scrollbar-button-north.png");
} catch(IOException ioErr) {
ioErr.printStackTrace();
}
SCROLL_DOWN = sd;
SCROLL_UP = su;
}

private Painter formPainer = new RadialGradientPainter(0x999999, 0);
public PimpLookAndFeel() {
Hashtable themeProps = new Hashtable();
themeProps.put("fgColor", "dddddd");
themeProps.put("SoftButton.fgColor", "0");
themeProps.put("Title.fgColor", "0");
themeProps.put("fgSelectionColor", "ffffff");
themeProps.put("bgColor", "0");
themeProps.put("bgSelectionColor", "0");
themeProps.put("transparency", "0");
themeProps.put("Button.transparency", "130");
themeProps.put("border", Border.getEmpty());
UIManager.getInstance().setThemeProps(themeProps);

Style s = UIManager.getInstance().getComponentStyle("Menu");
s.setBorder(new RoundedBorderLinearGradient(0xff0000, 0xffffff, true, 0xff, 10, 10));
s.setBgTransparency(255);
UIManager.getInstance().setComponentStyle("Menu", s);

s = UIManager.getInstance().getComponentStyle("Dialog");
s.setBorder(Border.createRoundBorder(10, 10));
s.setBgTransparency(100);
s.setBgColor(0);
s.setFgColor(0xffffff);
UIManager.getInstance().setComponentStyle("Dialog", s);
}

public void bind(Component c) {
if(c instanceof Form) {
if(!(c instanceof Dialog)) {
c.getStyle().setBgPainter(formPainer);
}
Form f = (Form)c;
f.getTitleStyle().setBgPainter(new LinearGradientPainter(0xffffff, 0xaaaaaa, false));
f.getSoftButtonStyle().setBgPainter(new LinearGradientPainter(0xaaaaaa, 0xffffff, false));
} else {
c.addFocusListener(this);
}
}

private void drawScrollImpl(Graphics gr, Component c, float offsetRatio, float blockSize, boolean vertical) {
int margin = 3;
int width, height;
width = SCROLL_UP.getWidth();

// check the conditions requiring us to redraw the cached image
if(scrollBarImage == null || imageOpacity != opacity || scrollBarImage.getHeight() != c.getHeight()) {
int aX, aY, bX, bY;
aX = margin;
bX = aX;
aY = margin;
bY = c.getHeight() - margin - SCROLL_UP.getHeight();
height = c.getHeight() - SCROLL_UP.getHeight() * 2 - margin * 2;
scrollBarImage = Image.createImage(SCROLL_UP.getWidth() + margin * 2, c.getHeight());
Graphics g = scrollBarImage.getGraphics();
g.setColor(0);
g.fillRect(0, 0, scrollBarImage.getWidth(), scrollBarImage.getHeight());
g.setColor(0xffffff);
g.fillRect(aX, aY + SCROLL_UP.getHeight(), width, height);
g.drawImage(SCROLL_UP, aX, aY);
g.drawImage(SCROLL_DOWN, bX, bY);

aY += SCROLL_UP.getHeight();
g.setColor(0xcccccc);
g.fillRoundRect(aX + 2, aY + 2, width - 4, height - 4, 10, 10);
g.setColor(0x333333);
int offset = (int)(height * offsetRatio);
g.fillRoundRect(aX + 2, aY + 2 + offset, width - 4, (int)(height * blockSize), 10, 10);
if(opacity != 255) {
scrollBarImage = scrollBarImage.modifyAlpha((byte)opacity, 0);
}
}

gr.drawImage(scrollBarImage, c.getX() + c.getWidth() - width - margin, c.getY());
}

/**
* @inheritDoc
*/

public void focusGained(final Component cmp) {
if(cmp instanceof Label) {
super.focusGained(cmp);
}
cmp.getComponentForm().repaint();
}

/**
* @inheritDoc
*/

public void focusLost(Component cmp) {
if(cmp instanceof Label) {
super.focusLost(cmp);
}
}

/**
* Draws a vertical scoll bar in the given component
*/

public void drawVerticalScroll(Graphics g, Component c, float offsetRatio, float blockSizeRatio) {
checkParentAnimation(c, offsetRatio, blockSizeRatio, false);
drawScrollImpl(g, c, offsetRatio, blockSizeRatio, true);
}

/**
* Scrollbar is drawn on top of existing widgets
*/

public int getVerticalScrollWidth() {
return 0;
}

/**
* Scrollbar is drawn on top of existing widgets
*/

public int getHorizontalScrollHeight() {
return 0;
}

private void checkParentAnimation(Component c, float offset, float blockSizeRatio, boolean vertical) {
if(scrollAnimation == null || (!scrollAnimation.isOK(offset, blockSizeRatio, c))) {
Form parent = c.getComponentForm();
scrollAnimation = new ScrollBarAnimation(parent, c, offset, blockSizeRatio, vertical);
}
}

private class ScrollBarAnimation implements Animation {
private Form parent;

private Component cmp;
private float scrollOffset;
private float blockSize;
private boolean vertical;

private Motion fadeMotion;
private long time = System.currentTimeMillis();

public ScrollBarAnimation(Form parent, Component cmp, float scrollOffset, float blockSize, boolean vertical) {
this.parent = parent;
parent.registerAnimated(this);
this.cmp = cmp;
this.scrollOffset = scrollOffset;
this.blockSize = blockSize;
this.vertical = vertical;
fadeMotion = Motion.createLinearMotion(255, 70, 2000);
opacity = 255;
}

public Component getComponent() {
return cmp;
}

public boolean isOK(float scrollOffset, float blockSize, Component cmp) {
if(scrollOffset == this.scrollOffset && blockSize == this.blockSize && cmp == this.cmp) {
return true;
}
if(parent != null) {
parent.deregisterAnimated(this);
}
return false;
}

public boolean animate() {
if(!parent.isVisible()) {
parent.deregisterAnimated(this);
return false;
}
if(fadeMotion != null) {
// wait one second before starting to fade...
if(time != 0) {
if(System.currentTimeMillis() - time >= 1000) {
fadeMotion.start();
time = 0;
}
return false;
}
int value = fadeMotion.getValue();
if(fadeMotion.isFinished()) {
fadeMotion = null;
}
if(opacity != value) {
opacity = value;
cmp.repaint();
}
return false;
}
parent.deregisterAnimated(this);
return false;
}

public void paint(Graphics g) {
}
}
}