List.java

/*
 * Copyright © 2012 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */
package com.nokia.example.attractions.views.list;

import com.nokia.example.attractions.Main;
import com.nokia.example.attractions.views.ViewMaster;
import com.nokia.example.attractions.Visual;
import com.nokia.example.attractions.utils.UIUtils;
import java.util.Vector;
import javax.microedition.lcdui.Graphics;

/**
 * List view custom UI control.
 * Provides a cursor for focus and a scrollbar for visualization.
 * Uses a ListDrawer based class for drawing each line of the list.
 */
public class List {

    private final static int SCROLLBAR_WIDTH = 4;
    private int focusedRowIndex = -1;
    private Listener listener;
    private Drawer drawer;
    private int x;  // top left x coordinage
    private int y;  // top left y coordinate
    private int width;  // width for the list
    private int height;  // height for the list
    Vector data = null;
    private int translateY = 0;  // y-coordinate of the top of visible area
    private volatile boolean refreshListHeight = false;
    private int listHeight = 0;
    private int bottomPadding = 0;
    private final ViewMaster view;

    protected List() {
        view = ViewMaster.getInstance();
    }

    /**
     * Creates a list object. If Gesture API is supported returns a list
     * which uses gestures.
     *
     * @param view ViewMaster object
     * @param drawer Drawer for the list items
     * @param listener Listener which is notified when an item is selected
     * @return the list object
     */
    public static List getList(Drawer drawer,
        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;
    }

    /**
     * Stops all animations the list might be showing
     */
    public void disable() {
        stopScrollAnimation();
    }

    /**
     * Enables list to show animations
     */
    public void enable() {
    }

