Home | History | Annotate | Download | only in input
      1 /*******************************************************************************
      2  * Copyright 2011 See AUTHORS file.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *   http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  ******************************************************************************/
     16 
     17 package com.badlogic.gdx.input;
     18 
     19 import com.badlogic.gdx.Gdx;
     20 import com.badlogic.gdx.InputAdapter;
     21 import com.badlogic.gdx.InputProcessor;
     22 import com.badlogic.gdx.math.Vector2;
     23 import com.badlogic.gdx.utils.TimeUtils;
     24 import com.badlogic.gdx.utils.Timer;
     25 import com.badlogic.gdx.utils.Timer.Task;
     26 
     27 /** {@link InputProcessor} implementation that detects gestures (tap, long press, fling, pan, zoom, pinch) and hands them to a
     28  * {@link GestureListener}.
     29  * @author mzechner */
     30 public class GestureDetector extends InputAdapter {
     31 	final GestureListener listener;
     32 	private float tapSquareSize;
     33 	private long tapCountInterval;
     34 	private float longPressSeconds;
     35 	private long maxFlingDelay;
     36 
     37 	private boolean inTapSquare;
     38 	private int tapCount;
     39 	private long lastTapTime;
     40 	private float lastTapX, lastTapY;
     41 	private int lastTapButton, lastTapPointer;
     42 	boolean longPressFired;
     43 	private boolean pinching;
     44 	private boolean panning;
     45 
     46 	private final VelocityTracker tracker = new VelocityTracker();
     47 	private float tapSquareCenterX, tapSquareCenterY;
     48 	private long gestureStartTime;
     49 	Vector2 pointer1 = new Vector2();
     50 	private final Vector2 pointer2 = new Vector2();
     51 	private final Vector2 initialPointer1 = new Vector2();
     52 	private final Vector2 initialPointer2 = new Vector2();
     53 
     54 	private final Task longPressTask = new Task() {
     55 		@Override
     56 		public void run () {
     57 			if (!longPressFired) longPressFired = listener.longPress(pointer1.x, pointer1.y);
     58 		}
     59 	};
     60 
     61 	/** Creates a new GestureDetector with default values: halfTapSquareSize=20, tapCountInterval=0.4f, longPressDuration=1.1f,
     62 	 * maxFlingDelay=0.15f. */
     63 	public GestureDetector (GestureListener listener) {
     64 		this(20, 0.4f, 1.1f, 0.15f, listener);
     65 	}
     66 
     67 	/** @param halfTapSquareSize half width in pixels of the square around an initial touch event, see
     68 	 *           {@link GestureListener#tap(float, float, int, int)}.
     69 	 * @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps.
     70 	 * @param longPressDuration time in seconds that must pass for the detector to fire a
     71 	 *           {@link GestureListener#longPress(float, float)} event.
     72 	 * @param maxFlingDelay time in seconds the finger must have been dragged for a fling event to be fired, see
     73 	 *           {@link GestureListener#fling(float, float, int)}
     74 	 * @param listener May be null if the listener will be set later. */
     75 	public GestureDetector (float halfTapSquareSize, float tapCountInterval, float longPressDuration, float maxFlingDelay,
     76 		GestureListener listener) {
     77 		this.tapSquareSize = halfTapSquareSize;
     78 		this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
     79 		this.longPressSeconds = longPressDuration;
     80 		this.maxFlingDelay = (long)(maxFlingDelay * 1000000000l);
     81 		this.listener = listener;
     82 	}
     83 
     84 	@Override
     85 	public boolean touchDown (int x, int y, int pointer, int button) {
     86 		return touchDown((float)x, (float)y, pointer, button);
     87 	}
     88 
     89 	public boolean touchDown (float x, float y, int pointer, int button) {
     90 		if (pointer > 1) return false;
     91 
     92 		if (pointer == 0) {
     93 			pointer1.set(x, y);
     94 			gestureStartTime = Gdx.input.getCurrentEventTime();
     95 			tracker.start(x, y, gestureStartTime);
     96 			if (Gdx.input.isTouched(1)) {
     97 				// Start pinch.
     98 				inTapSquare = false;
     99 				pinching = true;
    100 				initialPointer1.set(pointer1);
    101 				initialPointer2.set(pointer2);
    102 				longPressTask.cancel();
    103 			} else {
    104 				// Normal touch down.
    105 				inTapSquare = true;
    106 				pinching = false;
    107 				longPressFired = false;
    108 				tapSquareCenterX = x;
    109 				tapSquareCenterY = y;
    110 				if (!longPressTask.isScheduled()) Timer.schedule(longPressTask, longPressSeconds);
    111 			}
    112 		} else {
    113 			// Start pinch.
    114 			pointer2.set(x, y);
    115 			inTapSquare = false;
    116 			pinching = true;
    117 			initialPointer1.set(pointer1);
    118 			initialPointer2.set(pointer2);
    119 			longPressTask.cancel();
    120 		}
    121 		return listener.touchDown(x, y, pointer, button);
    122 	}
    123 
    124 	@Override
    125 	public boolean touchDragged (int x, int y, int pointer) {
    126 		return touchDragged((float)x, (float)y, pointer);
    127 	}
    128 
    129 	public boolean touchDragged (float x, float y, int pointer) {
    130 		if (pointer > 1) return false;
    131 		if (longPressFired) return false;
    132 
    133 		if (pointer == 0)
    134 			pointer1.set(x, y);
    135 		else
    136 			pointer2.set(x, y);
    137 
    138 		// handle pinch zoom
    139 		if (pinching) {
    140 			if (listener != null) {
    141 				boolean result = listener.pinch(initialPointer1, initialPointer2, pointer1, pointer2);
    142 				return listener.zoom(initialPointer1.dst(initialPointer2), pointer1.dst(pointer2)) || result;
    143 			}
    144 			return false;
    145 		}
    146 
    147 		// update tracker
    148 		tracker.update(x, y, Gdx.input.getCurrentEventTime());
    149 
    150 		// check if we are still tapping.
    151 		if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) {
    152 			longPressTask.cancel();
    153 			inTapSquare = false;
    154 		}
    155 
    156 		// if we have left the tap square, we are panning
    157 		if (!inTapSquare) {
    158 			panning = true;
    159 			return listener.pan(x, y, tracker.deltaX, tracker.deltaY);
    160 		}
    161 
    162 		return false;
    163 	}
    164 
    165 	@Override
    166 	public boolean touchUp (int x, int y, int pointer, int button) {
    167 		return touchUp((float)x, (float)y, pointer, button);
    168 	}
    169 
    170 	public boolean touchUp (float x, float y, int pointer, int button) {
    171 		if (pointer > 1) return false;
    172 
    173 		// check if we are still tapping.
    174 		if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) inTapSquare = false;
    175 
    176 		boolean wasPanning = panning;
    177 		panning = false;
    178 
    179 		longPressTask.cancel();
    180 		if (longPressFired) return false;
    181 
    182 		if (inTapSquare) {
    183 			// handle taps
    184 			if (lastTapButton != button || lastTapPointer != pointer || TimeUtils.nanoTime() - lastTapTime > tapCountInterval
    185 				|| !isWithinTapSquare(x, y, lastTapX, lastTapY)) tapCount = 0;
    186 			tapCount++;
    187 			lastTapTime = TimeUtils.nanoTime();
    188 			lastTapX = x;
    189 			lastTapY = y;
    190 			lastTapButton = button;
    191 			lastTapPointer = pointer;
    192 			gestureStartTime = 0;
    193 			return listener.tap(x, y, tapCount, button);
    194 		}
    195 
    196 		if (pinching) {
    197 			// handle pinch end
    198 			pinching = false;
    199 			listener.pinchStop();
    200 			panning = true;
    201 			// we are in pan mode again, reset velocity tracker
    202 			if (pointer == 0) {
    203 				// first pointer has lifted off, set up panning to use the second pointer...
    204 				tracker.start(pointer2.x, pointer2.y, Gdx.input.getCurrentEventTime());
    205 			} else {
    206 				// second pointer has lifted off, set up panning to use the first pointer...
    207 				tracker.start(pointer1.x, pointer1.y, Gdx.input.getCurrentEventTime());
    208 			}
    209 			return false;
    210 		}
    211 
    212 		// handle no longer panning
    213 		boolean handled = false;
    214 		if (wasPanning && !panning) handled = listener.panStop(x, y, pointer, button);
    215 
    216 		// handle fling
    217 		gestureStartTime = 0;
    218 		long time = Gdx.input.getCurrentEventTime();
    219 		if (time - tracker.lastTime < maxFlingDelay) {
    220 			tracker.update(x, y, time);
    221 			handled = listener.fling(tracker.getVelocityX(), tracker.getVelocityY(), button) || handled;
    222 		}
    223 		return handled;
    224 	}
    225 
    226 	/** No further gesture events will be triggered for the current touch, if any. */
    227 	public void cancel () {
    228 		longPressTask.cancel();
    229 		longPressFired = true;
    230 	}
    231 
    232 	/** @return whether the user touched the screen long enough to trigger a long press event. */
    233 	public boolean isLongPressed () {
    234 		return isLongPressed(longPressSeconds);
    235 	}
    236 
    237 	/** @param duration
    238 	 * @return whether the user touched the screen for as much or more than the given duration. */
    239 	public boolean isLongPressed (float duration) {
    240 		if (gestureStartTime == 0) return false;
    241 		return TimeUtils.nanoTime() - gestureStartTime > (long)(duration * 1000000000l);
    242 	}
    243 
    244 	public boolean isPanning () {
    245 		return panning;
    246 	}
    247 
    248 	public void reset () {
    249 		gestureStartTime = 0;
    250 		panning = false;
    251 		inTapSquare = false;
    252 	}
    253 
    254 	private boolean isWithinTapSquare (float x, float y, float centerX, float centerY) {
    255 		return Math.abs(x - centerX) < tapSquareSize && Math.abs(y - centerY) < tapSquareSize;
    256 	}
    257 
    258 	/** The tap square will not longer be used for the current touch. */
    259 	public void invalidateTapSquare () {
    260 		inTapSquare = false;
    261 	}
    262 
    263 	public void setTapSquareSize (float halfTapSquareSize) {
    264 		this.tapSquareSize = halfTapSquareSize;
    265 	}
    266 
    267 	/** @param tapCountInterval time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps. */
    268 	public void setTapCountInterval (float tapCountInterval) {
    269 		this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
    270 	}
    271 
    272 	public void setLongPressSeconds (float longPressSeconds) {
    273 		this.longPressSeconds = longPressSeconds;
    274 	}
    275 
    276 	public void setMaxFlingDelay (long maxFlingDelay) {
    277 		this.maxFlingDelay = maxFlingDelay;
    278 	}
    279 
    280 	/** Register an instance of this class with a {@link GestureDetector} to receive gestures such as taps, long presses, flings,
    281 	 * panning or pinch zooming. Each method returns a boolean indicating if the event should be handed to the next listener (false
    282 	 * to hand it to the next listener, true otherwise).
    283 	 * @author mzechner */
    284 	public static interface GestureListener {
    285 		/** @see InputProcessor#touchDown(int, int, int, int) */
    286 		public boolean touchDown (float x, float y, int pointer, int button);
    287 
    288 		/** Called when a tap occured. A tap happens if a touch went down on the screen and was lifted again without moving outside
    289 		 * of the tap square. The tap square is a rectangular area around the initial touch position as specified on construction
    290 		 * time of the {@link GestureDetector}.
    291 		 * @param count the number of taps. */
    292 		public boolean tap (float x, float y, int count, int button);
    293 
    294 		public boolean longPress (float x, float y);
    295 
    296 		/** Called when the user dragged a finger over the screen and lifted it. Reports the last known velocity of the finger in
    297 		 * pixels per second.
    298 		 * @param velocityX velocity on x in seconds
    299 		 * @param velocityY velocity on y in seconds */
    300 		public boolean fling (float velocityX, float velocityY, int button);
    301 
    302 		/** Called when the user drags a finger over the screen.
    303 		 * @param deltaX the difference in pixels to the last drag event on x.
    304 		 * @param deltaY the difference in pixels to the last drag event on y. */
    305 		public boolean pan (float x, float y, float deltaX, float deltaY);
    306 
    307 		/** Called when no longer panning. */
    308 		public boolean panStop (float x, float y, int pointer, int button);
    309 
    310 		/** Called when the user performs a pinch zoom gesture. The original distance is the distance in pixels when the gesture
    311 		 * started.
    312 		 * @param initialDistance distance between fingers when the gesture started.
    313 		 * @param distance current distance between fingers. */
    314 		public boolean zoom (float initialDistance, float distance);
    315 
    316 		/** Called when a user performs a pinch zoom gesture. Reports the initial positions of the two involved fingers and their
    317 		 * current positions.
    318 		 * @param initialPointer1
    319 		 * @param initialPointer2
    320 		 * @param pointer1
    321 		 * @param pointer2 */
    322 		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2);
    323 
    324 		/** Called when no longer pinching. */
    325 		public void pinchStop ();
    326 	}
    327 
    328 	/** Derrive from this if you only want to implement a subset of {@link GestureListener}.
    329 	 * @author mzechner */
    330 	public static class GestureAdapter implements GestureListener {
    331 		@Override
    332 		public boolean touchDown (float x, float y, int pointer, int button) {
    333 			return false;
    334 		}
    335 
    336 		@Override
    337 		public boolean tap (float x, float y, int count, int button) {
    338 			return false;
    339 		}
    340 
    341 		@Override
    342 		public boolean longPress (float x, float y) {
    343 			return false;
    344 		}
    345 
    346 		@Override
    347 		public boolean fling (float velocityX, float velocityY, int button) {
    348 			return false;
    349 		}
    350 
    351 		@Override
    352 		public boolean pan (float x, float y, float deltaX, float deltaY) {
    353 			return false;
    354 		}
    355 
    356 		@Override
    357 		public boolean panStop (float x, float y, int pointer, int button) {
    358 			return false;
    359 		}
    360 
    361 		@Override
    362 		public boolean zoom (float initialDistance, float distance) {
    363 			return false;
    364 		}
    365 
    366 		@Override
    367 		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
    368 			return false;
    369 		}
    370 
    371 		@Override
    372 		public void pinchStop () {
    373 		}
    374 	}
    375 
    376 	static class VelocityTracker {
    377 		int sampleSize = 10;
    378 		float lastX, lastY;
    379 		float deltaX, deltaY;
    380 		long lastTime;
    381 		int numSamples;
    382 		float[] meanX = new float[sampleSize];
    383 		float[] meanY = new float[sampleSize];
    384 		long[] meanTime = new long[sampleSize];
    385 
    386 		public void start (float x, float y, long timeStamp) {
    387 			lastX = x;
    388 			lastY = y;
    389 			deltaX = 0;
    390 			deltaY = 0;
    391 			numSamples = 0;
    392 			for (int i = 0; i < sampleSize; i++) {
    393 				meanX[i] = 0;
    394 				meanY[i] = 0;
    395 				meanTime[i] = 0;
    396 			}
    397 			lastTime = timeStamp;
    398 		}
    399 
    400 		public void update (float x, float y, long timeStamp) {
    401 			long currTime = timeStamp;
    402 			deltaX = x - lastX;
    403 			deltaY = y - lastY;
    404 			lastX = x;
    405 			lastY = y;
    406 			long deltaTime = currTime - lastTime;
    407 			lastTime = currTime;
    408 			int index = numSamples % sampleSize;
    409 			meanX[index] = deltaX;
    410 			meanY[index] = deltaY;
    411 			meanTime[index] = deltaTime;
    412 			numSamples++;
    413 		}
    414 
    415 		public float getVelocityX () {
    416 			float meanX = getAverage(this.meanX, numSamples);
    417 			float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
    418 			if (meanTime == 0) return 0;
    419 			return meanX / meanTime;
    420 		}
    421 
    422 		public float getVelocityY () {
    423 			float meanY = getAverage(this.meanY, numSamples);
    424 			float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f;
    425 			if (meanTime == 0) return 0;
    426 			return meanY / meanTime;
    427 		}
    428 
    429 		private float getAverage (float[] values, int numSamples) {
    430 			numSamples = Math.min(sampleSize, numSamples);
    431 			float sum = 0;
    432 			for (int i = 0; i < numSamples; i++) {
    433 				sum += values[i];
    434 			}
    435 			return sum / numSamples;
    436 		}
    437 
    438 		private long getAverage (long[] values, int numSamples) {
    439 			numSamples = Math.min(sampleSize, numSamples);
    440 			long sum = 0;
    441 			for (int i = 0; i < numSamples; i++) {
    442 				sum += values[i];
    443 			}
    444 			if (numSamples == 0) return 0;
    445 			return sum / numSamples;
    446 		}
    447 
    448 		private float getSum (float[] values, int numSamples) {
    449 			numSamples = Math.min(sampleSize, numSamples);
    450 			float sum = 0;
    451 			for (int i = 0; i < numSamples; i++) {
    452 				sum += values[i];
    453 			}
    454 			if (numSamples == 0) return 0;
    455 			return sum;
    456 		}
    457 	}
    458 }
    459