Java tutorial
package com.mygdx.game.ui; /******************************************************************************* * 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. ******************************************************************************/ import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.InputAdapter; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener; 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 MultiGestureDetector 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; final Pointer pointer1 = new Pointer(); final Pointer pointer2 = new Pointer(); private int pointersCount = 0; private final Task longPressTask = new Task() { @Override public void run() { if (!longPressFired) longPressFired = listener.longPress(pointer1.pos.x, pointer1.pos.y); } }; /** Creates a new GestureDetector with default values: halfTapSquareSize=20, tapCountInterval=0.4f, longPressDuration=1.1f, * maxFlingDelay=0.15f. */ public MultiGestureDetector(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 MultiGestureDetector(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; } private Pointer getPointerById(int pointerId) { if (pointer1.id == pointerId) { return pointer1; } else if (pointer2.id == pointerId) { return pointer2; } else if (pointer1.id < 0) { return pointer1; } else if (pointer2.id < 0) { return pointer2; } return null; } @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) { final Pointer pt = getPointerById(pointer); if (pt == null) return false; ++pointersCount; pt.touchDown(x, y, pointer); if (pointersCount == 1) { gestureStartTime = Gdx.input.getCurrentEventTime(); tracker.start(x, y, gestureStartTime); // Normal touch down. inTapSquare = true; pinching = false; longPressFired = false; tapSquareCenterX = x; tapSquareCenterY = y; if (!longPressTask.isScheduled()) Timer.schedule(longPressTask, longPressSeconds); } else { // pointersCount == 2 // Start pinch. inTapSquare = false; pinching = true; pointer1.startPinch(); pointer2.startPinch(); 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) { final Pointer pt = getPointerById(pointer); if (pt == null) return false; if (longPressFired) return false; pt.pos.set(x, y); // handle pinch zoom if (pinching) { if (listener != null) { boolean result = listener.pinch(pointer1.initialPos, pointer2.initialPos, pointer1.pos, pointer2.pos); return listener.zoom(pointer1.initialPos.dst(pointer2.initialPos), pointer1.pos.dst(pointer2.pos)) || 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) { final Pointer pt = getPointerById(pointer); if (pt == null || pt.id < 0) return false; --pointersCount; pt.touchUp(); // 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 (pointer1.id < 0) { // first pointer has lifted off, set up panning to use the second pointer... tracker.start(pointer2.pos.x, pointer2.pos.y, Gdx.input.getCurrentEventTime()); } else { // second pointer has lifted off, set up panning to use the first pointer... tracker.start(pointer1.pos.x, pointer1.pos.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 tapSquareSize) { this.tapSquareSize = tapSquareSize; } /** @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 MultiGestureDetector} 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 MultiGestureDetector}. * @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; } } private static class Pointer { int id = -1; final Vector2 pos = new Vector2(); final Vector2 initialPos = new Vector2(); Pointer() { } void touchUp() { id = -1; } void touchDown(float x, float y, int pointerId) { id = pointerId; pos.set(x, y); } void startPinch() { initialPos.set(pos); } } }