Sunday, August 31, 2008

Porting LWUIT - The LWUIT Micro Backend

Porting LWUIT is something I need to blog on more, which is something I fully intend to do. In the meantime Guillaume Legris sent us a link to a port he is working on of LWUIT on top of the Micro Backend.
I wasn't even aware of the existence of the micro backend before this email, which shows off how open sourcing a product can result in such pleasant and unexpected surprises. If you are working on such porting projects we obviously will try to help, I also intend to start a series of posts about the task of porting LWUIT to other platforms in the near future.

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

Monday, August 25, 2008

Touch Screen Support On The Samsung/Sprint Instinct Device

Chen and myself created a new video featuring some of the touch screen visual effects we demoed in the simulator here on the Instinct device.
No 3D transition effects are demoed since the device doesn't support JSR 184 (notice that LWUIT still runs unmodified on such a device!). We hope to upload additional such videos but most of our devices got confiscated by people around the office (our N95 is completely gone).

Sunday, August 24, 2008

The Model (MVC): Million Contacts March

Swing's approach to MVC is one of the hardest concepts for people to fully grasp, which is a real shame as it is probably the most important and powerful feature in Swing. LWUIT copied Swing's approach to MVC almost entirely but at a smaller scale. Chen already blogged about renderers in the past but that is only one piece of the puzzle, to fully understand it we need to understand models... But first lets recap, what is MVC:

Model - Represents the data for the component (list), the model can tell us exactly how many items are in it and which item resides at a given offset within the model. This differs from a simple Vector (or array) since all access to the model is controlled (the interface is simpler) and unlike a Vector/Array the model can notify us of changes that occur within it.

View - The view draws the content of the model. It is a "dumb" layer that has no notion of what is displayed and only knows how to draw. It tracks changes in the model (the model sends events) and redraws itself when it changes.

Controller - The controller accepts user input and performs changes to model which in turn cause the view to refresh.

LWUIT's List component uses the MVC paradigm to separate its implementation. List itself is the Controller (with a bit of View mixed in). The ListCellRenderer interface is a View and the ListModel is (you guessed it by now) the model.

When the list is painted it iterates over the visible elements in the model and asks for them, it then draws them using the renderer.

Why is this useful?

Since the model is a lightweight interface it can be implemented by you and replaced in runtime if so desired, this allows several very cool use cases:

1. A list can contain thousands of entries but only load the portion visible to the user. Since the model will only be queried for the elements that are visible to the user it won't need to load into memory a very large data set until the user starts scrolling down (at which point other elements may be offloaded from memory).

2. A list can cache efficiently. E.g. a list can mirror data from the server into local RAM without actually downloading all the data. Data can also be mirrored from RMS for better performance and discarded for better memory utilization.

3. No need for state copying. Since renderers allow us to display any object type, the list model interface can be implemented by the applications data structures (e.g. persistence/network engine) which would return internal application data structures saving you the need of copying application state into a list specific data structure.

4. Using the proxy pattern (as explained in a previous post) we can layer logic such as filtering, sorting, caching etc. on top of existing models without changing the model source code.

5. We can reuse generic models for several views e.g. a model that fetches data from the server can be initialized with different arguments to fetch different data for different views. View objects in different Form's can display the same model instance in different view instances thus they would update automatically when we change one global model.

Most of these use cases work best for lists that grow to a larger size or represent complex data which is what the list object is designed to do.

To show this off lets create a list with one million entries... What I am trying to prove here is that a list and a model have a very low overhead when used properly. Most of the overhead for rendering a list is in the renderer and the model implementation, both of which you can optimize to your hearts content. This is a very small price to pay for something as flexible, powerful and customizable as the LWUIT list!

class Contact {
private String name;
private String email;
private Image pic;

public Contact(String name, String email, Image pic) {
this.name = name;
this.email = email;
this.pic = pic;
}

public String getName() {
return name;
}

public String getEmail() {
return email;
}

public Image getPic() {
return pic;
}
}

