Wednesday, June 23, 2010

Headon That Table



A couple of important news before I begin, Terrence Barr has written a great new LWUIT introductory article for java.net and its at the top of java.sun.com... Great for introducing people to LWUIT!



Ofir has just passed a significant milestone of 1000 commits into our java.net SVN repository, take into consideration that we had an internal repository where we are passed the 56k version number... Its not strictly LWUIT commits though ;-)



The main subject of this post though is fixed table headers. The default LWUIT table implementation scrolls the title together with the table body, we made that choice since we assume small tables as the main use case. Fixing a column or a row (such as the titles of the table) into place isn't trivial in an efficient way. However, its possible to do something like that by using two tables and keeping their scrolling in sync.



This works great and pretty seamlessly for touch/keyboard usages however the approach has one major drawback that the size of the title might differ from the size of the cells thus causing the table titles to be misaligned from the table columns.



The solution I chose was to hardcode the cell sizes based on the size of the titles, you can obviously adapt that approach to something more appropriate for your need.



public class FixedTableDemo extends MIDlet {

private static final String[] TITLES = {"Title 1", "Title 2", "Title 3", "Title 4", "Title 5", "Title 6", "Title 7", "Title 8", "Title 9"};

private static final int ROWS = 100;

private static Dimension[] TITLE_SIZES;



static class MirroredTable extends Table {

private MirroredTable mirrorTo;

public MirroredTable(TableModel m) {

super(m);

}



public MirroredTable(TableModel m, boolean b) {

super(m, b);

}



public void setScrollX(int x) {

super.setScrollX(x);

if(isDragActivated()) {

mirrorTo.setScrollX(x);

}

}



/**

* @param mirrorTo the mirrorTo to set

*/


public void setMirrorTo(MirroredTable mirrorTo) {

this.mirrorTo = mirrorTo;

}



public Component createCell(Object value, int row, int column, boolean editable) {

Component c = super.createCell(value, row, column, editable);

if(TITLE_SIZES != null && TITLE_SIZES[column] != null) {

c.setPreferredSize(TITLE_SIZES[column]);

}

return c;

}

}



public void startApp() {

Display.init(this);

Resources r;

try {

r = Resources.open("/LWUITtheme.res");

UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0]));

} catch (IOException ex) {

ex.printStackTrace();

}

Form f = new Form("Table Title");

MirroredTable titlesTable = new MirroredTable(new DefaultTableModel(TITLES, new Object[0][0]));

TITLE_SIZES = new Dimension[TITLES.length];

for(int iter = 0 ; iter < TITLES.length ; iter++) {

TITLE_SIZES[iter] = titlesTable.createCell(TITLES[iter], -1, iter, false).getPreferredSize();

}



Object[][] body = new Object[ROWS][TITLES.length];

for(int rows = 0 ; rows < body.length ; rows++) {

for(int cols = 0 ; cols < body[rows].length ; cols++) {

body[rows][cols] = "" + rows + ", " + cols;

}

}

MirroredTable bodyTable = new MirroredTable(new DefaultTableModel(TITLES, body), false);

bodyTable.setMirrorTo(titlesTable);

titlesTable.setMirrorTo(bodyTable);

bodyTable.setScrollable(true);

titlesTable.setScrollableX(true);

f.setScrollable(false);

f.setLayout(new BorderLayout());

titlesTable.setTensileDragEnabled(false);

bodyTable.setTensileDragEnabled(false);

titlesTable.setIsScrollVisible(false);

f.addComponent(BorderLayout.NORTH, titlesTable);

f.addComponent(BorderLayout.CENTER, bodyTable);

f.show();

}



public void pauseApp() {

}



public void destroyApp(boolean unconditional) {

}

}





Sunday, June 6, 2010

Pimp the VirtualKeyboard - by Chen Fishbein

LWUIT 1.4 is just around the corner, before the release I wanted to share
Some of the VirtualKeyboard enhancements/improvements that will be released as part of 1.4.

Since the VirtualKeyboard is a pure LWUIT component it can be customized in various ways:

1. Changing the Virtual Keyboard look – All Virtual Keyboard items can be customized from the resource editor, the associated ui id's are:

