Thursday, July 29, 2010

Sliding It Back To My First Post

My very first post in this blog (May 2008) was about creating a progress indicator component. At the time LWUIT only had one style per component and the post was mostly about threading in LWUIT. Over that time we considered adding a progress indicator component frequently but had a very difficult issue with its customization. How can we create a component which is both powerful enough for general usage and not too restricted for the various use cases.

Enter the new Slider component which thanks to some advantages such as the multiple styles is far more powerful than the original and it is now a part of LWUIT. The slider can be manipulated like a gauge to control elements such as volume, it can be used to indicate position (with a text overlay inherited from Label) and it can be customized in pretty much any way imaginable.



The demo you see to your right is ridiculously simple containing 3 such sliders, the top and middle one are identical with the middle one adding a status overlay. The bottom slider is set for infinite progress support which is useful to indicate a "please wait" status.



To create such a theme I just customized the "Slider" and "SliderFull" styles with a border for the full state and the empty state (essentially the squares in the center are just the style of SliderFull painted on top of a style of Slider).



final Slider s1 = new Slider();

f.addComponent(s1);



final Slider s2 = new Slider();

s2.setRenderPercentageOnTop(true);

f.addComponent(s2);



Slider s3 = new Slider();

f.addComponent(s3);

s3.setInfinite(true);

new Thread() {

private boolean dir = true;

private int val = 0;

public void run() {

try {

sleep(1000);

while (Display.getInstance().getCurrent() == f) {

sleep(100);

s1.setProgress(val);

s2.setProgress(val);

if (dir) {

if (val > 100) {

dir = false;

}

val++;

} else {

if (val < 1) {

dir = true;

}

val--;

}

}

} catch (InterruptedException ex) {

ex.printStackTrace();

}

}

}.start();







Wednesday, July 21, 2010

The Highest Common Denominator Strategy

Normally I try to keep my blog posts on the technical or showcase side but for a change I want to share some general thoughts I have about the state of LWUIT and the recent enlightening poll on this blog and the javalobby.

While there is still a day to go with our poll as I write this, the results are quite interesting and not what I expected. Although in retrospect they make sense, after all this is a blog for an open source project so it makes sense that most of you would vote to open source FX. However, the blog poll otherwise is almost identical to the JavaLobby poll in the sense that 70% of the votes voted for either open sourcing FX or giving up on it altogether (visitors to the JavaLobby preferred giving up on FX while visitors to this blog preferred open sourcing).
This got me thinking a bit, is this the typical knee jerk reaction of "open sourcing will solve everything" or is there something specific in JavaFX that you saw and would want it to be open source?
Please be specific in the comments and if possible try to describe the experience you have with FX.

On to a completely different subject... I was talking to a friend in the states a while back about LWUIT's portability to various platforms. He claimed that portability is problematic since the native platform will come out with new features which the application will need to assimilate meaning a constant race against the native OS and constantly providing an inferior experience.

I attacked that notion with the "Highest Common Denominator" approach we are trying to instill using LWUIT. Most native OS's (even the shiny new ones) are ridiculously huge beasts with heavy dependencies that tend to stagnate immediately. Even a small change to the underlying GUI core will break at least 20% of the 3rd party applications and cause the applications shipping with the OS heavy maintainability costs...

Since LWUIT is statically linked changes to LWUIT don't break everything so we are bold in our changes where OS manufacturers are timid and fragmented...

This means that OS's don't activate big changes by default since these changes cause huge regressions. A great example is touch scrolling on Symbian phones that took quite a while to provide the proper kinetic/tensile drag feel you get from comparable devices or even Nokia's own N900.

However, LWUIT on Nokia devices provided kinetic and later on tensile drag on all Symbian touch phones including those that don't "officially" support this functionality. Essentially LWUIT provided features that are "superior" to the native platform thus escaping the original "lowest common denominator" issue of portable code and striking back with a higher baseline of functionality. This isn't limited to Symbian, I recently demonstrated "Android like" thumb scrolling in LWUIT which is in some ways superior to the native Android scrolling. It allows device rotation while scrolling, is fully customizable and still allows tensile lists...

