Implementing utility classes

ImageLoader

The ImageLoader class handles the loading of images. To save memory, loaded images are cached.

The loadImage methods load an image and store it to the cache, so that subsequent calls can retrieve the image from the cache and no duplicate images need to be created.

    public final Image loadImage(final String imagePath, final Hashtable cache)
        throws IOException {
        Image image = getImageFromCache(imagePath, cache);
        if (image == null) {
            InputStream in = this.getClass().getResourceAsStream(imagePath);
            if (in == null) {
                throw new IOException("Image not found.");
            }
            image = Image.createImage(in);
            cacheImage(imagePath, image, cache, false);
        }
        return image;
    }

The cacheImage method caches an image to the specified cache and to a static imageCache. The imageCache uses WeakReferences, so all available free memory is used for caching images even when the specified cache is cleaned. The getImageFromCache method retrieves an image from the specified cache.

    private void cacheImage(String key, Image image, Hashtable cache,
        boolean toWeakCache) {
        if (cache != null) {
            cache.put(key, image);
        }
        if (toWeakCache) {
            imageCache.put(key, new WeakReference(image));
        }
    }

    private Image getImageFromCache(String key, Hashtable cache) {
        Image image = null;
        if (cache != null) {
            image = (Image) cache.get(key);
        }
        if (image == null) {
            WeakReference imageRef = (WeakReference) imageCache.get(key);
            image = imageRef == null ? null : (Image) imageRef.get();
            if (image != null && cache != null) {
                cache.put(key, image);
            }
        }
        if (image == null) {
            imageCache.remove(key);
        }

        return image;
    }

The loadMapMarker method creates a map marker image. This method also uses a cache to prevent duplicates. Marker is created by drawing a label over a background image. By default images are immutable in Java ME. DirectUtils provides a method for creating mutable transparent images but it doesn't work correctly on all devices. The multiplyImages method takes two images and combines them by multiplying each pixel in the first image with a corresponding pixel in the second image. Transparency of the first image is preserved.

    public final Image loadMapMarker(final String id, final String imagePath,
        final Hashtable cache)
        throws IOException {
        Image background = loadImage(imagePath, cache);
        String url = "" + id + imagePath;
        Image image = null;
        if (cache != null) {
            image = (Image) cache.get(url);
        }
        if (image == null) {
            int w = background.getWidth();
            int h = background.getHeight();
            image = Image.createImage(w, h);
            Graphics g = image.getGraphics();
            g.setColor(0xffffff);
            g.fillRect(0, 0, w, h);
            g.setColor(Visual.MAP_MARKER_COLOR);
            g.setFont(Visual.MAP_MARKER_FONT);
            g.drawString(id, w / 2, h / 2 + g.getFont().getHeight() / 2 - (Main.
                isS60Phone() ? 3 : 5),
                Graphics.BOTTOM | Graphics.HCENTER);
            image = multiplyImages(background, image);
            if (cache != null) {
                cache.put(url, image);
            }
        }
        return image;
    }


    private static Image multiplyImages(Image img1, Image img2) {
        if (img1.getWidth() != img2.getWidth() || img1.getHeight()
            != img2.getHeight()) {
            throw new IllegalArgumentException(
                "Sizes of the images must be same");
        }
        int[] rawImg1 = new int[img1.getHeight() * img1.getWidth()];
        img1.getRGB(rawImg1, 0, img1.getWidth(), 0, 0,
            img1.getWidth(), img1.getHeight());
        int[] rawImg2 = new int[img2.getHeight() * img2.getWidth()];
        img2.getRGB(rawImg2, 0, img2.getWidth(), 0, 0, img2.getWidth(), img2.
            getHeight());

        int mrgb, mr, mg, mb, a, r, g, b;
        for (int i = 0, l = rawImg1.length; i < l; i++) {
            mrgb = rawImg2[i] & 0xffffff;
            if (mrgb < 0xffffff) {
                mr = mrgb >>> 16;
                mg = (mrgb & 0xff00) >>> 8;
                mb = mrgb & 0xff;
                a = rawImg1[i] & 0xff000000;
                r = (((rawImg1[i] & 0xff0000) * mr) / 0xff) & 0xff0000;
                g = (((rawImg1[i] & 0xff00) * mg) / 0xff) & 0xff00;
                b = (((rawImg1[i] & 0xff) * mb) / 0xff) & 0xff;
                rawImg1[i] = a | r | g | b;
            }
        }

        return Image.createRGBImage(rawImg1, img1.getWidth(),
            img1.getHeight(), true);
    }

TextWrapper

The TextWrapper class provides the wrapTextToWidth method, which splits a string to multiple lines.

    public static Vector wrapTextToWidth(String text, int wrapWidth,
        Font font) {
        if (wrapWidth < 20) {
            wrapWidth = 240;
        }

        Vector lines = new Vector();

        int start = 0;
        int position = 0;
        int length = text.length();
        while (position < length - 1) {
            start = position;
            int lastBreak = -1;
            int i = position;
            for (; i < length && font.stringWidth(text.substring(position, i))
                <= wrapWidth; i++) {
                if (text.charAt(i) == ' ') {
                    lastBreak = i;
                }
                else if (text.charAt(i) == '\n') {
                    lastBreak = i;
                    break;
                }
            }
            if (i == length) {
                position = i;
            }
            else if (lastBreak <= position) {
                position = i;
            }
            else {
                position = lastBreak;
            }

            lines.addElement(text.substring(start, position));

            if (position == lastBreak) {
                position++;
            }
        }

        return lines;
    }

UIUtils and FtUIUtils

The UIUtils class provides methods for customizing those parts of the UI that function differently on Series 40 full touch devices. The FtUIUtils class extends the UIUtils and provides the functionality specific to Series 40 full touch devices.

