Implementing the UI views

ViewMaster

The ViewMaster class handles the MIDlet UI views. It extends the GameCanvas class and performs all drawing operations in a separate thread to keep the animations smooth. The class also handles the logic for switching between views.

The class implements the singleton pattern, and the getInstance method provides access to the singleton object.

    public static ViewMaster getInstance() {
        if (instance == null) {
            instance = new ViewMaster();
            instance.initialize();
        }
        return instance;
    }

After the singleton instance is created, the MIDlet initialises all the views using the initialize method.

    private void initialize() {
        this.setCommandListener(this);
        try {
            defaultThumbnailIcon = ImageLoader.getInstance().loadImage(
                "/thumbnail.png", null);
        }
        catch (IOException e) {
        }
        try {
            Image i = ImageLoader.getInstance().loadImage("/loader_content.png",
                null);
            defaultLoaderSprite =
                new Sprite(i, i.getWidth() / 24, i.getHeight());
        }
        catch (IOException e) {
        }
        attractionsView = new AttractionsView();
        detailsView = new DetailsView();
        mapView = new MapView();
        guidesView = new GuidesView();
        aboutView = new AboutView();
        helpView = new HelpView();
        try {
            Class.forName("com.nokia.mid.payment.IAPClientPaymentListener");
            Class c = Class.forName("com.nokia.example.attractions.views."
                + "BuyGuidesView");
            buyGuidesView = (BaseView) (c.newInstance());
        }
        catch (Exception e) {
            buyGuidesView = null;
        }
    }

The thread for drawing is started in the showNotify method.

    protected void showNotify() {
        ...
        draw();
        final Graphics g = getGraphics();
        new Thread() {
            public void run() {
                while(!hidden) {
                    if (refreshScreen && canPaint) {
                        refreshScreen = false;
                        if(activeView != null) {
                            activeView.draw(g);
                            flushGraphics();
                        }
                    }
                    drawLock.sleep(50);
                }
            }
        }.start();
    }

The platform calls the paint method when the GameCanvas is shown and ready to be painted on. When the paint method is called, the MIDlet sets the canPaint flag in the ViewMaster.

    public final void paint(Graphics g) {
        canPaint = true;
        forceDraw();
    }

The draw method sets a flag indicating that the screen must be refreshed.

    public final void draw() {
        refreshScreen = true;
    }

The forceDraw method sets a flag indicating that the screen must be refreshed and wakes up the drawing thread.

    public final void forceDraw() {
        draw();
        drawLock.wakeup();
    }

The setView method adds a view to the top of the view stack, activates the view, and configures the softkey buttons for the view.

    private void setView(final BaseView view) {
        Main.getInstance().callSerially(new Runnable() {

            public void run() {
                if (activeView != null) {
                    if (activeView == view) {
                        // Trying to activate a view that's already active.
                        return;
                    }
                    else {
                        activeView.deactivate();
                    }
                }

                activeView = view;
                synchronized (viewStack) {
                    // Remove the view from viewstack.
                    viewStack.removeElement(activeView);
                    // Push the view into viewstack.
                    viewStack.addElement(activeView);
                }
                activeView.activate();
                forceDraw();
            }
        });
    }

The setPreviousView method activates the previous view from the view stack.

    public final void setPreviousView() {
        BaseView view = null;
        synchronized (viewStack) {
            if (viewStack.size() >= 2) {
                // First remove and forget the current view.
                viewStack.removeElement(viewStack.lastElement());
                // Get the 2nd to last, which we want to activate.
                view = (BaseView) viewStack.lastElement();
            }
        }
        // Activate the view.
        if (view != null) {
            setView(view);
        }
    }

List

The List class is a custom UI component for the views. It provides a cursor for focus and a scrollbar for visualising the number of items in the list. Only those items which intersect the displayable area are drawn. Drawing the list items is done by an implementation of the List.Drawer interface. The selection event is returned using the Listener interface.

