Tuesday, May 25, 2010

No Longer LOST

LOST has come to a rather disappointing end so this is probably my last LWUIT themed lost demo... Fortunately due to the size of the lost cast I was able to get a decently large set of names for this particular demo.



I really don't like scrollbars on touch devices, they don't "feel" right especially once you have used a proper touch device (please enough with the resistive displays...). Every now and again we get a "scrollbar type scrolling" in LWUIT request and we always say the same thing "get over it". Scrollbars can't work on small screens, only gestures can...

Then I tried the Android address book... It was illuminating in the sense that it kept the kinetic scroll gestures I love but provided this unintrusive thumb next to the scrollbar that would "appear" when touching the screen and gently fold when you let go of the screen. The cool thing about it is how it reacts to dragging. Unlike the rest of the screen, when you drag the thumb it acts similarly to a scrollbar thumb by dragging in the opposite direction...

This on its own would not seem like a big deal but the cool part is that when you use this method a letter indicating the area of the address book where you are is displayed in the center of the screen!

This allows users to find what they are looking for much faster on Android devices when dragging their thumb, the best part is that it isn't limited just to English and can work for every language! (E.g. the iPhone's right side index of letters doesn't localize well).



I decided I want something like this in LWUIT and implemented it in the code bellow (you can check the LWUIT incubator for the full code), since its purely in LWUIT it will work for all touch devices including J2ME devices such as the Nokia 5230 in the video (a 200 USD unlocked phone!).

As a bonus my version does some things the native Android version doesn't e.g. LWUIT supports screen rotation with this feature and the Android contacts application is always in portrait mode.



There are some requirements such as the list has to be the top level component in a none-scrollable form since it needs to do its own scrolling. I overrode the scrolling behavior when detecting the thumb which is why I had to derive the list. It was also useful for me when writing the letters for the entries.



Other than that the code is relatively simple and shows how you can manipulate LWUIT's scrolling behavior completely without changing a single line of code in LWUIT itself...



/**

* This class must be the top level scrollable to work propely, all of its parent containers

* must be scrollable false!

*

* @author Shai Almog

*/


