1 /* 2 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.tk.quantum; 27 28 import com.sun.glass.events.KeyEvent; 29 import com.sun.glass.events.TouchEvent; 30 31 import java.security.AccessController; 32 import java.security.PrivilegedAction; 33 import java.util.HashMap; 34 import java.util.Map; 35 import javafx.event.EventType; 36 import javafx.scene.input.SwipeEvent; 37 38 class SwipeGestureRecognizer implements GestureRecognizer { 39 private static final boolean VERBOSE = false; 40 41 // Swipes must be longer than that 42 private static final double DISTANCE_THRESHOLD = 10; // pixel 43 44 // Traveling this distance against the swipe direction at its end cancels it 45 private static final double BACKWARD_DISTANCE_THRASHOLD = 5; // pixel 46 47 private SwipeRecognitionState state = SwipeRecognitionState.IDLE; 48 MultiTouchTracker tracker = new MultiTouchTracker(); 49 private ViewScene scene; 50 51 SwipeGestureRecognizer(final ViewScene scene) { 52 this.scene = scene; 53 } 54 55 @Override 56 public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect, 57 int touchEventCount) { 58 tracker.params(modifiers, isDirect); 59 } 60 61 @Override 62 public void notifyNextTouchEvent(long time, int type, long touchId, 63 int x, int y, int xAbs, int yAbs) { 64 switch(type) { 65 case TouchEvent.TOUCH_PRESSED: 66 tracker.pressed(touchId, time, x, y, xAbs, yAbs); 67 break; 68 case TouchEvent.TOUCH_STILL: 69 /* NOBREAK */ 70 case TouchEvent.TOUCH_MOVED: 71 tracker.progress(touchId, time, xAbs, yAbs); 72 break; 73 case TouchEvent.TOUCH_RELEASED: 74 tracker.released(touchId, time, x, y, xAbs, yAbs); 75 break; 76 default: 77 throw new RuntimeException("Error in swipe gesture recognition: " 78 + "unknown touch state: " + state); 79 } 80 } 81 82 @Override 83 public void notifyEndTouchEvent(long time) { 84 // nothing to do 85 } 86 87 private EventType<SwipeEvent> calcSwipeType(TouchPointTracker tracker) { 88 89 final double distanceX = tracker.getDistanceX(); 90 final double distanceY = tracker.getDistanceY(); 91 final double absDistanceX = Math.abs(distanceX); 92 final double absDistanceY = Math.abs(distanceY); 93 94 final boolean horizontal = absDistanceX > absDistanceY; 95 96 final double primaryDistance = horizontal ? distanceX : distanceY; 97 final double absPrimaryDistance = horizontal ? absDistanceX : absDistanceY; 98 final double absSecondaryDistance = horizontal ? absDistanceY : absDistanceX; 99 final double absPrimaryLength = horizontal ? 100 tracker.lengthX : tracker.lengthY; 101 final double maxSecondaryDeviation = horizontal ? 102 tracker.maxDeviationY : tracker.maxDeviationX; 103 final double lastPrimaryMovement = horizontal ? 104 tracker.lastXMovement : tracker.lastYMovement; 105 106 if (absPrimaryDistance <= DISTANCE_THRESHOLD) { 107 // too small movement 108 return null; 109 } 110 111 if (absSecondaryDistance > absPrimaryDistance * 0.839 /* tan(2Pi/9) */) { 112 // too diagonal - in range of 10 degrees 113 return null; 114 } 115 116 if (maxSecondaryDeviation > absPrimaryLength / (tracker.getDuration() / 100.0)) { 117 // too imprecise for the performed speed (the slower movement, 118 // the higher precision requred) 119 return null; 120 } 121 122 if (absPrimaryLength > absPrimaryDistance * 1.5) { 123 // too much back and forth 124 return null; 125 } 126 127 if (Math.signum(primaryDistance) != Math.signum(lastPrimaryMovement) && 128 Math.abs(lastPrimaryMovement) > BACKWARD_DISTANCE_THRASHOLD) { 129 // gesture finished in the oposite direction 130 return null; 131 } 132 133 if (horizontal) { 134 return tracker.getDistanceX() < 0 135 ? SwipeEvent.SWIPE_LEFT : SwipeEvent.SWIPE_RIGHT; 136 } else { 137 return tracker.getDistanceY() < 0 138 ? SwipeEvent.SWIPE_UP : SwipeEvent.SWIPE_DOWN; 139 } 140 } 141 142 private void handleSwipeType(final EventType<SwipeEvent> swipeType, 143 final CenterComputer cc, final int touchCount, final int modifiers, final boolean isDirect) 144 { 145 if (swipeType == null) { 146 return; 147 } 148 if (VERBOSE) { 149 System.err.println("handleSwipeType swipeType=" + swipeType); 150 } 151 152 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 153 if (scene.sceneListener != null) { 154 scene.sceneListener.swipeEvent(swipeType, touchCount, 155 cc.getX(), cc.getY(), 156 cc.getAbsX(), cc.getAbsY(), 157 (modifiers & KeyEvent.MODIFIER_SHIFT) != 0, 158 (modifiers & KeyEvent.MODIFIER_CONTROL) != 0, 159 (modifiers & KeyEvent.MODIFIER_ALT) != 0, 160 (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0, 161 isDirect); 162 } 163 return null; 164 }, scene.getAccessControlContext()); 165 } 166 167 private static class CenterComputer { 168 double totalAbsX = 0, totalAbsY = 0; 169 double totalX = 0, totalY = 0; 170 int count = 0; 171 172 public void add(double x, double y, double xAbs, double yAbs) { 173 totalAbsX += xAbs; 174 totalAbsY += yAbs; 175 totalX += x; 176 totalY += y; 177 178 count++; 179 } 180 181 public double getX() { 182 return count == 0 ? 0 : totalX / count; 183 } 184 185 public double getY() { 186 return count == 0 ? 0 : totalY / count; 187 } 188 189 public double getAbsX() { 190 return count == 0 ? 0 : totalAbsX / count; 191 } 192 193 public double getAbsY() { 194 return count == 0 ? 0 : totalAbsY / count; 195 } 196 197 public void reset() { 198 totalX = 0; 199 totalY = 0; 200 totalAbsX = 0; 201 totalAbsY = 0; 202 count = 0; 203 } 204 } 205 206 private class MultiTouchTracker { 207 SwipeRecognitionState state = SwipeRecognitionState.IDLE; 208 Map<Long, TouchPointTracker> trackers = 209 new HashMap<Long, TouchPointTracker>(); 210 CenterComputer cc = new CenterComputer(); 211 int modifiers; 212 boolean direct; 213 private int touchCount; 214 private int currentTouchCount; 215 private EventType<SwipeEvent> type; 216 217 public void params(int modifiers, boolean direct) { 218 this.modifiers = modifiers; 219 this.direct = direct; 220 } 221 222 public void pressed(long id, long nanos, int x, int y, int xAbs, int yAbs) { 223 currentTouchCount++; 224 switch (state) { 225 case IDLE: 226 currentTouchCount = 1; 227 state = SwipeRecognitionState.ADDING; 228 /* NOBREAK */ 229 case ADDING: 230 TouchPointTracker tracker = new TouchPointTracker(); 231 tracker.start(nanos, x, y, xAbs, yAbs); 232 trackers.put(id, tracker); 233 break; 234 case REMOVING: 235 // we don't allow for swipes with varying touch count 236 state = SwipeRecognitionState.FAILURE; 237 break; 238 default: 239 break; 240 } 241 } 242 243 public void released(long id, long nanos, int x, int y, int xAbs, int yAbs) { 244 if (state != SwipeRecognitionState.FAILURE) { 245 TouchPointTracker tracker = trackers.get(id); 246 247 if (tracker == null) { 248 // we don't know this ID, something went completely wrong 249 state = SwipeRecognitionState.FAILURE; 250 throw new RuntimeException("Error in swipe gesture " 251 + "recognition: released unknown touch point"); 252 } 253 254 tracker.end(nanos, x, y, xAbs, yAbs); 255 cc.add(tracker.beginX, tracker.beginY, 256 tracker.beginAbsX, tracker.beginAbsY); 257 cc.add(tracker.endX, tracker.endY, 258 tracker.endAbsX, tracker.endAbsY); 259 260 final EventType<SwipeEvent> swipeType = calcSwipeType(tracker); 261 262 switch (state) { 263 case IDLE: 264 reset(); 265 throw new RuntimeException("Error in swipe gesture " 266 + "recognition: released touch point outside " 267 + "of gesture"); 268 case ADDING: 269 state = SwipeRecognitionState.REMOVING; 270 touchCount = currentTouchCount; 271 type = swipeType; 272 break; 273 case REMOVING: 274 if (type != swipeType) { 275 // each finger does something else 276 state = SwipeRecognitionState.FAILURE; 277 } 278 break; 279 default: 280 break; 281 } 282 trackers.remove(id); 283 } 284 285 currentTouchCount--; 286 287 if (currentTouchCount == 0) { 288 if (state == SwipeRecognitionState.REMOVING) { 289 handleSwipeType(type, cc, touchCount, modifiers, direct); 290 } 291 292 state = SwipeRecognitionState.IDLE; 293 reset(); 294 } 295 } 296 297 public void progress(long id, long nanos, int x, int y) { 298 299 if (state == SwipeRecognitionState.FAILURE) { 300 return; 301 } 302 303 TouchPointTracker tracker = trackers.get(id); 304 305 if (tracker == null) { 306 // we don't know this ID, something went completely wrong 307 state = SwipeRecognitionState.FAILURE; 308 throw new RuntimeException("Error in swipe gesture " 309 + "recognition: reported unknown touch point"); 310 } 311 312 tracker.progress(nanos, x, y); 313 } 314 315 void reset() { 316 trackers.clear(); 317 cc.reset(); 318 state = SwipeRecognitionState.IDLE; 319 } 320 } 321 322 private static class TouchPointTracker { 323 long beginTime, endTime; 324 double beginX, beginY, endX, endY; 325 double beginAbsX, beginAbsY, endAbsX, endAbsY; 326 double lengthX, lengthY; 327 double maxDeviationX, maxDeviationY; 328 double lastXMovement, lastYMovement; 329 double lastX, lastY; 330 331 public void start(long nanos, double x, double y, double absX, double absY) { 332 beginX = x; 333 beginY = y; 334 beginAbsX = absX; 335 beginAbsY = absY; 336 lastX = absX; 337 lastY = absY; 338 beginTime = nanos / 1000000; 339 } 340 341 public void end(long nanos, double x, double y, double absX, double absY) { 342 progress(nanos, absX, absY); 343 endX = x; 344 endY = y; 345 endAbsX = absX; 346 endAbsY = absY; 347 endTime = nanos / 1000000; 348 } 349 350 public void progress(long nanos, double x, double y) { 351 final double deltaX = x - lastX; 352 final double deltaY = y - lastY; 353 354 lengthX += Math.abs(deltaX); 355 lengthY += Math.abs(deltaY); 356 lastX = x; 357 lastY = y; 358 359 final double devX = Math.abs(x - beginAbsX); 360 if (devX > maxDeviationX) { maxDeviationX = devX; } 361 362 final double devY = Math.abs(y - beginAbsY); 363 if (devY > maxDeviationY) { maxDeviationY = devY; } 364 365 if (Math.signum(deltaX) == Math.signum(lastXMovement)) { 366 lastXMovement += deltaX; 367 } else { 368 lastXMovement = deltaX; 369 } 370 371 if (Math.signum(deltaY) == Math.signum(lastYMovement)) { 372 lastYMovement += deltaY; 373 } else { 374 lastYMovement = deltaY; 375 } 376 } 377 378 public double getDistanceX() { 379 return endX - beginX; 380 } 381 382 public double getDistanceY() { 383 return endY - beginY; 384 } 385 386 public long getDuration() { 387 return endTime - beginTime; 388 } 389 } 390 391 private enum SwipeRecognitionState { 392 IDLE, 393 ADDING, 394 REMOVING, 395 FAILURE 396 } 397 }