A highest common denominator strategy is harder to maintain since it requires a developer keeps his vigil over new platform features to introduce them immediately. A developer must also work against the LWUIT trunk where the new features are introduced constantly to support such development. The benefit however is in the ability to carry these features to the platforms that are behind which puts the developer at a vantage point in those platforms.

We believe that LWUIT enables this strategy and we are working hard to make this strategy practically seamless to anyone using LWUIT, most of our work and improvements (e.g. the major style change in 1.2) is designed around this premise.

Friday, July 16, 2010

How Can Oracle Make JavaFX More Popular?

The guys at the JavaLobby asked "How Can Oracle Make JavaFX More Popular?", however the discussion seems to be hopelessly slanted towards the desktop crowd and a couple of Android related posts.
I'm quite curious to see what the thoughts of the crowd visiting this blog which mostly focuses on mobile and LWUIT. So I added a poll to the top of the blog, please vote your heart and try not to stuff the ballots ;-)

Thursday, July 15, 2010

Grow Your Container With This Fool Proof Method

Growing stuff is usually the subject filling my spambox... But into my inbox came a question from Terrence on how to grow a container.
Generally what he wanted was an effect similar to the one found on Google maps (that you can see in the video right here).
I tried to implement something like this in the most generic way possible which required a minor tweak to LWUIT (allowing us to restore the original preferred size of a component).

Generally the whole grow/shrink logic is contained in one method: "grow". Its relatively simple and just creates a motion between the current size of the component and the preferred size thus allowing the new component to "grow into place". The motion updates the preferred size and relayouts the form to create the smooth animation effect we see in the video.

I needed the ability to restore the default preferred size otherwise I would be stuck in the larger size after animating once. An alternative solution would have been to use a custom Container sublclass or to keep the original values stored neither one of which is my favorite...

Another "complexity" relates to the rather complex nature of preferred size, text area reports a relatively large preferred size before layout actually occurs. The reason is that it can't possibly know the target size of the container it will be placed in so it just guesses. So to support this situation I needed to layout the text area once in order to allow it to fit into place.

The code for this demo is in the incubator and pasted bellow:
public class GrowMidlet extends MIDlet {
private static final String LONG_TEXT_1 = "There is some text that should take more than one line to show so it will be cut off at some point when shown in a single line but when we show it as multiline it should work out and layout just fine and dandy... ";
private static final String LONG_TEXT_2 = "Growing text can be longer or shorter, it can be placed in the middle of the screen or anywhere really this is quite generic!";
private static final String LONG_TEXT_3 = "Using this is as simple as pie with any container type you can think of and any text length!";
public void startApp() {
try {
Display.init(this);
Resources r = Resources.open("/pimpTheme.res");
UIManager.getInstance().setThemeProps(r.getTheme(r.getThemeResourceNames()[0]));
Form f = new Form("Grow Demo");
f.addComponent(createGrowingContainer(LONG_TEXT_1, "TextAreaMe"));
f.addComponent(createGrowingContainer(LONG_TEXT_2, "TextAreaThem"));
f.addComponent(createGrowingContainer(LONG_TEXT_3, "TextAreaMe"));
f.show();
} catch (IOException ex) {
ex.printStackTrace();
}
}

private Container createGrowingContainer(String text, String uiid) {
final Container growingContainer = new Container(new BorderLayout());
final Label oneLine = new Label(text);
final Button grow = new Button("more...");
final TextArea multiLine = new TextArea(text);
final Button shrink = new Button("less...");
multiLine.setUIID("Label");
growingContainer.setUIID(uiid);
multiLine.setGrowByContent(true);
multiLine.setSingleLineTextArea(false);
growingContainer.addComponent(BorderLayout.CENTER, oneLine);
growingContainer.addComponent(BorderLayout.EAST, grow);
grow.addActionListener(new ActionListener() {

boolean firstTime = false;

public void actionPerformed(ActionEvent evt) {
growingContainer.removeAll();
growingContainer.addComponent(BorderLayout.CENTER, multiLine);
Container flow = new Container();
if (shrink.getParent() != null) {
shrink.getParent().removeAll();
}
flow.addComponent(shrink);
growingContainer.addComponent(BorderLayout.SOUTH, flow);
// the first time around the text area doesn't report the correct preferred size
// since it doesn't know its screen placement
if (firstTime) {
growingContainer.revalidate();
}
grow(growingContainer);
growingContainer.revalidate();
}
});
shrink.addActionListener(new ActionListener() {

public void actionPerformed(ActionEvent evt) {
growingContainer.removeAll();
growingContainer.addComponent(BorderLayout.CENTER, oneLine);
growingContainer.addComponent(BorderLayout.EAST, grow);
grow(growingContainer);
growingContainer.revalidate();
}
});
return growingContainer;
}

private void grow(final Component c) {
final Motion wMotion = Motion.createSplineMotion(c.getWidth(), c.getPreferredW(), 300);
final Motion hMotion = Motion.createSplineMotion(c.getHeight(), c.getPreferredH(), 300);
wMotion.start();
hMotion.start();
c.setPreferredSize(new Dimension(c.getWidth(), c.getHeight()));
c.getComponentForm().registerAnimated(new Animation() {
public boolean animate() {
if(wMotion.isFinished() && hMotion.isFinished()) {
c.getComponentForm().deregisterAnimated(this);
c.setPreferredSize(null);
c.getComponentForm().revalidate();
return false;
}
c.setPreferredSize(new Dimension(wMotion.getValue(), hMotion.getValue()));
c.getComponentForm().revalidate();
return false;
}

public void paint(Graphics g) {
}
});
}

public void pauseApp() {
}

public void destroyApp(boolean unconditional) {
}
}

