com.badlogic.gdx.input.GestureDetector.java Source code

Java tutorial

Introduction

Here is the source code for com.badlogic.gdx.input.GestureDetector.java

Source

/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.input;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.utils.TimeUtils;
import com.badlogic.gdx.utils.Timer;
import com.badlogic.gdx.utils.Timer.Task;

/** {@link InputProcessor} implementation that detects gestures (tap, long press, fling, pan, zoom, pinch) and hands them to a
 * {@link GestureListener}.
 * @author mzechner */
public class GestureDetector extends InputAdapter {
    final GestureListener listener;
    private float tapSquareSize;
    private long tapCountInterval;
    private float longPressSeconds;
    private long maxFlingDelay;

    private boolean inTapSquare;
    private int tapCount;
    private long lastTapTime;
    private float lastTapX, lastTapY;
    private int lastTapButton, lastTapPointer;
    boolean longPressFired;
    private boolean pinching;
    private boolean panning;

    private final VelocityTracker tracker = new VelocityTracker();
    private float tapSquareCenterX, tapSquareCenterY;
    private long gestureStartTime;
    Vector2 pointer1 = new Vector2();
    private final Vector2 pointer2 = new Vector2();
    private final Vector2 initialPointer1 = new Vector2();
    private final Vector2 initialPointer2 = new Vector2();

    private final Task longPressTask = new Task() {
        @Override
        public void run() {
            if (!longPressFired)
                longPressFired = listener.longPress(pointer1.x, pointer1.y);
        }
    };

    /** Creates a new GestureDetector with default values: halfTapSquareSize=20, tapCountInterval=0.4f, longPressDuration=1.1f,
     * maxFlingDelay=0.15f. */
    public GestureDetector(GestureListener listener) {
        this(20, 0.4f, 1.1f, 0.15f, listener);
    }

    /** @param halfTapSquareSize half width in pixels of the square around an initial touch event, see
     *           {@link GestureListener#tap(float, float, int, int)}.
     * @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps.
     * @param longPressDuration time in seconds that must pass for the detector to fire a
     *           {@link GestureListener#longPress(float, float)} event.
     * @param maxFlingDelay time in seconds the finger must have been dragged for a fling event to be fired, see
     *           {@link GestureListener#fling(float, float, int)}
     * @param listener May be null if the listener will be set later. */
    public GestureDetector(float halfTapSquareSize, float tapCountInterval, float longPressDuration,
            float maxFlingDelay, GestureListener listener) {
        this.tapSquareSize = halfTapSquareSize;
        this.tapCountInterval = (long) (tapCountInterval * 1000000000l);
        this.longPressSeconds = longPressDuration;
        this.maxFlingDelay = (long) (maxFlingDelay * 1000000000l);
        this.listener = listener;
    }

    @Override
    public boolean touchDown(int x, int y, int pointer, int button) {
        return touchDown((float) x, (float) y, pointer, button);
    }

    public boolean touchDown(float x, float y, int pointer, int button) {
        if (pointer > 1)
            return false;

        if (pointer == 0) {
            pointer1.set(x, y);
            gestureStartTime = Gdx.input.getCurrentEventTime();
            tracker.start(x, y, gestureStartTime);
            if (Gdx.input.isTouched(1)) {
                // Start pinch.
                inTapSquare = false;
                pinching = true;
                initialPointer1.set(pointer1);
                initialPointer2.set(pointer2);
                longPressTask.cancel();
            } else {
                // Normal touch down.
                inTapSquare = true;
                pinching = false;
                longPressFired = false;
                tapSquareCenterX = x;
                tapSquareCenterY = y;
                if (!longPressTask.isScheduled())
                    Timer.schedule(longPressTask, longPressSeconds);
            }
        } else {
            // Start pinch.
            pointer2.set(x, y);
            inTapSquare = false;
            pinching = true;
            initialPointer1.set(pointer1);
            initialPointer2.set(pointer2);
            longPressTask.cancel();
        }
        return listener.touchDown(x, y, pointer, button);
    }

    @Override
    public boolean touchDragged(int x, int y, int pointer) {
        return touchDragged((float) x, (float) y, pointer);
    }

    public boolean touchDragged(float x, float y, int pointer) {
        if (pointer > 1)
            return false;
        if (longPressFired)
            return false;

        if (pointer == 0)
            pointer1.set(x, y);
        else
            pointer2.set(x, y);

        // handle pinch zoom
        if (pinching) {
            if (listener != null) {
                boolean result = listener.pinch(initialPointer1, initialPointer2, pointer1, pointer2);
                return listener.zoom(initialPointer1.dst(initialPointer2), pointer1.dst(pointer2)) || result;
            }
            return false;
        }

        // update tracker
        tracker.update(x, y, Gdx.input.getCurrentEventTime());

        // check if we are still tapping.
        if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) {
            longPressTask.cancel();
            inTapSquare = false;
        }

