CompatibilityList.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;

/**
 * List view custom UI control with gesture controls.
 */
final class CompatibleList
    extends List
    implements ViewMaster.PointerEventListener {

    private Thread delayThread;
    private boolean enabled = false;
    private final ViewMaster view;
    private int oldY;
    private long oldTime;
    private int dragThreshold = 10;
    private boolean dragging = false;
    private Animator animator = null;
    private int friction = 100;
    private int maxScrollSpeed = 100;
    private int minScrollSpeed = 10;
    private int velocityY = 0;

    CompatibleList() {
        view = ViewMaster.getInstance();
    }

    public final void disable() {
        if (!enabled) {
            return;
        }
        enabled = false;
        view.setPointerEventListener(null);
        super.disable();
    }

    public final void enable() {
        if (enabled) {
            return;
        }
        enabled = true;
        super.enable();
        view.setPointerEventListener(this);
    }

    public final void resize(int x, int y, int width, int height) {
        super.resize(x, y, width, height);
        dragThreshold = height / 20;
        maxScrollSpeed = height;
        minScrollSpeed = height / 3;
        friction = height / 2;
    }

    private void animate(int deltaY, int velocityY, boolean lastFrame) {
        addTranslateY(deltaY);
        this.velocityY = velocityY;
        // Scrolling is no longer active if this is the last frame
        if (lastFrame) {
            stopScrollAnimation();
        }
        // Request repaint from ViewMaster
        view.forceDraw();
    }

    /**
     * Stops scrolling.
     */
    protected final void stopScrollAnimation() {
        velocityY = 0;
        if (animator != null) {
            animator.close();
            animator = null;
        }
    }

    public final void pointerDragged(int x, int y) {
        final long currentTime = System.currentTimeMillis();
        final int dy = y - oldY;
        final int dt = (int) (currentTime - oldTime);
        if (Math.abs(dy) > dragThreshold) {
            dragging = true;
        }
        if (dragging) {
            if (dt > 0) {
                velocityY = (velocityY + dy * 1000 / dt) / 2;
            }
            oldY = y;
            oldTime = currentTime;
        }
        if (delayThread == null && dragging) {
            if (animator != null) {
                stopScrollAnimation();
            }
            else {
                addTranslateY(dy);
                view.forceDraw();
            }
        }
    }

    public final void pointerPressed(int x, int y) {
        oldY = y;
        oldTime = System.currentTimeMillis();
    }

    public final void pointerReleased(int x, int y) {
        if (delayThread == null) {
            if (dragging) {
                if (animator != null) {
                    animator.close();
                    animator = null;
                }
                final int v = Math.min(Math.max(-maxScrollSpeed, velocityY),
                    maxScrollSpeed);
                if (Math.abs(v) > minScrollSpeed) {
                    animator = new Animator(v, friction);
                    animator.start();
                }
            }
            else {
                if (animator != null) {
                    stopScrollAnimation();
                }
                else {
                    handleSelectEvent(x, y);
                }
            }
        }
        dragging = false;
    }

    /**
     * Common handler function for selection events.
     * 
     * @param event Gesture event.
     */
    private void handleSelectEvent(int x, int y) {
        setFocusedRowIndex(getRowAt(x, y));
        if (isFocusedRowIndex()) {
            view.draw();
            // Delay the event handling, 
            // so that user gets a chance to see the focus.
            delayThread = new Thread() {

                public void run() {
                    try {
                        Thread.sleep(100);
                    }
                    catch (InterruptedException e) {
                        return;
                    }
                    select();
                    resetFocusedRowIndex();
                    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;
    }

    /**
     * Animation thread. 
     */
    private class Animator
        extends Thread {

        private static final int MAX_FPS = 30;
        private volatile boolean run = true;
        private int startSpeed;
        private int friction;

        /**
         * Constructor.
         * 
         * @param startSpeed
         * @param friction
         */
        public Animator(final int startSpeed, final int friction) {
            if (friction < 0) {
                throw new IllegalArgumentException(
                    "Friction cannot be negative.");
            }
            this.startSpeed = startSpeed;
            this.friction = startSpeed > 0 ? friction : -friction;
        }

        /**
         * @see Thread#run()
         */
        public void run() {
            final long startTime = System.currentTimeMillis();
            final int tMax = Math.abs(startSpeed * 1000 / friction);
            if (tMax == 0) {
                animate(0, 0, true);
                return;
            }
            final int sMax = startSpeed / 2 * tMax;
            final int dtMin = 1000 / MAX_FPS;

            int t = 0;
            int sUpdated = 0;

            int ds = 0;
            boolean lastFrame = false;
            long frameEndTime = startTime;
            try {
                while (run) {
                    Thread.sleep(Math.max(dtMin - (int) (System.
                        currentTimeMillis() - frameEndTime), 0));

                    t = (int) (System.currentTimeMillis() - startTime);

                    if (t < tMax) {
                        ds += startSpeed * t - friction * t * t / 2000
                            - sUpdated;
                    }
                    else {
                        ds += sMax - sUpdated;
                        lastFrame = true;
                        run = false;
                    }
                    if (run && (lastFrame || Math.abs(ds / 1000) > 0)) {
                        final int v = startSpeed - friction * t / 2000;
                        animate(ds / 1000, v, lastFrame);
                        sUpdated += ds;
                        ds = 0;
                    }

                    frameEndTime = System.currentTimeMillis();
                }
            }
            catch (Exception e) {
                animate(0, 0, true);
            }
        }

        ;
		
		/**
		 * Closes this thread.
		 */
		public void close() {
            run = false;
        }
    }
}