VKB – this id is used to style the Virtual Keyboard body.
VKBtooltip – this id is used to style the popup tooltip.
VKBButton – this id is used to style a regular button on the virtual keyboard (usually a char or a string).
VKBSpecialButton – this id is used to style the special buttons such as: 'Space', 'SH', ...
VKBTextInput – this id is used to style the textfield on the virtual keyboard.






















2. Adding a language -

The example below demonstrates how to add an input mode that supports hebrew:
Create an array of String arrays, each array represents a buttons column.
private static final String[][] DEFAULT_HEBREW = new String[][]{       
{"\u05e7", "\u05e8", "\u05d0", "\u05d8", "\u05d5", "\u05df", "\u05dd", "\u05e4", "$Delete$"}, 
{"\u05e9", "\u05d3", "\u05d2", "\u05db", "\u05e2", "\u05d9", "\u05d7", "\u05dc", "\u05da"}, 
{"\u05d6", "\u05e1", "\u05d1", "\u05d4", "\u05e0", "\u05de", "\u05e6", "\u05ea", "\u05e5"}, 
{"$Mode$", "$Space$", "\u05E3", "$OK$"}   };

Now extends the VirtualKeyboard and make sure when the VirtualKeyboard is initialized the new language mode is added.

public static class HebrewK extends VirtualKeyboard {  
public HebrewK() {    
   addInputMode("\u05d0\u05d1\u05d2", DEFAULT_HEBREW);    
   setInputModeOrder(new String[]{"\u05d0\u05d1\u05d2", QWERTY_MODE,    
   NUMBERS_SYMBOLS_MODE, NUMBERS_MODE, SYMBOLS_MODE   
  }
 );  
} 
}
Now you need to make sure the new HebrewK will be used as the default virtual keyboard.
Call this:
VKBImplementationFactory.init(HebrewK.class);
instead of the regular
VKBImplementationFactory.init();



3. Binding a VirtualKeyboard to a TextField – Now we have a use case where a TextField should accept only numbers, therefore launching the regular VirtualKeyboard will be a mistake.
What we need to do is to create a 'numbers only' VirtualKeyboard and launch it on a specific TextField.

TextField txt = new TextField(); 
txt.setConstraint(TextField.NUMERIC); 
txt.setInputModeOrder(new String[]{"123"}); 
txt.setInputMode("123");  
VirtualKeyboard vkb = new VirtualKeyboard(); 
vkb.setInputModeOrder(new String[]{VirtualKeyboard.NUMBERS_MODE});  
VirtualKeyboard.bindVirtualKeyboard(txt, vkb);

4. Adding your own button to a TextField – There are several use cases where you would want to place your own buttons on a specific Virtual Keyboard, for example if you are asking the user to insert input for a search field you might want a “search” command instead of the regular “ok” command that will automatically when pressed will invoke a submit action to the network.
To accomplish this you need to create a new virtual keyboard, declare your own input buttons and to add your own special button to be part of the virtual keyboard.



Declare a new input with a new special button “Search” (By default Virtual Keyboard is able to understand only the following special keys: "Shift", "Delete", "T9", “Mode”, “Space”, “OK”):

String[][] SEARCH_QWERTY = new String[][]{ 
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"}, 
{"a", "s", "d", "f", "g", "h", "j", "k", "l"}, 
{"$Shift$", "z", "x", "c", "v", "b", "n", "m", "$Delete$"}, 
{"$Mode$", "$Space$", "$Search$"} };  

VirtualKeyboard vkb = new VirtualKeyboard(); 
//add the new input mode 
vkb.addInputMode("ABC_S", SEARCH_QWERTY); 
vkb.setInputModeOrder(new String[]{"ABC_S"}); 
//add the new special button to the vkb 
vkb.addSpecialButton("Search", new Command("Search") {  
public void actionPerformed(ActionEvent evt) { 
  //search logic
... 
} 
});  
//bind the vkb to the textfield 
VirtualKeyboard.bindVirtualKeyboard(txt, vkb); 
f.addComponent(txt); 

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;

}

}