        // if we have left the tap square, we are panning
        if (!inTapSquare) {
            panning = true;
            return listener.pan(x, y, tracker.deltaX, tracker.deltaY);
        }

        return false;
    }

    @Override
    public boolean touchUp(int x, int y, int pointer, int button) {
        return touchUp((float) x, (float) y, pointer, button);
    }

    public boolean touchUp(float x, float y, int pointer, int button) {
        if (pointer > 1)
            return false;

        // check if we are still tapping.
        if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY))
            inTapSquare = false;

        boolean wasPanning = panning;
        panning = false;

        longPressTask.cancel();
        if (longPressFired)
            return false;

        if (inTapSquare) {
            // handle taps
            if (lastTapButton != button || lastTapPointer != pointer
                    || TimeUtils.nanoTime() - lastTapTime > tapCountInterval
                    || !isWithinTapSquare(x, y, lastTapX, lastTapY))
                tapCount = 0;
            tapCount++;
            lastTapTime = TimeUtils.nanoTime();
            lastTapX = x;
            lastTapY = y;
            lastTapButton = button;
            lastTapPointer = pointer;
            gestureStartTime = 0;
            return listener.tap(x, y, tapCount, button);
        }

        if (pinching) {
            // handle pinch end
            pinching = false;
            panning = true;
            // we are in pan mode again, reset velocity tracker
            if (pointer == 0) {
                // first pointer has lifted off, set up panning to use the second pointer...
                tracker.start(pointer2.x, pointer2.y, Gdx.input.getCurrentEventTime());
            } else {
                // second pointer has lifted off, set up panning to use the first pointer...
                tracker.start(pointer1.x, pointer1.y, Gdx.input.getCurrentEventTime());
            }
            return false;
        }

        // handle no longer panning
        boolean handled = false;
        if (wasPanning && !panning)
            handled = listener.panStop(x, y, pointer, button);

        // handle fling
        gestureStartTime = 0;
        long time = Gdx.input.getCurrentEventTime();
        if (time - tracker.lastTime < maxFlingDelay) {
            tracker.update(x, y, time);
            handled = listener.fling(tracker.getVelocityX(), tracker.getVelocityY(), button) || handled;
        }
        return handled;
    }

    /** No further gesture events will be triggered for the current touch, if any. */
    public void cancel() {
        longPressTask.cancel();
        longPressFired = true;
    }

    /** @return whether the user touched the screen long enough to trigger a long press event. */
    public boolean isLongPressed() {
        return isLongPressed(longPressSeconds);
    }

    /** @param duration
     * @return whether the user touched the screen for as much or more than the given duration. */
    public boolean isLongPressed(float duration) {
        if (gestureStartTime == 0)
            return false;
        return TimeUtils.nanoTime() - gestureStartTime > (long) (duration * 1000000000l);
    }

    public boolean isPanning() {
        return panning;
    }

    public void reset() {
        gestureStartTime = 0;
        panning = false;
        inTapSquare = false;
    }

    private boolean isWithinTapSquare(float x, float y, float centerX, float centerY) {
        return Math.abs(x - centerX) < tapSquareSize && Math.abs(y - centerY) < tapSquareSize;
    }

    /** The tap square will not longer be used for the current touch. */
    public void invalidateTapSquare() {
        inTapSquare = false;
    }

    public void setTapSquareSize(float halfTapSquareSize) {
        this.tapSquareSize = halfTapSquareSize;
    }

    /** @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps. */
    public void setTapCountInterval(float tapCountInterval) {
        this.tapCountInterval = (long) (tapCountInterval * 1000000000l);
    }

    public void setLongPressSeconds(float longPressSeconds) {
        this.longPressSeconds = longPressSeconds;
    }

    public void setMaxFlingDelay(long maxFlingDelay) {
        this.maxFlingDelay = maxFlingDelay;
    }

    /** Register an instance of this class with a {@link GestureDetector} to receive gestures such as taps, long presses, flings,
     * panning or pinch zooming. Each method returns a boolean indicating if the event should be handed to the next listener (false
     * to hand it to the next listener, true otherwise).
     * @author mzechner */
    public static interface GestureListener {
        /** @see InputProcessor#touchDown(int, int, int, int) */
        public boolean touchDown(float x, float y, int pointer, int button);

        /** Called when a tap occured. A tap happens if a touch went down on the screen and was lifted again without moving outside
         * of the tap square. The tap square is a rectangular area around the initial touch position as specified on construction
         * time of the {@link GestureDetector}.
         * @param count the number of taps. */
        public boolean tap(float x, float y, int count, int button);

        public boolean longPress(float x, float y);

        /** Called when the user dragged a finger over the screen and lifted it. Reports the last known velocity of the finger in
         * pixels per second.
         * @param velocityX velocity on x in seconds
         * @param velocityY velocity on y in seconds */
        public boolean fling(float velocityX, float velocityY, int button);

        /** Called when the user drags a finger over the screen.
         * @param deltaX the difference in pixels to the last drag event on x.
         * @param deltaY the difference in pixels to the last drag event on y. */
        public boolean pan(float x, float y, float deltaX, float deltaY);

        /** Called when no longer panning. */
        public boolean panStop(float x, float y, int pointer, int button);

        /** Called when the user performs a pinch zoom gesture. The original distance is the distance in pixels when the gesture
         * started.
         * @param initialDistance distance between fingers when the gesture started.
         * @param distance current distance between fingers. */
        public boolean zoom(float initialDistance, float distance);

        /** Called when a user performs a pinch zoom gesture. Reports the initial positions of the two involved fingers and their
         * current positions.
         * @param initialPointer1
         * @param initialPointer2
         * @param pointer1
         * @param pointer2 */
        public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2);
    }

    /** Derrive from this if you only want to implement a subset of {@link GestureListener}.
     * @author mzechner */
    public static class GestureAdapter implements GestureListener {
        @Override
        public boolean touchDown(float x, float y, int pointer, int button) {
            return false;
        }

        @Override
        public boolean tap(float x, float y, int count, int button) {
            return false;
        }

        @Override
        public boolean longPress(float x, float y) {
            return false;
        }

        @Override
        public boolean fling(float velocityX, float velocityY, int button) {
            return false;
        }

        @Override
        public boolean pan(float x, float y, float deltaX, float deltaY) {
            return false;
        }

        @Override
        public boolean panStop(float x, float y, int pointer, int button) {
            return false;
        }

        @Override
        public boolean zoom(float initialDistance, float distance) {
            return false;
        }

        @Override
        public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
            return false;
        }
    }

    static class VelocityTracker {
        int sampleSize = 10;
        float lastX, lastY;
        float deltaX, deltaY;
        long lastTime;
        int numSamples;
        float[] meanX = new float[sampleSize];
        float[] meanY = new float[sampleSize];
        long[] meanTime = new long[sampleSize];

        public void start(float x, float y, long timeStamp) {
            lastX = x;
            lastY = y;
            deltaX = 0;
            deltaY = 0;
            numSamples = 0;
            for (int i = 0; i < sampleSize; i++) {
                meanX[i] = 0;
                meanY[i] = 0;
                meanTime[i] = 0;
            }
            lastTime = timeStamp;
        }

        public void update(float x, float y, long timeStamp) {
            long currTime = timeStamp;
            deltaX = x - lastX;
            deltaY = y - lastY;
            lastX = x;
            lastY = y;
            long deltaTime = currTime - lastTime;
            lastTime = currTime;
            int index = numSamples % sampleSize;
            meanX[index] = deltaX;
            meanY[index] = deltaY;
            meanTime[index] = deltaTime;
            numSamples++;
        }

        public float getVelocityX() {
            float meanX = getAverage(this.meanX, numSamples);
            float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
            if (meanTime == 0)
                return 0;
            return meanX / meanTime;
        }

        public float getVelocityY() {
            float meanY = getAverage(this.meanY, numSamples);
            float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
            if (meanTime == 0)
                return 0;
            return meanY / meanTime;
        }

        private float getAverage(float[] values, int numSamples) {
            numSamples = Math.min(sampleSize, numSamples);
            float sum = 0;
            for (int i = 0; i < numSamples; i++) {
                sum += values[i];
            }
            return sum / numSamples;
        }

        private long getAverage(long[] values, int numSamples) {
            numSamples = Math.min(sampleSize, numSamples);
            long sum = 0;
            for (int i = 0; i < numSamples; i++) {
                sum += values[i];
            }
            if (numSamples == 0)
                return 0;
            return sum / numSamples;
        }

        private float getSum(float[] values, int numSamples) {
            numSamples = Math.min(sampleSize, numSamples);
            float sum = 0;
            for (int i = 0; i < numSamples; i++) {
                sum += values[i];
            }
            if (numSamples == 0)
                return 0;
            return sum;
        }
    }
}