The static getList method creates a new list component. If the device supports FrameAnimator and Gesture APIs, a GestureList object is returned; if not, a List object is returned.

    public static List getList(ViewMaster view, Drawer drawer, Listener listener) {
        Listener listener) {
        List list = null;
        try {
            Class.forName("com.nokia.mid.ui.frameanimator.FrameAnimator");
            Class.forName("com.nokia.mid.ui.gestures."
                + "GestureRegistrationManager");
            Class c = Class.forName("com.nokia.example.attractions.views.list."
                + "GestureList");
            list = (List) c.newInstance();
        }
        catch (Exception e) {
            list = Main.isS60Phone() ? new S60List() : new List();
        }
        list.drawer = drawer;
        list.listener = listener;
        return list;
    }

GestureList

The GestureList class extends the custom List class with support for touch events and kinetic scrolling.

When the list is enabled, the class registers the frame animator and the zone for receiving gestures.

    public final void enable() {
        if (enabled) {
            return;
        }
        enabled = true;
        super.enable();
        // Use default values for maxFps an maxPps
        // (zero param means that default is used).
        final short maxFps = 0;
        final short maxPps = 0;
        if (animator.register((short) 0, (short) 0, maxFps, maxPps, this)
            != true) {
            throw new RuntimeException("FrameAnimator.register() failed!");
        }
        if (GestureRegistrationManager.register(view, zone) != true) {
            throw new RuntimeException(
                "GestureRegistrationManager.register() failed!");
        }
        GestureRegistrationManager.setListener(view, this);
    }

When the list is disabled, the class unregisters the gesture zone and the frame animator.

    public final void disable() {
        if (!enabled) {
            return;
        }
        enabled = false;
        GestureRegistrationManager.unregister(view, zone);
        super.disable();
        animator.unregister();
    }

BaseView

All UI views extend the BaseView class.

When a view is activated, the ViewMaster calls the activate method. In this method, views refresh the data that is drawn, activate any touch event listeners, and so on.

    public void activate() {
        active = true;
        logEvent("activated");
    }

When a view is hidden, the ViewMaster calls the deactivate method. In this method, views stop their animations, deactivate touch event listeners, and so on.

    public void deactivate() {
        viewMaster.removeCommand(exitCmd);
        viewMaster.removeCommand(backCmd);
        viewMaster.removeCommand(mapCmd);
        viewMaster.removeCommand(guidesCmd);
        viewMaster.removeCommand(buyGuidesCmd);
        viewMaster.removeCommand(aboutCmd);
        viewMaster.removeCommand(helpCmd);
        active = false;
    }

The draw method draws the view to the screen. Every subclass overrides this method with their own dedicated implementation.

    public void draw(final Graphics g) {
    }

DetailsView

The DetailsView shows the details of an attraction. The view can contain a lot of text, and drawing text can be a slow operation. To achieve smooth scrolling animations, the MIDlet draws the text to a buffer. The MIDlet updates the buffer only if the text changes or the screen dimensions change.

The drawBuffer method draws the details of an attraction to the buffer.

    protected final void drawBuffer(Graphics g) {
        int w = getContentWidth();
        int x0 = 0;
        int y0 = 0;

        g.setFont(Visual.SMALL_BOLD_FONT);
        g.setColor(Visual.LIST_SECONDARY_COLOR);
        g.drawString(attraction.getStreet(), x0, y0, Graphics.TOP
            | Graphics.LEFT);

        if (attraction.getDistance() != null) {
            g.drawString(attraction.getDistance(), w, y0, Graphics.TOP
                | Graphics.RIGHT);
        }

        y0 += Visual.SMALL_BOLD_FONT.getHeight() + 4;

        g.setFont(Visual.SMALL_FONT);
        g.setColor(Visual.LIST_PRIMARY_COLOR);

        for (int i = 0; i < lines.size(); i++) {
            g.drawString((String) lines.elementAt(i), x0, y0, Graphics.TOP
                | Graphics.LEFT);
            y0 += Visual.SMALL_FONT.getHeight();
        }
    }