Thursday, August 28, 2008

Pimp My LWUIT Part 6: GlassPane ScrollBar

I mentioned the GlassPane ScrollBar before but the code wasn't public yet. It has been out for some time now so I should probably refresh my previous post explaining how to achieve effects such as this as well as some other very cool special effects you can accomplish with such a tool.

A painter is an interface that abstracts the idea of painting, unlike a component which has a state and a hierarchy (everything from focus to enabled state, positioning etc..). Painter doesn't carry all of that complexity, it only paints a region and keeping state is purely optional. A painter can be as simple as an image (tiled, scaled, centered etc.) or as elaborate as a special effect like tinting, blurring, swirling etc... Since you decide what to paint and how to do so in runtime a painter can be very smart, it can adapt to device capabilities (e.g. using 3D or SVG when applicable and falling back to plain images when not), it can often scale better for different resolutions (since it can use primitive vector graphics).

All LWUIT components support background painters, this allows you to install custom code to paint the background of each and every LWUIT component without deriving or changing their source code. This is a very powerful tool allowing a developer the ability to customize the UI of LWUIT in unprecedented ways! With our current version we incorporated the glass pane this is a term borrowed directly from Swing however we implemented it differently. The LWUIT glass pane is a painter drawn on top of the form, this allows a developer to paint something after the form has completed painting without worrying about corruption.

Why not just derive from Form and override paint?

Because it might not be invoked... E.g. a Form with a button on it will invoke paint() the first time it is shown.
However when the user selects or presses the button this triggers a Button.repaint() call which will only call the paint method of the button (not the entire form). This is an efficient approach for painting but it will obviously circumvent any attempt at drawing something on top of that button without deriving it as well...

Deriving all the components in the form is not very feasible due to the complexity of repeatedly painting the same area (this matters when the glass pane/component is translucent), not to mention the labor involved in doing something like this...

Unlike deriving from a Form the glass pane is clever about individual component repaints, such components seamlessly trigger a paint of the glass pane itself with the proper clipping region to avoid a situation of painting the entire screen just to reconstruct a small area.

To change the original PimpLookAndFeel we just update bind to use the glass pane but most of the code remains the same (some coordinate calculations were changed and minor bugs were fixed so the full source is pasted bellow):
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;
private Component scrollComponent;
private float offsetRatio;
private float blockSizeRatio;

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(0xffd800, 0xfffa75);
public PimpLookAndFeel() {
Hashtable themeProps = new Hashtable();
themeProps.put("fgColor", "666666");
themeProps.put("SoftButton.fgColor", "666666");
themeProps.put("Title.fgColor", "0");
themeProps.put("fgSelectionColor", "0");
themeProps.put("bgColor", "ffd800");
themeProps.put("bgSelectionColor", "ffd800");
themeProps.put("transparency", "0");
themeProps.put("CommandList.margin", "6,6,6,6");
themeProps.put("Button.transparency", "130");
themeProps.put("border", Border.getEmpty());
UIManager.getInstance().setThemeProps(themeProps);

Style s = UIManager.getInstance().getComponentStyle("Menu");
s.setBorder(new DropShadowRoundedBorderLinearGradient(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(170);
s.setBgColor(0);
s.setFgColor(0xffffff);
UIManager.getInstance().setComponentStyle("Dialog", s);

s = UIManager.getInstance().getComponentStyle("DialogBody");
s.setBorder(null);
s.setBgTransparency(0);
s.setFgColor(0xffffff);
s.setFgSelectionColor(0xffffff);
s.setFont(Font.createSystemFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_LARGE));
UIManager.getInstance().setComponentStyle("DialogBody", s);
}

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

// install a glass pane that will draw the
PainterChain.installGlassPane(f, new Painter() {
public void paint(Graphics g, Rectangle rect) {
if(scrollComponent != null && scrollComponent.getComponentForm() == f) {
checkParentAnimation(scrollComponent, offsetRatio, blockSizeRatio, false);
drawScrollImpl(g, scrollComponent, offsetRatio, blockSizeRatio, true);
}
}
});
}
}

private void drawScrollImpl(Graphics gr, Component c, float offsetRatio, float blockSize, boolean vertical) {
int posX = c.getAbsoluteX() + c.getScrollX();
int posY = c.getAbsoluteY() + c.getScrollY();
gr.translate(posX, posY);
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);
scrollBarImage = scrollBarImage.modifyAlpha((byte)opacity, 0);
}

gr.drawImage(scrollBarImage, c.getWidth() - width - margin, 0);
gr.translate(-posX, -posY);
}


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

public void drawVerticalScroll(Graphics g, Component c, float offsetRatio, float blockSizeRatio) {
scrollComponent = c;
this.offsetRatio = offsetRatio;
this.blockSizeRatio = blockSizeRatio;
}

/**
* 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) {
}
}
}

2 comments:

  1. Hello!
    Your code is great, the output is very nice... but I just can't make it work =(
    I've tried creating a form, binding it to a PimpLookAndFeel object, but I can't make the scroll show. Can you help me? How should my midlet look?

    ReplyDelete
  2. Did you add content to the form so a scroll will show?
    Try getting a scroll to show without the PIMP LF, then make sure to initialize the look before creating/showing the form.
    Also make sure you have all the image resources in place and aren't getting any exceptions.

    ReplyDelete