Wednesday, July 7, 2010

Swipe That Image

The touch device Swipe gesture has been well supported on LWUIT from day one however strictly for the purpose of scrolling. Its pretty easy and common to implement the swipe for other uses such as paging through pictures but there users often want immediate feedback which is a bit harder.

Novices to LWUIT might think it makes sense to try and combine the swipe and transition, but that is probably not so practical since the transitions assume speed and structure (partially because they are generic and can slide between anything)...



This demo shows off Yoga posture pictures taken from beyoga.co.il (copyrighted!) in such a slide demo on the HD2. The code works well on pretty much any device with or without touch and is actually really simple to implement. I will commit it to my incubator directory soon:



public class BrowserMIDlet extends MIDlet implements Runnable {

private static final String[] IMAGE_NAMES = {

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/03/image/19.JPG",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/08/image/setu_bandhasana.jpg",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/02/image/Kukkutasana.jpg",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/016/image/parighasana.jpg",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/06/image/Kapotasana.jpg",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/014/image/24.jpg",

"http://beyoga.co.il/beyoga/gallery/asana/mainColumnParagraphs/0/subPar/07/image/18.jpg"

};



private Image[] slides;

private Form images;





public void startApp() {

try {

Display.init(this);

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

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

images = new Form("Be Yoga");

images.setLayout(new BorderLayout());

Image progress = Image.createImage("/progress0.png");

AnimationObject[] animations = new AnimationObject[] { AnimationObject.createAnimationImage(progress, 0, 0) };

animations[0].defineOrientation(AnimationObject.MOTION_TYPE_LINEAR, 0, 1500, 0, 359);

Timeline t = Timeline.createTimeline(1500, animations, new Dimension(progress.getWidth(), progress.getHeight()));

images.addComponent(BorderLayout.NORTH, new Label("Loading Images Please Wait..."));

images.addComponent(BorderLayout.CENTER, new Label(t));

images.show();

new Thread() {

public void run() {

// download the images from the web site

slides = new Image[IMAGE_NAMES.length];

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

InputStream i = null;

try {

i = Connector.openInputStream(IMAGE_NAMES[iter]);

slides[iter] = EncodedImage.createImage(i);

i.close();

} catch (IOException ex) {

ex.printStackTrace();

Dialog.show("Error", ex.getMessage(), "Exit", null);

BrowserMIDlet.this.notifyDestroyed();

} finally {

try {

i.close();

} catch (IOException ex) {

ex.printStackTrace();

}

}

}

Display.getInstance().callSerially(BrowserMIDlet.this);

}

}.start();

} catch (IOException ex) {

ex.printStackTrace();

}

}