The UIUtils.getInstance method returns an FtUIUtils object if the MIDlet is run on a Series 40 full touch device.

    private static UIUtils getInstance() {
        if (instance == null) {
            try {
                Class.forName("com.nokia.mid.ui.IconCommand");
                Class.forName("com.nokia.mid.ui.CategoryBar");
                Class.forName("com.nokia.mid.ui.VirtualKeyboard");
                Class clazz = Class.forName("com.nokia.example.attractions."
                    + "utils.FtUIUtils");
                instance = (UIUtils) clazz.newInstance();
            }
            catch (Exception e) {
                instance = new UIUtils();
            }
        }
        return instance;
    }

The creation of new Command objects is implemented in the UIUtils class, so that IconCommand objects can be used only when the device supports them.

The UIUtils class contains a static method for creating Commands. The UIUtils.createCommand method creates a new Command using the newCommand method, which the FtUIUtils class overrides to return an IconCommand object instead.

    public static Command createCommand(int command) {
        return getInstance().newCommand(command);
    }

The UIUtils.newCommand method returns basic Command objects.

    protected Command newCommand(int command) {
        Command result;
        switch (command) {
            case EXIT:
                result = new Command("Exit", Command.EXIT, 1);
                break;
            case BACK:
                result = new Command("Back", Command.BACK, 1);
                break;
            case MAP:
                result = new Command("Map", Command.SCREEN, 1);
                break;
            case GUIDES:
                result = new Command("Guides", Command.SCREEN, 2);
                break;
            case BUY_GUIDES:
                result = new Command("Buy more", Command.SCREEN, 2);
                break;
            case ABOUT:
                result = new Command("About", Command.HELP, 3);
                break;
            case HELP:
                result = new Command("Help", Command.HELP, 3);
                break;
            case POLICY:
                result = new Command("Policy", Command.HELP, 3);
                break;
            case SETTINGS:
                result = new Command("Settings", Command.HELP, 3);
                break;
            case OPEN:
                result = new Command("Open", Command.OK, 1);
                break;
            case BUY:
                result = new Command("Buy", Command.OK, 1);
                break;
            case ACCEPT:
                result = new Command("Accept", Command.OK, 1);
                break;
            case CANCEL:
                result = new Command("Cancel", Command.CANCEL, 1);
                break;
            case CHANGE:
                result = new Command("Change", Command.OK, 1);
                break;
            case SAVE:
                result = new Command("Save", Command.OK, 2);
                break;
            default:
                result = null;
                break;
        }
        return result;
    }

By comparison, the FtUIUtils.newCommand method, which overrides the UIUtils.newCommand method, returns IconCommand objects when needed.

protected Command newCommand(int command) {
        Command result;
        switch (command) {
            case EXIT:
                result = new IconCommand("Exit", Command.EXIT, 1,
                    IconCommand.ICON_BACK);
                break;
            case BACK:
                result = new IconCommand("Back", Command.BACK, 1,
                    IconCommand.ICON_BACK);
                break;
            case MAP:
                result = newCommand("Map", "/icons/map.png",
                    Command.SCREEN, 1);
                break;
            case GUIDES:
                result = new Command("Guides", Command.SCREEN, 2);
                break;
            case BUY_GUIDES:
                result = newCommand("Buy more", "/icons/ovi_store.png",
                    Command.SCREEN, 2);
                break;
            case ABOUT:
                result = new Command("About", Command.HELP, 3);
                break;
            case HELP:
                result = new Command("Help", Command.HELP, 3);
                break;
            case POLICY:
                result = new Command("Policy", Command.HELP, 3);
                break;
            case SETTINGS:
                result = new IconCommand("Settings", Command.HELP, 3,
                    IconCommand.ICON_OPTIONS);
                break;
            case OPEN:
                result = new Command("Open", Command.OK, 1);
                break;
            case BUY:
                result = new Command("Buy", Command.OK, 1);
                break;
            case ACCEPT:
                result = newCommand("Accept", "/icons/ok.png",
                    Command.OK, 1);
                break;
            case CANCEL:
                result = newCommand("Cancel", "/icons/cancel.png",
                    Command.BACK, 1);
                break;
            case CHANGE:
                result = new Command("Change", Command.OK, 1);
                break;
            case SAVE:
                result = newCommand("Save", "/icons/save.png", Command.OK, 2);
                break;
            default:
                result = null;
                break;
        }
        return result;
    }

The UIUtils.init method is called to initialize the UI when the MIDlet is started.

    public static void init() {
        getInstance().initialize();
    }

The UIUtils.initialize method does nothing. The FtUIUtils.initialize method, which overrides the UIUtils.initialize method, hides the default open keypad command from the options menu on a Series 40 full touch device.

    protected void initialize() {
        super.initialize();
        VirtualKeyboard.hideOpenKeypadCommand(true);
    }

On a Series 40 full touch device, the back button is an overlay. This means that some padding needs to be added to scrollable views so that the back button does not hide any content underneath it. The UIUtils.bottomPadding method calls the getBottomPadding method of the UIUtils or FtUIUtils instance to return how much padding is needed.

    public static int bottomPadding() {
        return getInstance().getBottomPadding();
    }

The UIUtils.getBottomPadding method returns 0 as padding is only needed on Series 40 full touch devices. The FtUIUtils.getBottomPadding method, which overrides the UIUtils.getBottomPadding method, returns the height of the back button on a Series 40 full touch device.

    protected int getBottomPadding() {
        return CategoryBar.getBestImageHeight(CategoryBar.IMAGE_TYPE_BACKGROUND)
            - 4;
    }