• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
GestureDetector(GestureListener listener)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. */
GestureDetector(float halfTapSquareSize, float tapCountInterval, float longPressDuration, float maxFlingDelay, GestureListener listener)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
touchDown(int x, int y, int pointer, int button)85 	public boolean touchDown (int x, int y, int pointer, int button) {
86 		return touchDown((float)x, (float)y, pointer, button);
87 	}
88 
touchDown(float x, float y, int pointer, int button)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
touchDragged(int x, int y, int pointer)125 	public boolean touchDragged (int x, int y, int pointer) {
126 		return touchDragged((float)x, (float)y, pointer);
127 	}
128 
touchDragged(float x, float y, int pointer)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
touchUp(int x, int y, int pointer, int button)166 	public boolean touchUp (int x, int y, int pointer, int button) {
167 		return touchUp((float)x, (float)y, pointer, button);
168 	}
169 
touchUp(float x, float y, int pointer, int button)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. */
cancel()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. */
isLongPressed()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. */
isLongPressed(float duration)239 	public boolean isLongPressed (float duration) {
240 		if (gestureStartTime == 0) return false;
241 		return TimeUtils.nanoTime() - gestureStartTime > (long)(duration * 1000000000l);
242 	}
243 
isPanning()244 	public boolean isPanning () {
245 		return panning;
246 	}
247 
reset()248 	public void reset () {
249 		gestureStartTime = 0;
250 		panning = false;
251 		inTapSquare = false;
252 	}
253 
isWithinTapSquare(float x, float y, float centerX, float centerY)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. */
invalidateTapSquare()259 	public void invalidateTapSquare () {
260 		inTapSquare = false;
261 	}
262 
setTapSquareSize(float halfTapSquareSize)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. */
setTapCountInterval(float tapCountInterval)268 	public void setTapCountInterval (float tapCountInterval) {
269 		this.tapCountInterval = (long)(tapCountInterval * 1000000000l);
270 	}
271 
setLongPressSeconds(float longPressSeconds)272 	public void setLongPressSeconds (float longPressSeconds) {
273 		this.longPressSeconds = longPressSeconds;
274 	}
275 
setMaxFlingDelay(long maxFlingDelay)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) */
touchDown(float x, float y, int pointer, int button)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. */
tap(float x, float y, int count, int button)292 		public boolean tap (float x, float y, int count, int button);
293 
longPress(float x, float y)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 */
fling(float velocityX, float velocityY, int button)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. */
pan(float x, float y, float deltaX, float deltaY)305 		public boolean pan (float x, float y, float deltaX, float deltaY);
306 
307 		/** Called when no longer panning. */
panStop(float x, float y, int pointer, int button)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. */
zoom(float initialDistance, float distance)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 */
pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2)322 		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2);
323 
324 		/** Called when no longer pinching. */
pinchStop()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
touchDown(float x, float y, int pointer, int button)332 		public boolean touchDown (float x, float y, int pointer, int button) {
333 			return false;
334 		}
335 
336 		@Override
tap(float x, float y, int count, int button)337 		public boolean tap (float x, float y, int count, int button) {
338 			return false;
339 		}
340 
341 		@Override
longPress(float x, float y)342 		public boolean longPress (float x, float y) {
343 			return false;
344 		}
345 
346 		@Override
fling(float velocityX, float velocityY, int button)347 		public boolean fling (float velocityX, float velocityY, int button) {
348 			return false;
349 		}
350 
351 		@Override
pan(float x, float y, float deltaX, float deltaY)352 		public boolean pan (float x, float y, float deltaX, float deltaY) {
353 			return false;
354 		}
355 
356 		@Override
panStop(float x, float y, int pointer, int button)357 		public boolean panStop (float x, float y, int pointer, int button) {
358 			return false;
359 		}
360 
361 		@Override
zoom(float initialDistance, float distance)362 		public boolean zoom (float initialDistance, float distance) {
363 			return false;
364 		}
365 
366 		@Override
pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2)367 		public boolean pinch (Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) {
368 			return false;
369 		}
370 
371 		@Override
pinchStop()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 
start(float x, float y, long timeStamp)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 
update(float x, float y, long timeStamp)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 
getVelocityX()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 
getVelocityY()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 
getAverage(float[] values, int numSamples)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 
getAverage(long[] values, int numSamples)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 
getSum(float[] values, int numSamples)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