public void run() {

images.removeAll();

images.addComponent(BorderLayout.CENTER, new Label() {

private int pos = 0;

private int dest = 1;

private int initialX = 0;

private int currentX = 0;

private Image currentImage = null;

private Image destImage = null;

private int currentImageOffset = 0;

private int destImageOffset = -1;

private Motion slideToDestMotion;



{

setFocusable(true);

requestFocus();

}



public boolean animate() {

boolean b = super.animate();

if(slideToDestMotion != null) {

currentX = slideToDestMotion.getValue();

if(slideToDestMotion.isFinished()) {

slideToDestMotion = null;

int dist = currentX - initialX;

if(dist != 0) {

if(Math.abs(dist) > getWidth() / 2) {

if(dist > 0) {

pos = pos - 1;

if(pos < 0) {

pos = slides.length - 1;

}

} else {

pos = pos + 1;

if(pos == slides.length) {

pos = 0;

}

}

dest = pos;

}

}

currentX = 0;

initialX = 0;

getComponentForm().deregisterAnimated(this);

}

return true;

}

return b;

}



private Image getCurrentImage() {

if(pos == currentImageOffset && currentImage != null && currentImage.getWidth() == getWidth()) {

return currentImage;

}

currentImage = slides[pos].scaledSmallerRatio(getWidth(), getHeight());

currentImageOffset = pos;

return currentImage;

}



private Image getDestImage() {

if(dest == destImageOffset && destImage != null && destImage.getWidth() == getWidth()) {

return destImage;

}

destImage = slides[dest].scaledSmallerRatio(getWidth(), getHeight());

destImageOffset = dest;

return destImage;

}



public void keyReleased(int c) {

int g = Display.getInstance().getGameAction(c);

if(slideToDestMotion == null) {

switch(g) {

case Display.GAME_LEFT:

dest = pos - 1;

if(dest < 0) {

dest = slides.length - 1;

}

slideToDestMotion = Motion.createSplineMotion(0, getWidth() + initialX, 200);

slideToDestMotion.start();

getComponentForm().registerAnimated(this);

break;

case Display.GAME_RIGHT:

dest = pos + 1;

if(dest == slides.length) {

dest = 0;

}

slideToDestMotion = Motion.createSplineMotion(0, -getWidth(), 200);

slideToDestMotion.start();

getComponentForm().registerAnimated(this);

break;

}

}

super.keyReleased(c);

}



public void pointerPressed(int x, int y) {

initialX = x;

currentX = x;

}



public void pointerDragged(int x, int y) {

currentX = x;

int dist = currentX - initialX;

if(dist != 0) {

if(dist > 0) {

dest = pos - 1;

if(dest < 0) {

dest = slides.length - 1;

}

} else {

dest = pos + 1;

if(dest == slides.length) {

dest = 0;

}

}

}

repaint();

}



public void pointerReleased(int x, int y) {

int dist = currentX - initialX;

if(dist != 0) {

if(Math.abs(dist) > getWidth() / 2) {

if(dist > 0) {

slideToDestMotion = Motion.createSplineMotion(currentX, getWidth() + initialX, 250);

} else {

slideToDestMotion = Motion.createSplineMotion(currentX, initialX - getWidth(), 250);

}

} else {

slideToDestMotion = Motion.createSplineMotion(currentX, initialX, 250);

}

slideToDestMotion.start();

getComponentForm().registerAnimated(this);

}

repaint();

}



public void paint(Graphics g) {

Image currentImg = getCurrentImage();

int currentImgX = getX() + (getWidth() - currentImg.getWidth()) / 2;

int currentImgY = getY() + (getHeight() - currentImg.getHeight()) / 2;

if(currentX == initialX) {

g.drawImage(currentImg, currentImgX, currentImgY);

} else {

Image destImg = getDestImage();

int dist = currentX - initialX;

g.drawImage(currentImg, currentImgX + dist, currentImgY);

int destImgX = getX() + (getWidth() - destImg.getWidth()) / 2;

int destImgY = getY() + (getHeight() - destImg.getHeight()) / 2;

if(dist > 0) {

g.drawImage(destImg, destImgX - getWidth() + dist, destImgY);

} else {

g.drawImage(destImg, destImgX + getWidth() + dist, destImgY);

}

}

}

});

images.revalidate();

}



public void pauseApp() {

}



public void destroyApp(boolean unconditional) {

}

}