Wednesday, November 25, 2009

Ni Hao LWUIT: Chinese Blog By Max Mu

Max Mu who you might recall as the developer who ported Sun's open source Phone ME VM to the PSP, has started a LWUIT blog in Chinese.
Chinese speakers should definitely check it out!

Tuesday, November 24, 2009

Building A Project On The BlackBerry

Update 2: The Netbeans link within the body died a while back. Updated instructions for 6.7 and newer are available here.

Updated: I forgot to add the main method and UiApplication section. It is now part 9 of the instructions.

While Thorsten & myself wrote build instructions for the BlackBerry in the forum/mailing list in the past they are somewhat outdated by now and aren't as easy to find using Google. So here is a step by step guide on porting to the BlackBerry devices with some explanations of the Caveats.

The standard LWUIT won't work on the Blackberry since it references API's such as 184/226, notice that LWUIT doesn't need or use these API's when they are not available! On the Blackberry device the very reference to these API's will cause the application to fail. Furthermore, as I explained in a previous post, Blackberry devices have some "issues" when using classes compiled without the RIM toolchain and LWUIT isn't compiled with the RIM toolchain.
Last but not least, the performance and user experience of the MIDP based LWUIT on Blackberry isn't very good...

The solution for all these issues is to use the RIM API's, which unfortunately means you need to create two separate versions of your application (3 if you need blackberry Storm support) .