    /**
     * This methods is called when the position of the list on the screen
     * changes.
     *
     * @param x
     * @param y
     * @param width
     * @param height
     */
    public void resize(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    /**
     * @return width of the area for drawing content
     */
    public final int getContentWidth() {
        return width - SCROLLBAR_WIDTH;
    }

    /**
     * Sets the data to be displayed.
     * @param data
     */
    public final void setData(Vector data) {
        if (this.data != null && data != null && this.data == data
            && this.data.size() == data.size()) {
            return;
        }

        reset();
        this.data = data;
        contentHeightChanged();
    }

    public final void reset() {
        translateY = 0;
        resetFocusedRowIndex();
    }

    /**
     * This method is called when the height of the content changes and
     * the layout of the list should be updated.
     */
    public final void contentHeightChanged() {
        refreshListHeight = true;
    }

    /**
     * Returns the data for accessing.
     * @return
     */
    public final Vector getData() {
        return data;
    }

    /**
     * @return true if data is set
     */
    public final boolean hasData() {
        return data != null;
    }

    /**
     * Select the currently focused row.
     */
    public final void select() {
        if (isFocusedRowIndex() && listener != null) {
            listener.select(focusedRowIndex);
        }
    }

    /**
     * Paints the list.
     * @param g
     */
    public final void draw(Graphics g) {

        if (drawer == null) {
            return;
        }

        int yOffset = translateY;

        if (refreshListHeight || isFocusedRowIndex()) {
            refreshListHeight = false;
            bottomPadding = UIUtils.bottomPadding();
            listHeight = bottomPadding;
            if (data != null) {
                int heightSoFar = 0;
                int heightNext = 0;
                for (int i = 0, size = data.size(); i < size; i++) {
                    final int itemHeight = drawer.itemHeight();
                    heightSoFar = heightNext;
                    heightNext += itemHeight;
                    if (i == focusedRowIndex && itemHeight < height) {
                        if (heightSoFar + yOffset < 0) {
                            // Focused item is above the view -> move view up.
                            yOffset = -heightSoFar;
                        }
                        else if (heightNext + yOffset > height) {
                            // Focused item is below the view -> move view down.
                            yOffset = height - heightNext;
                        }
                    }
                }
                listHeight += heightNext;
            }
        }

        if (yOffset > 0) {
            // Trying to scroll beyond list start.
            yOffset = 0;
            stopScrollAnimation();
        }
        else if ((yOffset + listHeight) < height) {
            if (listHeight < height) {
                yOffset = 0;
            }
            else {
                // Trying to scroll beyond list end.
                yOffset = -listHeight + height;
            }
            stopScrollAnimation();
        }

        translateY = yOffset;

        int prevClipX = g.getClipX();
        int prevClipY = g.getClipY();
        int prevClipWidth = g.getClipWidth();
        int prevClipHeight = g.getClipHeight();

        // Ensure that text does not overlap outside our allocated area.
        g.setClip(x, y, width, height);

        // Background
        ViewMaster.drawBackground(g, x, y, width, height, false);

        // Row drawing loop
        if (data != null) {
            int heightSoFar = 0;
            int heightNext = 0;
            for (int i = 0, size = data.size(); i < size; i++) {
                heightSoFar = heightNext;
                heightNext += drawer.itemHeight();

                if (heightNext + yOffset < 0) {
                    // Item is not visible -> skip drawing it.
                    continue;

                }
                else if (heightSoFar + yOffset > height) {
                    // Item would be drawn "under" the visible area
                    // -> stop drawing.
                    break;
                }
                drawer.drawItem(data, g, i,
                    this.x, this.y + heightSoFar + yOffset,
                    width - SCROLLBAR_WIDTH, height, i == focusedRowIndex);
            }
        }

        // Detemine the need for scrollbar, and the height for it.
        if (listHeight > height) {
            int scrollBarHeight = (height - bottomPadding) * height / listHeight
                + 1;
            int yPos = (height - bottomPadding) * (-yOffset) / listHeight;
            g.setColor(Visual.LIST_SCROLLBAR_COLOR);
            g.fillRect(width - SCROLLBAR_WIDTH, yPos + this.y,
                SCROLLBAR_WIDTH, scrollBarHeight);
        }

        g.setClip(prevClipX, prevClipY, prevClipWidth, prevClipHeight);
    }

    protected final void addTranslateY(int delta) {
        translateY += delta;
    }

    protected final int getTranslateY() {
        return translateY;
    }

    protected final int itemHeight() {
        return drawer.itemHeight();
    }

    protected final void resetFocusedRowIndex() {
        focusedRowIndex = -1;
        view.draw();
    }

    protected final void setFocusedRowIndex(int index) {
        focusedRowIndex = index;
    }

    protected final boolean isFocusedRowIndex() {
        return focusedRowIndex > -1;
    }

    /**
     * Move cursor up one row.
     */
    public final void focusUp() {
        focusedRowIndex--;
        if (data == null || data.isEmpty()) {
            resetFocusedRowIndex();
        }
        else if (focusedRowIndex < 0) {
            focusedRowIndex = 0;
        }
    }

    /**
     * Move cursor down one row.
     */
    public final void focusDown() {
        focusedRowIndex++;
        if (data == null || data.isEmpty()) {
            resetFocusedRowIndex();
        }
        else if (focusedRowIndex >= data.size()) {
            focusedRowIndex = data.size() - 1;
        }
    }

    /**
     * Scroll up one fourth of the view height
     */
    public final void scrollUp() {
        translateY += height / 4;
    }

    /**
     * Scroll down one fourth of the view height
     */
    public final void scrollDown() {
        translateY -= height / 4;
    }

    /**
     * Stops scrolling animation.
     */
    protected void stopScrollAnimation() {
    }

    /**
     * Interface for notifying that a row is selected.
     */
    public static interface Listener {

        /**
         * A row was selected.
         * @param focusedRowIndex
         */
        void select(int focusedRowIndex);
    }

    /**
     * An interface for rendering the rows in a list.
     */
    public static interface Drawer {

        /**
         * Returns the height of one list item (ie. row) in pixels.
         * @return
         */
        int itemHeight();

        /**
         * Draws one item.
         * @param data Data vector
         * @param g Graphics context to use for drawing
         * @param itemIndex Item to draw from the data vector
         * @param x Top left x coordinate of the drawing area
         * @param y Top left y coordinate of the drawing area
         * @param width Width of the drawing area
         * @param height Height of the drawing area
         * @param focused true if this item is currently focused
         * (ie. a highlight is required)
         */
        void drawItem(Vector data, Graphics g, int itemIndex, int x, int y,
            int width, int height, boolean focused);
    }
}