com.sun.javafx.tk.quantum.SwipeGestureRecognizer Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javafx.tk.quantum;
import com.sun.glass.events.KeyEvent;
import com.sun.glass.events.TouchEvent;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
import javafx.event.EventType;
import javafx.scene.input.SwipeEvent;
class SwipeGestureRecognizer implements GestureRecognizer {
private static final double TANGENT_30_DEGREES = 0.577;
private static final double TANGENT_45_DEGREES = 1;
private static final boolean VERBOSE = false;
// Swipes must be longer than that
private static final double DISTANCE_THRESHOLD = 10; // pixel
// Traveling this distance against the swipe direction at its end cancels it
private static final double BACKWARD_DISTANCE_THRASHOLD = 5; // pixel
private SwipeRecognitionState state = SwipeRecognitionState.IDLE;
MultiTouchTracker tracker = new MultiTouchTracker();
private ViewScene scene;
SwipeGestureRecognizer(final ViewScene scene) {
this.scene = scene;
}
@Override
public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect,
int touchEventCount) {
tracker.params(modifiers, isDirect);
}
@Override
public void notifyNextTouchEvent(long time, int type, long touchId,
int x, int y, int xAbs, int yAbs) {
switch(type) {
case TouchEvent.TOUCH_PRESSED:
tracker.pressed(touchId, time, x, y, xAbs, yAbs);
break;
case TouchEvent.TOUCH_STILL:
/* NOBREAK */
case TouchEvent.TOUCH_MOVED:
tracker.progress(touchId, time, xAbs, yAbs);
break;
case TouchEvent.TOUCH_RELEASED:
tracker.released(touchId, time, x, y, xAbs, yAbs);
break;
default:
throw new RuntimeException("Error in swipe gesture recognition: "
+ "unknown touch state: " + state);
}
}
@Override
public void notifyEndTouchEvent(long time) {
// nothing to do
}
private EventType calcSwipeType(TouchPointTracker tracker) {
final double distanceX = tracker.getDistanceX();
final double distanceY = tracker.getDistanceY();
final double absDistanceX = Math.abs(distanceX);
final double absDistanceY = Math.abs(distanceY);
final boolean horizontal = absDistanceX > absDistanceY;
final double primaryDistance = horizontal ? distanceX : distanceY;
final double absPrimaryDistance = horizontal ? absDistanceX : absDistanceY;
final double absSecondaryDistance = horizontal ? absDistanceY : absDistanceX;
final double absPrimaryLength = horizontal ?
tracker.lengthX : tracker.lengthY;
final double maxSecondaryDeviation = horizontal ?
tracker.maxDeviationY : tracker.maxDeviationX;
final double lastPrimaryMovement = horizontal ?
tracker.lastXMovement : tracker.lastYMovement;
if (absPrimaryDistance <= DISTANCE_THRESHOLD) {
// too small movement
return null;
}
if (absSecondaryDistance > absPrimaryDistance * TANGENT_30_DEGREES) {
// too diagonal - in range of 60 degrees
return null;
}
if (maxSecondaryDeviation > absPrimaryDistance * TANGENT_45_DEGREES) {
// maximum deviation on the secondary axis, is too big
return null;
}
int swipeMaxDuration = Integer.getInteger("com.sun.javafx.gestures.swipe.maxduration", 300);
if (tracker.getDuration() > swipeMaxDuration) {
return null;
}
if (absPrimaryLength > absPrimaryDistance * 1.5) {
// too much back and forth
return null;
}
if (Math.signum(primaryDistance) != Math.signum(lastPrimaryMovement) &&
Math.abs(lastPrimaryMovement) > BACKWARD_DISTANCE_THRASHOLD) {
// gesture finished in the oposite direction
return null;
}
if (horizontal) {
return tracker.getDistanceX() < 0
? SwipeEvent.SWIPE_LEFT : SwipeEvent.SWIPE_RIGHT;
} else {
return tracker.getDistanceY() < 0
? SwipeEvent.SWIPE_UP : SwipeEvent.SWIPE_DOWN;
}
}
@SuppressWarnings("removal")
private void handleSwipeType(final EventType swipeType,
final CenterComputer cc, final int touchCount, final int modifiers, final boolean isDirect)
{
if (swipeType == null) {
return;
}
if (VERBOSE) {
System.err.println("handleSwipeType swipeType=" + swipeType);
}
AccessController.doPrivileged((PrivilegedAction) () -> {
if (scene.sceneListener != null) {
scene.sceneListener.swipeEvent(swipeType, touchCount,
cc.getX(), cc.getY(),
cc.getAbsX(), cc.getAbsY(),
(modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
(modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
(modifiers & KeyEvent.MODIFIER_ALT) != 0,
(modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
isDirect);
}
return null;
}, scene.getAccessControlContext());
}
private static class CenterComputer {
double totalAbsX = 0, totalAbsY = 0;
double totalX = 0, totalY = 0;
int count = 0;
public void add(double x, double y, double xAbs, double yAbs) {
totalAbsX += xAbs;
totalAbsY += yAbs;
totalX += x;
totalY += y;
count++;
}
public double getX() {
return count == 0 ? 0 : totalX / count;
}
public double getY() {
return count == 0 ? 0 : totalY / count;
}
public double getAbsX() {
return count == 0 ? 0 : totalAbsX / count;
}
public double getAbsY() {
return count == 0 ? 0 : totalAbsY / count;
}
public void reset() {
totalX = 0;
totalY = 0;
totalAbsX = 0;
totalAbsY = 0;
count = 0;
}
}
private class MultiTouchTracker {
SwipeRecognitionState state = SwipeRecognitionState.IDLE;
Map trackers = new HashMap<>();
CenterComputer cc = new CenterComputer();
int modifiers;
boolean direct;
private int touchCount;
private int currentTouchCount;
private EventType type;
public void params(int modifiers, boolean direct) {
this.modifiers = modifiers;
this.direct = direct;
}
public void pressed(long id, long nanos, int x, int y, int xAbs, int yAbs) {
currentTouchCount++;
switch (state) {
case IDLE:
currentTouchCount = 1;
state = SwipeRecognitionState.ADDING;
/* NOBREAK */
case ADDING:
TouchPointTracker tracker = new TouchPointTracker();
tracker.start(nanos, x, y, xAbs, yAbs);
trackers.put(id, tracker);
break;
case REMOVING:
// we don't allow for swipes with varying touch count
state = SwipeRecognitionState.FAILURE;
break;
default:
break;
}
}
public void released(long id, long nanos, int x, int y, int xAbs, int yAbs) {
if (state != SwipeRecognitionState.FAILURE) {
TouchPointTracker tracker = trackers.get(id);
if (tracker == null) {
// we don't know this ID, something went completely wrong
state = SwipeRecognitionState.FAILURE;
throw new RuntimeException("Error in swipe gesture "
+ "recognition: released unknown touch point");
}
tracker.end(nanos, x, y, xAbs, yAbs);
cc.add(tracker.beginX, tracker.beginY,
tracker.beginAbsX, tracker.beginAbsY);
cc.add(tracker.endX, tracker.endY,
tracker.endAbsX, tracker.endAbsY);
final EventType swipeType = calcSwipeType(tracker);
switch (state) {
case IDLE:
reset();
throw new RuntimeException("Error in swipe gesture "
+ "recognition: released touch point outside "
+ "of gesture");
case ADDING:
state = SwipeRecognitionState.REMOVING;
touchCount = currentTouchCount;
type = swipeType;
break;
case REMOVING:
if (type != swipeType) {
// each finger does something else
state = SwipeRecognitionState.FAILURE;
}
break;
default:
break;
}
trackers.remove(id);
}
currentTouchCount--;
if (currentTouchCount == 0) {
if (state == SwipeRecognitionState.REMOVING) {
handleSwipeType(type, cc, touchCount, modifiers, direct);
}
state = SwipeRecognitionState.IDLE;
reset();
}
}
public void progress(long id, long nanos, int x, int y) {
if (state == SwipeRecognitionState.FAILURE) {
return;
}
TouchPointTracker tracker = trackers.get(id);
if (tracker == null) {
// we don't know this ID, something went completely wrong
state = SwipeRecognitionState.FAILURE;
throw new RuntimeException("Error in swipe gesture "
+ "recognition: reported unknown touch point");
}
tracker.progress(nanos, x, y);
}
void reset() {
trackers.clear();
cc.reset();
state = SwipeRecognitionState.IDLE;
}
}
private static class TouchPointTracker {
long beginTime, endTime;
double beginX, beginY, endX, endY;
double beginAbsX, beginAbsY, endAbsX, endAbsY;
double lengthX, lengthY;
double maxDeviationX, maxDeviationY;
double lastXMovement, lastYMovement;
double lastX, lastY;
public void start(long nanos, double x, double y, double absX, double absY) {
beginX = x;
beginY = y;
beginAbsX = absX;
beginAbsY = absY;
lastX = absX;
lastY = absY;
beginTime = nanos / 1000000;
}
public void end(long nanos, double x, double y, double absX, double absY) {
progress(nanos, absX, absY);
endX = x;
endY = y;
endAbsX = absX;
endAbsY = absY;
endTime = nanos / 1000000;
}
public void progress(long nanos, double x, double y) {
final double deltaX = x - lastX;
final double deltaY = y - lastY;
lengthX += Math.abs(deltaX);
lengthY += Math.abs(deltaY);
lastX = x;
lastY = y;
final double devX = Math.abs(x - beginAbsX);
if (devX > maxDeviationX) { maxDeviationX = devX; }
final double devY = Math.abs(y - beginAbsY);
if (devY > maxDeviationY) { maxDeviationY = devY; }
if (Math.signum(deltaX) == Math.signum(lastXMovement)) {
lastXMovement += deltaX;
} else {
lastXMovement = deltaX;
}
if (Math.signum(deltaY) == Math.signum(lastYMovement)) {
lastYMovement += deltaY;
} else {
lastYMovement = deltaY;
}
}
public double getDistanceX() {
return endX - beginX;
}
public double getDistanceY() {
return endY - beginY;
}
public long getDuration() {
return endTime - beginTime;
}
}
private enum SwipeRecognitionState {
IDLE,
ADDING,
REMOVING,
FAILURE
}
}