I won't go into too many details on the BlackBerry devices, but a couple of years ago they introduced the Storm device which was their first touch screen device (featuring a click screen). Supporting the Storm requires importing classes of the API that aren't supported in older versions of RIM's API, hence Storm compatibility requires using a relatively new BlackBerry OS that most users don't have.
So you will need two builds and two blackberry Java Development Environments (JDE's) if you need to support Storm.

The instructions bellow are also geared towards the NetBeans IDE and Ant build.

LWUIT currently has two maintained BlackBerry ports, one created by Thorsten & another created by myself. Each has its advantages and you can easily go back and forth between them. Since I used NetBeans I will explain based on my port but you can easily replace the content of the src directory with Thorsten's port and it should be pretty seamless.

A standard LWUIT MIDlet should work fine on a RIM device using the ports, however RIM also offers support for a CLDC application. Using this approach tends to provide improved performance/security handling for RIM although its completely optional.

  1. Download the JDE versions, I recommend 4.2.x and 4.7.x (for Storm support).
  2. Follow the NetBeans BlackBerry knowledge base article to add the JDE versions as platforms to NetBeans (no need to follow the part about editing the build.xml).
  3. Fetch the LWUIT sources from SVN and open the BlackBerry project. Clean & Build the project (notice that you MUST clean & build, a plain build will often fail since the RIM port needs to replace some classes from LWUIT).
  4. If using storm as well, change the configuration of the Blackberry LWUIT port to "touch" and again clean & build.
  5. Download the BB ant tasks which are much easier to work with than the approach taken by the knowledge base article.
  6. Add "configurations" to the NetBeans project for BlackBerry and BlackBerryTouch. Set them to use the appropriate JDE Platforms.
  7. In the blackberry configurations add an "Ability" called RIM. This will allow you to use #ifdef RIM
    rather than #ifdef BlackBerryTouch && BlackBerry
    (yes, I can't stand #ifdef's but they are better than just copy and pasting the entire code and with RIM we don't have a choice).
  8. In the libraries for the project add both LWUIT & the appropriate blackberry project. Make sure to add the touch JAR only to the touch (Storm) configuration and the standard jar to the other BlackBerry configuration.
  9. If you are building a CLDC UiApplication add the following to your MIDlet class definition (instead of extends MIDlet):
    public MyMIDlet extends //#ifdef RIM
    net.rim.device.api.ui.UiApplication
    //#else
    //# javax.microedition.midlet.MIDlet
    //#endif
    {

    Then add this to the body of the MIDlet:
    //#ifdef Blackberry
    public static void main(String[] argv) {
    new MyMIDlet().startApp();
    }
    //#endif

    You might want to also add similar methods for notifyDestroyed/platformRequest:
    public void notifyDestroyed() {
    System.exit(0);
    }
    public void platformRequest(String s) {
    net.rim.blackberry.api.browser.Browser.getDefaultSession().displayPage(s);
    }

  10. Open the build.xml file of your NetBeans project and add the following, update the paths and names marked in bold (notice this is geared towards a CLDC UiApplication, you might need to modify this slightly for a MIDlet):
    <target name="post-init">
    <available file="${platform.home}/bin/rapc.exe" property="do.rapc">
    <available file="${platform.home}/simulator/9500.bat" property="bbtouch">
    <condition property="jpda.port" value="8000">
    <isset property="do.rapc">
    </isset>
    </condition>

    <target name="post-preprocess" depends="copyBBSources,copyBBTouchSources">
    </target>

    <typedef resource="bb-ant-defs.xml" classpath="bb-ant-tools.jar">
    <target name="bbbuild" description="blackberry build" depends="init,copyBBSources,copyBBTouchSources">
    <echo message="Compiling ${preprocessed.dir}"></echo>
    <mkdir dir="${dist.dir}/bbant">
    <rapc verbose="true" output="${name}" jdehome="${platform.home}" import="${platform.bootclasspath}" destdir="${dist.dir}/bbant/" noconvert="true">
    <src location="${basedir}/${preprocessed.dir}">
    <jdp title="Vendor" version="1" type="cldc" icon="${basedir}/src/icon.png" />
    </jdp>
    <!-- sigtool jdehome="C:\Program Files\Research In Motion\BlackBerry JDE 4.7.0" codfile="${dist.dir}/bbant/${name}.cod" password="" / -->
    <alx destdir="${dist.dir}/bbant" filename="${name}.alx">
    <application id="AppName">
    <codset>
    <fileset dir="${dist.dir}/bbant" includes="*.cod">
    </fileset>
    </codset>
    </application>

    <mkdir dir="${dist.dir}/bbant-final">
    <jadtool description="desc" id="appId" input="${dist.dir}/${dist.jad}" destdir="${dist.dir}/bbant-final">
    <fileset dir="${dist.dir}/bbant" includes="*.cod">
    </fileset>
    </jadtool>

    <target name="copyBBTouchSources" if="bbtouch">
    <echo message="Copying blackberry touch sources"></echo>
    <copydir forceoverwrite="true" src="$%7Bproject.BlackberryPort%7D%5Cbuild%5Ctouch%5Cpreprocessed" dest="${basedir}/${preprocessed.dir}">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/M3G.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/SVGImage.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/animations/Transition3D.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/impl/midp/SVGImplementation.java">
    </touch>

    <target name="copyBBSources" if="do.rapc">
    <echo message="Copying blackberry sources ${preprocessed.dir}"></echo>
    <copydir src="$%7Bproject.LWUIT%7D%5Csrc" dest="${basedir}/${preprocessed.dir}">
    <copydir forceoverwrite="true" src="$%7Bproject.BlackberryPort%7D%5Cbuild%5Cpreprocessed" dest="${basedir}/${preprocessed.dir}">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/M3G.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/SVGImage.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/animations/Transition3D.java">
    <touch file="${basedir}/${preprocessed.dir}/com/sun/lwuit/impl/midp/SVGImplementation.java">
    </touch>

    <target name="post-jar" if="do.rapc">
    <rapc verbose="true" output="${name}" jdehome="${platform.home}" import="${platform.bootclasspath}" destdir="${platform.home}/simulator/" noconvert="true">
    <src location="${basedir}/${preprocessed.dir}">
    <jdp title="Vendor" version="1" type="cldc" icon="${basedir}/src/icon.png" />
    </jdp>
    </src>

    <target name="post-clean">
    <delete failonerror="false">
    <fileset dir="${platform.home}/simulator">
    <include name="**/${name}.*">
    </include>
    </fileset>
    </delete>
    </target>


Monday, November 16, 2009

Rounded Fading Galore

The look of some modern phones is wasteful in screen real-estate but stunning in visual effect. The rounded UI and faded scrolling are are gorgeous effects on newer devices that make a huge difference with very little work.

These effect can be added to LWUIT easily without overhauling anything. I neglected to add padding to the softbutton area in the demo code but other than that the code is trivial. I used a glass pane for the rounded border effect, you can use hardcoded image files to provide proper anti-aliasing.
The fade effect required gradient image masks and replacing the look and feel which in itself wasn't too difficult either.

BTW I very much enjoyed reading Angelo's post on gradients, this is something that our UI designer has been riding on me to get for a very long time... I'm still trying to figure out a way to integrate this into the whole designer/style paradigm in an easy to use way...

/**
* Allows overriding the form to give it a "rounded border" look and feel
* with a fade out in the bottom in case of scrolling.
*
* @author Shai Almog
*/

public class RoundFadeGlassPane implements Painter {
private Image topLeft;
private Image topRight;
private Image bottomLeft;
private Image bottomRight;
private Form parentForm;

public RoundFadeGlassPane(Form f) {
parentForm = f;
Image blackCorner = Image.createImage(60, 60);

Graphics cornersGraphics = blackCorner.getGraphics();
cornersGraphics.setColor(0);
cornersGraphics.fillRect(0, 0, blackCorner.getWidth(), blackCorner.getHeight());
cornersGraphics.setColor(0xffffff);
cornersGraphics.fillRoundRect(0, 0, 200, 200, 60, 60);

// get the white color since it might be modified when rendering and might not actually be 0xffffff
// e.g. it might be 0xfefefe due to rendering issues
int white = blackCorner.getRGBCached()[blackCorner.getHeight() / 2 * blackCorner.getWidth()];

// remove the white color from the image so only the black corners remain
blackCorner = blackCorner.modifyAlpha((byte)0xff, white);

topLeft = blackCorner;
topRight = topLeft.rotate(90);
bottomRight = topLeft.rotate(180);
bottomLeft = topLeft.rotate(270);
PainterChain.installGlassPane(f, this);
}

public void paint(Graphics g, Rectangle rect) {
g.drawImage(topLeft, 0, 0);
g.drawImage(topRight, parentForm.getWidth() - topRight.getWidth(), 0);
g.drawImage(bottomLeft, 0, parentForm.getHeight() - bottomRight.getHeight());
g.drawImage(bottomRight, parentForm.getWidth() - topRight.getWidth(), parentForm.getHeight() - bottomRight.getHeight());
}
}

/**
* Simple look and feel automatically installing the "round look" for applications
*
* @author Shai Almog
*/

public class RoundFadeLookAndFeel extends DefaultLookAndFeel {
private Object topMask;
private Object bottomMask;
private Image topCache;
private Image bottomCache;

public void bind(Component c) {
if(c instanceof Form) {
new RoundFadeGlassPane((Form)c);
}
}

private Object createFadeMask(boolean direction) {
Image mask = Image.createImage(Display.getInstance().getDisplayWidth(), Display.getInstance().getDisplayHeight() / 10);
Graphics g = mask.getGraphics();
g.setColor(0xffffff);
g.fillRect(0, 0, mask.getWidth(), mask.getHeight());
if(direction) {
g.fillLinearGradient(0, 0xffffff, 0, -5,
mask.getWidth(), mask.getHeight(), false);
} else {
g.fillLinearGradient(0xffffff, 0, 0, 5,
mask.getWidth(), mask.getHeight(), false);
}
return mask.createMask();
}

private Object getTopMask() {
if(topCache == null || topCache.getWidth() !=
Display
.getInstance().getDisplayWidth()) {
topMask = createFadeMask(false);
topCache = Image.createImage(
Display.getInstance().getDisplayWidth(),
Display.getInstance().getDisplayHeight() / 10);
}
return topMask;
}

private Object getBottomMask() {
if(bottomCache == null || bottomCache.getWidth() !=
Display.getInstance().getDisplayWidth()) {
bottomMask = createFadeMask(true);
bottomCache = Image.createImage(
Display.getInstance().getDisplayWidth(),
Display.getInstance().getDisplayHeight() / 10);
}
return bottomMask;
}

public void drawVerticalScroll(Graphics g, Component c,
float offsetRatio, float blockSizeRatio) {
if(offsetRatio + blockSizeRatio < 0.995) {
// we need to draw the fade on the bottom
Object bottom = getBottomMask();
Graphics temp = bottomCache.getGraphics();
temp.translate(0, bottomCache.getHeight() - c.getHeight());
c.paintBackgrounds(temp);
g.drawImage(bottomCache.applyMask(bottom), c.getX(), c.getY() + c.getHeight() - bottomCache.getHeight());
}
if(offsetRatio > 0) {
// we need to draw the fade on the top
Object top = getTopMask();
Graphics temp = topCache.getGraphics();
temp.translate(0, topCache.getHeight() - c.getHeight());
c.paintBackgrounds(temp);
g.drawImage(topCache.applyMask(top), c.getX(), c.getY());
}
super.drawVerticalScroll(g, c, offsetRatio, blockSizeRatio);
}
}

Tuesday, November 3, 2009

Optimized For The Touch

The LWUIT developer guide generally recommends increasing the padding of components to make them "finger friendly" as a generic guide for LWUIT on touch devices. While this is true, its somewhat basic and we can do much more in LWUIT and outside of it to make our application easier for the touch...

Two of the newer features in LWUIT's SVN are touch menus and tactile feedback, both are off by default and should be explicitly activated. The reason for this is that we don't want to "enforce" our opinion on how touch should look/feel on an application.
You can query whether the device supports touch by invoking Display.getInstance().isTouchScreenDevice()

Notice that the isTouchScreenDevice() method doesn't work properly on some simulators... However, it seems to work reasonably well on the actual devices.

To enable touch menus just use:
UIManager.getInstance().getLookAndFeel().setTouchMenus(true);

You can customize the look of the buttons via the "TouchCommand" selector in the theme.

The tactile feedback causes the phone to vibrate when the user presses a component that is intractable (e.g. list/button). Since most touch screens aren't of very good quality this is very useful in giving the user a sense that the screen actually registered his interaction. To enable this you can use:
UIManager.getInstance().getLookAndFeel().setTactileTouchDuration(100);

The argument is the number of milliseconds to vibrate on touch 50 to 100 seem like reasonable numbers, 0 disables the feature.

Component now has isTactileTouch()/setTactileTouch(boolean) methods to toggle the tactile touch vibration per a specific component. By default LWUIT tries to initialize this based on focusability so for custom components it might be useful to manipulate this flag.

A somewhat older flag which we would recommend is fireOnClick() for Lists. By default lists require two clicks to activate an entry, the first selects the entry and the second opens it.
This allows "context command flow", e.g. a command such as "remove" that removes the current selected entry. However, on touch devices this sort of behavior is often inconvenient and you would expect the "remove" command to move you to a separate list of checkboxes to select the entries to remove.
You would normally expect a single tap on the entry to act like the fire key (send an action event immediately) and not like simple selection. Calling List.fireOnClick(true) makes the list behave like that which is more convenient for touch device, however you need to ensure your application flow can handle this.

There is also the virtual keyboard that Chen blogged about recently, this is immensely useful for touch and Chen has improved its performance considerably!

As a side note unrelated to LWUIT, many devices have a "compatibility" mode on by default where keys are placed by the device at the bottom of the touch screen to allow "none-touch" Java ME applications to run on the device. Applications are expected to explicitly declare their support for touch to utilize the full screen of the device. This is done using the following Jad flags:

Navi-Key-Hidden: true
Nokia-MIDlet-On-Screen-Keypad: no
MIDlet-Touch-Support: true

Notice that the last entry (MIDlet-Touch-Support) is required by current/older Samsung/LG devices but is illegal by the MIDP specification hence fails on Nokia etc. so for support on these devices you would need a copy of your JAD (only the jad) with this attribute added.

Update: LG  have added another JAD attribute required to hide the native vkb which you should probably add to all applications:
LGE-MIDlet-Display-Nav-Keypad: no