public class ThumbList extends List {

/**

* Delay for the thumb to start "returning" from the moment the user released the touch screen.

*/


private static final int THUMB_SLIDEBACK_DELAY = 2200;



/**

* Duration for the slide animation

*/


private static final int THUMB_SLIDE_DURATION = 300;



/**

* Indicates whether the thumb image is is showing

*/


private boolean thumbShowing = false;



/**

* Flags for thumb slide timeout

*/


private long thumbTimerStartTime;

private int thumbTimer = -1;



/**

* Animation motion returning the thumb to its "place"

*/


private Motion thumbSlidebackMotion;



/**

* Thumb coordinates on the screen, the X isn't the real X since the width should be added

*/


private int thumbPositionX;

private int thumbPositionY;



/**

* Flag indicating that we are now dragging via the thumb and not the gesture

*/


private boolean thumbDragMode;



/**

* Background image for the letter displayed on the screen

*/


private Image transparentRoundRect;



/**

* Font used for the letter on the screen during thumb drag mode

*/


private Font largeFont = Font.createSystemFont(Font.FACE_PROPORTIONAL, Font.STYLE_BOLD, Font.SIZE_LARGE);



/**

* Image of the thumb

*/


private Image thumb;



public ThumbList(ListModel model) {

super(model);

try {

thumb = Image.createImage("/thumb.png");



// the thumbnail image is too small for really high DPI devices, double it

if(Display.getInstance().getDisplayWidth() > 600 || Display.getInstance().getDisplayHeight() > 600) {

thumb = thumb.scaledHeight(thumb.getHeight() * 2);

}

} catch (IOException ex) {

ex.printStackTrace();

}

int size = largeFont.charWidth('W') * 3;

transparentRoundRect = Image.createImage(size, size);

Image mask = Image.createImage(size, size);

Graphics g = mask.getGraphics();

g.setColor(0);

g.fillRect(0, 0, size, size);

g.setColor(0x999999);

g.fillRoundRect(0, 0, size, size, 12, 12);

g = transparentRoundRect.getGraphics();

g.setColor(0xffffff);

g.fillRoundRect(0, 0, size, size, 12, 12);

g.setColor(0);

g.fillRoundRect(2, 2, size - 4, size - 4, 12, 12);

transparentRoundRect = transparentRoundRect.applyMask(mask.createMask());

}



/**

* We must register as an animated otherwise the thumb won't get callbacks to slide back into place

*/


protected void initComponent() {

getComponentForm().registerAnimated(this);

}



protected void deinitialize() {

getComponentForm().deregisterAnimated(this);

}



/**

* Overriding the press to detect thumb presses and to start showing the thumb

*/


public void pointerPressed(int x, int y) {

thumbShowing = true;

thumbSlidebackMotion = null;

thumbTimer = -1;

thumbPositionX = thumb.getWidth();

int myY = y - getAbsoluteY() - getScrollY();

int myX = x - getAbsoluteX() - getScrollX();

if(myX >= getWidth() - thumbPositionX && myY >= thumbPositionY && myY <= thumbPositionY + thumb.getHeight()) {

thumbDragMode = true;

return;

}



super.pointerPressed(x, y);

}



/**

* We block pointer events from the list when in thumb drag mode and move the list

* ourselves in this method

*/


public void pointerDragged(int x, int y) {

if(thumbDragMode) {

float scrollH = getScrollDimension().getHeight() + thumb.getHeight();

float ratio = ((float)y - getAbsoluteY() - getScrollY()) / ((float)getHeight());

setScrollY((int)(scrollH * ratio));

repaint();

} else {

super.pointerDragged(x, y);

}

}



/**

* We block pointer events from the list when in thumb drag mode, we activate the animation

* to hide the thumb

*/


public void pointerReleased(int x, int y) {

if(thumbDragMode) {

thumbDragMode = false;

repaint();

} else {

super.pointerReleased(x, y);

}

thumbTimerStartTime = System.currentTimeMillis();

thumbTimer = THUMB_SLIDEBACK_DELAY;

}



/**

* We don't need to override the actual paint method since we must draw our own scrollbar

*/


protected void paintScrollbarY(Graphics g) {

super.paintScrollbarY(g);

if(thumbShowing) {

float scrollH = getScrollDimension().getHeight() + thumb.getHeight();

float offset = (((float) getScrollY()) / (scrollH - getHeight()));

thumbPositionY = (int) (offset * (getHeight() - thumb.getHeight()));

g.drawImage(thumb, getX() + getWidth() - thumbPositionX, getY() + thumbPositionY);

if(thumbDragMode) {

int tx = g.getTranslateX();

int ty = g.getTranslateY();

g.translate(-tx, -ty);

int x = getWidth() / 2 - transparentRoundRect.getWidth() / 2;

int y = getHeight() / 2 - transparentRoundRect.getHeight();

g.drawImage(transparentRoundRect, x, y);

g.setFont(largeFont);

char c = getCurrentChar();

g.setColor(0xffffff);

g.drawChar(c, getWidth() / 2 - largeFont.stringWidth("" + c) / 2,

getHeight() / 2 - transparentRoundRect.getHeight() / 2 - largeFont.getHeight() / 2);

g.translate(tx, ty);

}

}

}



/**

* Gets the character matching the current list element (assumed) that the user sees on the screen

*/


private char getCurrentChar() {

float scrollH = getScrollDimension().getHeight() + thumb.getHeight();

float offset = (((float) getScrollY()) / (scrollH - getHeight()));

int item = (int)(getModel().getSize() * offset);

if(item < getModel().getSize() && item > 0) {

return ("" + getModel().getItemAt(item)).charAt(0);

}

return ' ';

}



/**

* Update thumb animation state

*/


public boolean animate() {

boolean v = super.animate();

if(thumbTimer > 0) {

long t = System.currentTimeMillis();

thumbTimer = THUMB_SLIDEBACK_DELAY - ((int)(t - thumbTimerStartTime));

if(thumbTimer < 0) {

thumbSlidebackMotion = Motion.createLinearMotion(thumb.getWidth(), 0, THUMB_SLIDE_DURATION);

thumbSlidebackMotion.start();

}

v = true;

} else {

if(thumbSlidebackMotion != null) {

thumbPositionX = thumbSlidebackMotion.getValue();

if(thumbSlidebackMotion.isFinished()) {

thumbSlidebackMotion = null;

thumbShowing = false;

}



// we still want to return true to render the last frame for a finished motion

v = true;

}

}

return v;

}

}



1 comment:

  1. Shai,

    Do you have an example of creating a LWUIT List from a DOM Document in JME?

    Is there a fast, quick, efficient way to transverse Nodes of the Document and add them to the List?

    ReplyDelete