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

1 comment:

  1. Hi, your sources are cool but I don't know how to use it in my MIDlet. Can you help me?
    Thanks
    Anh

    ReplyDelete