GestureList.java

/**
* Copyright (c) 2012-2013 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.views.ViewMaster;
import com.nokia.mid.ui.frameanimator.FrameAnimator;
import com.nokia.mid.ui.frameanimator.FrameAnimatorListener;
import com.nokia.mid.ui.gestures.GestureEvent;
import com.nokia.mid.ui.gestures.GestureInteractiveZone;
import com.nokia.mid.ui.gestures.GestureListener;
import com.nokia.mid.ui.gestures.GestureRegistrationManager;

/**
 * List view custom UI control with gesture controls.
 */
final class GestureList
    extends List
    implements GestureListener, FrameAnimatorListener {

    private final static int GESTURES = GestureInteractiveZone.GESTURE_DRAG
        | GestureInteractiveZone.GESTURE_TAP
        | GestureInteractiveZone.GESTURE_FLICK;
    private final FrameAnimator animator;
    private boolean scrollingActive;
    private int pendingGestureEvent;
    private Thread delayThread;
    private boolean enabled = false;
    private final GestureInteractiveZone zone;
    private final ViewMaster view;

    GestureList() {
        zone = new GestureInteractiveZone(GESTURES);
        animator = new FrameAnimator();
        view = ViewMaster.getInstance();
    }

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

    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);
    }

    public final void resize(int x, int y, int width, int height) {
        super.resize(x, y, width, height);
        zone.setRectangle(x, y, width, height);
    }

    /**
     * @see com.nokia.mid.ui.frameanimator.FrameAnimatorListener#animate(
     * com.nokia.mid.ui.frameanimator.FrameAnimator, int, int, short, short, 
     * short, boolean)
     */
    public final void animate(FrameAnimator animator, int x, int y, short delta,
        short deltaX, short deltaY, boolean lastFrame) {
        addTranslateY(deltaY);
        // Scrolling is no longer active if this is the last frame
        scrollingActive = !lastFrame;
        // Request repaint from ViewMaster
        view.forceDraw();
    }

    /**
     * Stops scrolling.
     */
    private void stopScrolling() {
        scrollingActive = false;
        animator.stop();
    }

    /**
     * @see com.nokia.mid.ui.gestures.GestureListener#gestureAction(
     * java.lang.Object, com.nokia.mid.ui.gestures.GestureInteractiveZone, 
     * com.nokia.mid.ui.gestures.GestureEvent)
     */
    public final void gestureAction(Object container,
        GestureInteractiveZone zone, GestureEvent event) {

        // Block gesture handling if already handling one,
        // this may happen due to delayed handling of events 
        // (delay is needed for showing focus on selected item)
        if (pendingGestureEvent > 0) {
            return;
        }

        resetFocusedRowIndex();
        switch (event.getType()) {
            case GestureInteractiveZone.GESTURE_DRAG: {
                if (scrollingActive) {
                    stopScrolling();
                }
                else {
                    addTranslateY(event.getDragDistanceY());
                    view.forceDraw();
                }
                break;
            }
            case GestureInteractiveZone.GESTURE_TAP: {
                if (scrollingActive) {
                    stopScrolling();
                }
                else {
                    handleSelectEvent(event);
                }
                break;
            }
            case GestureInteractiveZone.GESTURE_FLICK: {
                // Start vertical flick only if the gesture is 
                // more vertical than horizontal.
                float angle = Math.abs(event.getFlickDirection());
                if ((angle > (Math.PI / 4) && angle < (3 * Math.PI / 4))) {
                    int startSpeed = event.getFlickSpeedY();
                    int direction = FrameAnimator.FRAME_ANIMATOR_VERTICAL;
                    int friction = FrameAnimator.FRAME_ANIMATOR_FRICTION_LOW;
                    scrollingActive = true;
                    animator.kineticScroll(startSpeed, direction, friction, 0);
                }
                break;
            }
            default:
                break;
        }
    }

    /**
     * Common handler function for selection events.
     * 
     * @param event Gesture event.
     */
    private void handleSelectEvent(GestureEvent event) {
        setFocusedRowIndex(getRowAt(event.getStartX(), event.getStartY()));
        if (isFocusedRowIndex()) {
            view.draw();
            // Delay the event handling, 
            // so that user gets a chance to see the focus.
            // Gesture is stored so that we can handle it later on.
            pendingGestureEvent = event.getType();
            delayThread = new Thread() {

                public void run() {
                    try {
                        Thread.sleep(100);
                    }
                    catch (InterruptedException e) {
                        return;
                    }
                    if (pendingGestureEvent > 0) {
                        switch (pendingGestureEvent) {
                            case GestureInteractiveZone.GESTURE_TAP:
                                select();
                                break;
                            default:
                                break;
                        }
                        resetFocusedRowIndex();
                        pendingGestureEvent = 0;
                        delayThread = null;
                    }
                }
            };
            delayThread.start();
        }
    }

    /**
     * Finds the row with the given coordinates.
     * 
     * @param x X-coordinate.
     * @param y Y-coordinate.
     * @return Row at the given coordinates or -1 if no row found.
     */
    private int getRowAt(int x, int y) {
        int listY = y - getTranslateY();
        int heightSoFar = 0;
        int heightNext = 0;

        // Go through all items (pretend that list item heights can vary).
        if (data != null) {
            for (int i = 0; i < data.size(); i++) {
                heightSoFar = heightNext;
                heightNext += itemHeight();

                if (listY >= heightSoFar && listY <= heightNext) {
                    return i;
                }
            }
        }

        return -1;
    }

    protected final void stopScrollAnimation() {
        if (scrollingActive) {
            stopScrolling();
        }
    }
}