class ContactsRenderer extends Container implements ListCellRenderer {

private Label name = new Label("");
private Label email = new Label("");
private Label pic = new Label("");

private Label focus = new Label("");

public ContactsRenderer() {
setLayout(new BorderLayout());
addComponent(BorderLayout.WEST, pic);
Container cnt = new Container(new BoxLayout(BoxLayout.Y_AXIS));
name.getStyle().setBgTransparency(0);
email.getStyle().setBgTransparency(0);
cnt.addComponent(name);
cnt.addComponent(email);
addComponent(BorderLayout.CENTER, cnt);
}

public Component getListCellRendererComponent(List list, Object value, int index, boolean isSelected) {
Contact person = (Contact) value;
name.setText(index + ": " + person.getName());
email.setText(person.getEmail());
pic.setIcon(person.getPic());
return this;
}

public Component getListFocusComponent(List list) {
return focus;
}
}
String[][] CONTACTS_INFO = {
{"Nir V.","Nir.Vazana@Sun.COM"},
{"Tidhar G.","Tidhar.Gilor@Sun.COM"},
{"Iddo A.","Iddo.Arie@Sun.COM"},
{"Ari S.","Ari.Shapiro@Sun.COM"},
{"Chen F.","Chen.Fishbein@Sun.COM"},
{"Yoav B.","Yoav.Barel@Sun.COM"},
{"Moshe S.","Moshe.Sambol@Sun.COM"},
{"Keren S.","Keren.Strul@Sun.COM"},
{"Amit H.","Amit.Harel@Sun.COM"},
{"Arkady N.","Arcadi.Novosiolok@Sun.COM"},
{"Shai A.","Shai.Almog@Sun.COM"},
{"Elina K.","Elina.Kleyman@Sun.COM"},
{"Yaniv V.","Yaniv.Vakrat@Sun.COM"},
{"Nadav B.","Nadav.Benedek@Sun.COM"},
{"Martin L.","Martin.Lichtbrun@Sun.COM"},
{"Tamir S.","Tamir.Shabat@Sun.COM"},
{"Nir S.","Nir.Shabi@Sun.COM"},
{"Eran K.","Eran.Katz@Sun.COM"}
};

int contactWidth= 36;
int contactHeight= 48;
int cols = 4;
Resources images = Resources.open("/images.res");
Image contacts = images.getImage("people.jpg");
Image[] persons = new Image[CONTACTS_INFO.length];
for(int i = 0; i < persons.length ; i++){
persons[i] = contacts.subImage((i%cols)*contactWidth, (i/cols)*contactHeight, contactWidth, contactHeight, true);
}

final Contact[] contactArray = new Contact[persons.length];
for (int i = 0; i < contactArray.length; i++) {
int pos = i % CONTACTS_INFO.length;
contactArray[i] = new Contact(CONTACTS_INFO[pos][0], CONTACTS_INFO[pos][1], persons[pos]);
}

Form millionList = new Form("Million");
millionList.setScrollable(false);
List l = new List(new ListModel() {
private int selection;
public Object getItemAt(int index) {
return contactArray[index % contactArray.length];
}

public int getSize() {
return 1000000;
}

public int getSelectedIndex() {
return selection;
}

public void setSelectedIndex(int index) {
selection = index;
}

public void addDataChangedListener(DataChangedListener l) {
}

public void removeDataChangedListener(DataChangedListener l) {
}

public void addSelectionListener(SelectionListener l) {
}

public void removeSelectionListener(SelectionListener l) {
}

public void addItem(Object item) {
}

public void removeItem(int index) {
}
});
l.setListCellRenderer(new ContactsRenderer());
l.setFixedSelection(List.FIXED_NONE_CYCLIC);
millionList.setLayout(new BorderLayout());
millionList.addComponent(BorderLayout.CENTER, l);
millionList.show();