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