From 61aa3e5603827b72da752e46de68b2954d80f7d8 Mon Sep 17 00:00:00 2001 From: drdev Date: Thu, 6 Mar 2014 15:32:58 +0000 Subject: [PATCH] Improve gesture handling to not make press effect happen immediately and make release effect apply when starting to pan or pinch --- .gitattributes | 1 + forge-m-base/src/forge/Forge.java | 161 +++++----- .../src/forge/screens/LaunchScreen.java | 4 +- forge-m-base/src/forge/toolbox/FButton.java | 4 +- .../src/forge/toolbox/FDisplayObject.java | 12 +- .../src/forge/toolbox/FGestureAdapter.java | 304 ++++++++++++++++++ forge-m-base/src/forge/toolbox/FLabel.java | 4 +- forge-m-base/src/forge/toolbox/FList.java | 4 +- 8 files changed, 400 insertions(+), 94 deletions(-) create mode 100644 forge-m-base/src/forge/toolbox/FGestureAdapter.java diff --git a/.gitattributes b/.gitattributes index 740c87a7c00..4b297ed21d7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16038,6 +16038,7 @@ forge-m-base/src/forge/screens/settings/SettingsScreen.java -text forge-m-base/src/forge/toolbox/FButton.java -text forge-m-base/src/forge/toolbox/FContainer.java -text forge-m-base/src/forge/toolbox/FDisplayObject.java -text +forge-m-base/src/forge/toolbox/FGestureAdapter.java -text forge-m-base/src/forge/toolbox/FLabel.java -text forge-m-base/src/forge/toolbox/FList.java -text forge-m-base/src/forge/toolbox/FOverlay.java -text diff --git a/forge-m-base/src/forge/Forge.java b/forge-m-base/src/forge/Forge.java index 6e5c32ed83d..bd32cf4155d 100644 --- a/forge-m-base/src/forge/Forge.java +++ b/forge-m-base/src/forge/Forge.java @@ -15,7 +15,6 @@ import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.g2d.BitmapFont.HAlignment; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; -import com.badlogic.gdx.input.GestureDetector; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; @@ -30,6 +29,7 @@ import forge.screens.SplashScreen; import forge.screens.home.HomeScreen; import forge.toolbox.FContainer; import forge.toolbox.FDisplayObject; +import forge.toolbox.FGestureAdapter; public class Forge implements ApplicationListener { private static Forge game; @@ -78,7 +78,7 @@ public class Forge implements ApplicationListener { FSkin.loadFull(splashScreen); - Gdx.input.setInputProcessor(new FGestureDetector()); + Gdx.input.setInputProcessor(new MainInputProcessor()); openScreen(new HomeScreen()); splashScreen = null; } @@ -152,105 +152,106 @@ public class Forge implements ApplicationListener { shapeRenderer.dispose(); } - private static class FGestureDetector extends GestureDetector { + private static class MainInputProcessor extends FGestureAdapter { private static final ArrayList potentialListeners = new ArrayList(); @Override - public boolean touchUp(float x, float y, int pointer, int button) { - for (FDisplayObject listener : potentialListeners) { - if (listener.touchUp(listener.screenToLocalX(x), listener.screenToLocalY(y))) { - break; - } + public boolean touchDown(int x, int y, int pointer, int button) { + potentialListeners.clear(); + if (currentScreen != null) { //base potential listeners on object containing touch down point + currentScreen.buildTouchListeners(x, y, potentialListeners); } - return super.touchUp(x, y, pointer, button); + return super.touchDown(x, y, pointer, button); } - private FGestureDetector() { - super(new GestureListener() { - @Override - public boolean touchDown(float x, float y, int pointer, int button) { - potentialListeners.clear(); - if (currentScreen != null) { //base potential listeners on object containing touch down point - currentScreen.buildTouchListeners(x, y, potentialListeners); - } - for (FDisplayObject listener : potentialListeners) { - if (listener.touchDown(listener.screenToLocalX(x), listener.screenToLocalY(y))) { - return true; - } - } - return false; + @Override + public boolean press(float x, float y) { + for (FDisplayObject listener : potentialListeners) { + if (listener.press(listener.screenToLocalX(x), listener.screenToLocalY(y))) { + return true; } + } + return false; + } - @Override - public boolean tap(float x, float y, int count, int button) { - for (FDisplayObject listener : potentialListeners) { - if (listener.tap(listener.screenToLocalX(x), listener.screenToLocalY(y), count)) { - return true; - } - } - return false; + @Override + public boolean release(float x, float y) { + for (FDisplayObject listener : potentialListeners) { + if (listener.release(listener.screenToLocalX(x), listener.screenToLocalY(y))) { + return true; } + } + return false; + } - @Override - public boolean longPress(float x, float y) { - for (FDisplayObject listener : potentialListeners) { - if (listener.longPress(listener.screenToLocalX(x), listener.screenToLocalY(y))) { - return true; - } - } - return false; + @Override + public boolean longPress(float x, float y) { + for (FDisplayObject listener : potentialListeners) { + if (listener.longPress(listener.screenToLocalX(x), listener.screenToLocalY(y))) { + return true; } + } + return false; + } - @Override - public boolean fling(float velocityX, float velocityY, int button) { - for (FDisplayObject listener : potentialListeners) { - if (listener.fling(velocityX, velocityY)) { - return true; - } - } - return false; + @Override + public boolean tap(float x, float y, int count) { + for (FDisplayObject listener : potentialListeners) { + if (listener.tap(listener.screenToLocalX(x), listener.screenToLocalY(y), count)) { + return true; } + } + return false; + } - @Override - public boolean pan(float x, float y, float deltaX, float deltaY) { - for (FDisplayObject listener : potentialListeners) { - if (listener.pan(listener.screenToLocalX(x), listener.screenToLocalY(y), deltaX, deltaY)) { - return true; - } - } - return false; + @Override + public boolean fling(float velocityX, float velocityY) { + for (FDisplayObject listener : potentialListeners) { + if (listener.fling(velocityX, velocityY)) { + return true; } + } + return false; + } - @Override - public boolean panStop(float x, float y, int pointer, int button) { - for (FDisplayObject listener : potentialListeners) { - if (listener.panStop(listener.screenToLocalX(x), listener.screenToLocalY(y))) { - return true; - } - } - return false; + @Override + public boolean pan(float x, float y, float deltaX, float deltaY) { + for (FDisplayObject listener : potentialListeners) { + if (listener.pan(listener.screenToLocalX(x), listener.screenToLocalY(y), deltaX, deltaY)) { + return true; } + } + return false; + } - @Override - public boolean zoom(float initialDistance, float distance) { - for (FDisplayObject listener : potentialListeners) { - if (listener.zoom(initialDistance, distance)) { - return true; - } - } - return false; + @Override + public boolean panStop(float x, float y) { + for (FDisplayObject listener : potentialListeners) { + if (listener.panStop(listener.screenToLocalX(x), listener.screenToLocalY(y))) { + return true; } + } + return false; + } - @Override - public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) { - for (FDisplayObject listener : potentialListeners) { - if (listener.pinch(initialPointer1, initialPointer2, pointer1, pointer2)) { - return true; - } - } - return false; + @Override + public boolean zoom(float initialDistance, float distance) { + for (FDisplayObject listener : potentialListeners) { + if (listener.zoom(initialDistance, distance)) { + return true; } - }); + } + return false; + } + + @Override + public boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2) { + for (FDisplayObject listener : potentialListeners) { + if (listener.pinch(initialPointer1, initialPointer2, pointer1, pointer2)) { + return true; + } + } + return false; } } diff --git a/forge-m-base/src/forge/screens/LaunchScreen.java b/forge-m-base/src/forge/screens/LaunchScreen.java index 3d59defe8df..8e15c5d4287 100644 --- a/forge-m-base/src/forge/screens/LaunchScreen.java +++ b/forge-m-base/src/forge/screens/LaunchScreen.java @@ -53,13 +53,13 @@ public abstract class LaunchScreen extends FScreen { } @Override - public final boolean touchDown(float x, float y) { + public final boolean press(float x, float y) { pressed = true; return true; } @Override - public final boolean touchUp(float x, float y) { + public final boolean release(float x, float y) { pressed = false; return true; } diff --git a/forge-m-base/src/forge/toolbox/FButton.java b/forge-m-base/src/forge/toolbox/FButton.java index 5e768ea01ea..e7ae9437479 100644 --- a/forge-m-base/src/forge/toolbox/FButton.java +++ b/forge-m-base/src/forge/toolbox/FButton.java @@ -89,7 +89,7 @@ public class FButton extends FDisplayObject { } @Override - public final boolean touchDown(float x, float y) { + public final boolean press(float x, float y) { if (isToggled()) { return true; } imgL = FSkinImage.BTN_DOWN_LEFT; imgM = FSkinImage.BTN_DOWN_CENTER; @@ -98,7 +98,7 @@ public class FButton extends FDisplayObject { } @Override - public final boolean touchUp(float x, float y) { + public final boolean release(float x, float y) { if (isToggled()) { return true; } resetImg(); return true; diff --git a/forge-m-base/src/forge/toolbox/FDisplayObject.java b/forge-m-base/src/forge/toolbox/FDisplayObject.java index 7b3715e0ab9..0af7de2d8a6 100644 --- a/forge-m-base/src/forge/toolbox/FDisplayObject.java +++ b/forge-m-base/src/forge/toolbox/FDisplayObject.java @@ -98,11 +98,15 @@ public abstract class FDisplayObject { } } - public boolean touchDown(float x, float y) { + public boolean press(float x, float y) { return false; } - public boolean touchUp(float x, float y) { + public boolean longPress(float x, float y) { + return false; + } + + public boolean release(float x, float y) { return false; } @@ -110,10 +114,6 @@ public abstract class FDisplayObject { return false; } - public boolean longPress(float x, float y) { - return tap(x, y, 1); //treat longPress the same as a tap by default - } - public boolean fling(float velocityX, float velocityY) { return false; } diff --git a/forge-m-base/src/forge/toolbox/FGestureAdapter.java b/forge-m-base/src/forge/toolbox/FGestureAdapter.java new file mode 100644 index 00000000000..1bd2184d814 --- /dev/null +++ b/forge-m-base/src/forge/toolbox/FGestureAdapter.java @@ -0,0 +1,304 @@ +package forge.toolbox; + +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.InputAdapter; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.TimeUtils; +import com.badlogic.gdx.utils.Timer; +import com.badlogic.gdx.utils.Timer.Task; + +public abstract class FGestureAdapter extends InputAdapter { + public abstract boolean press(float x, float y); + public abstract boolean longPress(float x, float y); + public abstract boolean release(float x, float y); + public abstract boolean tap(float x, float y, int count); + public abstract boolean fling(float velocityX, float velocityY); + public abstract boolean pan(float x, float y, float deltaX, float deltaY); + public abstract boolean panStop(float x, float y); + public abstract boolean zoom(float initialDistance, float distance); + public abstract boolean pinch(Vector2 initialPointer1, Vector2 initialPointer2, Vector2 pointer1, Vector2 pointer2); + + private float tapSquareSize, pressDelay, longPressDelay, lastTapX, lastTapY, tapSquareCenterX, tapSquareCenterY; + private long tapCountInterval, flingDelay, lastTapTime, gestureStartTime; + private int tapCount, lastTapButton, lastTapPointer; + private boolean inTapSquare, pressed, longPressed, pinching, panning; + + private final VelocityTracker tracker = new VelocityTracker(); + Vector2 pointer1 = new Vector2(); + private final Vector2 pointer2 = new Vector2(); + private final Vector2 initialPointer1 = new Vector2(); + private final Vector2 initialPointer2 = new Vector2(); + + private final Task pressTask = new Task() { + @Override + public void run () { + if (!pressed) { + press(pointer1.x, pointer1.y); + pressed = true; + } + } + }; + private final Task longPressTask = new Task() { + @Override + public void run () { + if (!longPressed) { + longPress(pointer1.x, pointer1.y); + longPressed = true; + } + } + }; + + public FGestureAdapter() { + this(20f, 0.4f, 0.1f, 1.1f, 0.15f); + } + + /** @param tapSquareSize0 half width in pixels of the square around an initial touch event + * @param tapCountInterval0 time in seconds that must pass for two touch down/up sequences to be detected as consecutive taps. + * @param pressDelay0 time in seconds that must pass for a press event to be fired. + * @param longPressDelay0 time in seconds that must pass for a long press event to be fired. + * @param flingDelay0 time in seconds the finger must have been dragged for a fling event to be fired. */ + public FGestureAdapter(float tapSquareSize0, float tapCountInterval0, float pressDelay0, float longPressDelay0, float flingDelay0) { + tapSquareSize = tapSquareSize0; + tapCountInterval = (long)(tapCountInterval0 * 1000000000l); + pressDelay = pressDelay0; + longPressDelay = longPressDelay0; + flingDelay = (long)(flingDelay0 * 1000000000l); + } + + @Override + public boolean touchDown(int x, int y, int pointer, int button) { + return touchDown((float)x, (float)y, pointer, button); + } + private boolean touchDown(float x, float y, int pointer, int button) { + if (pointer > 1) { return false; } + + if (pointer == 0) { + pointer1.set(x, y); + gestureStartTime = Gdx.input.getCurrentEventTime(); + tracker.start(x, y, gestureStartTime); + if (Gdx.input.isTouched(1)) { + // Start pinch. + inTapSquare = false; + pinching = true; + initialPointer1.set(pointer1); + initialPointer2.set(pointer2); + releasePress(x, y); + } + else { + // Normal touch down. + inTapSquare = true; + pinching = false; + pressed = false; + tapSquareCenterX = x; + tapSquareCenterY = y; + if (!pressTask.isScheduled()) { + Timer.schedule(pressTask, pressDelay); + } + if (!longPressTask.isScheduled()) { + Timer.schedule(longPressTask, longPressDelay); + } + } + } + else { + // Start pinch. + pointer2.set(x, y); + inTapSquare = false; + pinching = true; + initialPointer1.set(pointer1); + initialPointer2.set(pointer2); + releasePress(pointer1.x, pointer1.y); + } + return true; + } + + @Override + public boolean touchDragged(int x, int y, int pointer) { + return touchDragged((float)x, (float)y, pointer); + } + private boolean touchDragged(float x, float y, int pointer) { + if (pointer > 1) { return false; } + + if (pointer == 0) { + pointer1.set(x, y); + } + else { + pointer2.set(x, y); + } + + // handle pinch zoom + if (pinching) { + boolean result = pinch(initialPointer1, initialPointer2, pointer1, pointer2); + return zoom(initialPointer1.dst(initialPointer2), pointer1.dst(pointer2)) || result; + } + + // update tracker + tracker.update(x, y, Gdx.input.getCurrentEventTime()); + + // check if we are still tapping. + if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) { + releasePress(x, y); + inTapSquare = false; + } + + // if we have left the tap square, we are panning + if (!inTapSquare) { + panning = true; + return pan(x, y, tracker.deltaX, tracker.deltaY); + } + + return false; + } + + @Override + public boolean touchUp(int x, int y, int pointer, int button) { + return touchUp((float)x, (float)y, pointer, button); + } + private boolean touchUp(float x, float y, int pointer, int button) { + if (pointer > 1) { return false; } + + // check if we are still tapping. + if (inTapSquare && !isWithinTapSquare(x, y, tapSquareCenterX, tapSquareCenterY)) { + inTapSquare = false; + } + + boolean wasPanning = panning; + panning = false; + + releasePress(x, y); + + if (inTapSquare) { + // handle taps + if (lastTapButton != button || lastTapPointer != pointer + || TimeUtils.nanoTime() - lastTapTime > tapCountInterval + || !isWithinTapSquare(x, y, lastTapX, lastTapY)) { + tapCount = 0; + } + tapCount++; + lastTapTime = TimeUtils.nanoTime(); + lastTapX = x; + lastTapY = y; + lastTapButton = button; + lastTapPointer = pointer; + gestureStartTime = 0; + return tap(x, y, tapCount); + } + + if (pinching) { + // handle pinch end + pinching = false; + panning = true; + // we are in pan mode again, reset velocity tracker + if (pointer == 0) { + // first pointer has lifted off, set up panning to use the second pointer... + tracker.start(pointer2.x, pointer2.y, Gdx.input.getCurrentEventTime()); + } + else { + // second pointer has lifted off, set up panning to use the first pointer... + tracker.start(pointer1.x, pointer1.y, Gdx.input.getCurrentEventTime()); + } + return false; + } + + // handle no longer panning + boolean handled = false; + if (wasPanning && !panning) { + handled = panStop(x, y); + } + + // handle fling + gestureStartTime = 0; + long time = Gdx.input.getCurrentEventTime(); + if (time - tracker.lastTime < flingDelay) { + tracker.update(x, y, time); + handled = fling(tracker.getVelocityX(), tracker.getVelocityY()) || handled; + } + return handled; + } + + private void releasePress(float x, float y) { + pressTask.cancel(); + longPressTask.cancel(); + longPressed = false; + if (pressed) { + pressed = false; + release(x, y); + } + } + + private boolean isWithinTapSquare(float x, float y, float centerX, float centerY) { + return Math.abs(x - centerX) < tapSquareSize && Math.abs(y - centerY) < tapSquareSize; + } + + private static class VelocityTracker { + int sampleSize = 10; + float lastX, lastY; + float deltaX, deltaY; + long lastTime; + int numSamples; + float[] meanX = new float[sampleSize]; + float[] meanY = new float[sampleSize]; + long[] meanTime = new long[sampleSize]; + + public void start(float x, float y, long timeStamp) { + lastX = x; + lastY = y; + deltaX = 0; + deltaY = 0; + numSamples = 0; + for (int i = 0; i < sampleSize; i++) { + meanX[i] = 0; + meanY[i] = 0; + meanTime[i] = 0; + } + lastTime = timeStamp; + } + + public void update(float x, float y, long timeStamp) { + long currTime = timeStamp; + deltaX = x - lastX; + deltaY = y - lastY; + lastX = x; + lastY = y; + long deltaTime = currTime - lastTime; + lastTime = currTime; + int index = numSamples % sampleSize; + meanX[index] = deltaX; + meanY[index] = deltaY; + meanTime[index] = deltaTime; + numSamples++; + } + + public float getVelocityX() { + float meanX = getAverage(this.meanX, numSamples); + float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f; + if (meanTime == 0) return 0; + return meanX / meanTime; + } + + public float getVelocityY() { + float meanY = getAverage(this.meanY, numSamples); + float meanTime = getAverage(this.meanTime, numSamples) / 1000000000.0f; + if (meanTime == 0) return 0; + return meanY / meanTime; + } + + private float getAverage(float[] values, int numSamples) { + numSamples = Math.min(sampleSize, numSamples); + float sum = 0; + for (int i = 0; i < numSamples; i++) { + sum += values[i]; + } + return sum / numSamples; + } + + private long getAverage(long[] values, int numSamples) { + numSamples = Math.min(sampleSize, numSamples); + long sum = 0; + for (int i = 0; i < numSamples; i++) { + sum += values[i]; + } + if (numSamples == 0) return 0; + return sum / numSamples; + } + } +} diff --git a/forge-m-base/src/forge/toolbox/FLabel.java b/forge-m-base/src/forge/toolbox/FLabel.java index 9d54ad5d019..1f9000ddb1d 100644 --- a/forge-m-base/src/forge/toolbox/FLabel.java +++ b/forge-m-base/src/forge/toolbox/FLabel.java @@ -116,7 +116,7 @@ public class FLabel extends FDisplayObject { } @Override - public final boolean touchDown(float x, float y) { + public final boolean press(float x, float y) { if (opaque || selectable) { pressed = true; return true; @@ -125,7 +125,7 @@ public class FLabel extends FDisplayObject { } @Override - public final boolean touchUp(float x, float y) { + public final boolean release(float x, float y) { if (pressed) { pressed = false; return true; diff --git a/forge-m-base/src/forge/toolbox/FList.java b/forge-m-base/src/forge/toolbox/FList.java index b671516e767..410c6d09c75 100644 --- a/forge-m-base/src/forge/toolbox/FList.java +++ b/forge-m-base/src/forge/toolbox/FList.java @@ -160,12 +160,12 @@ public class FList extends FScrollPane { value = value0; } - public boolean touchDown(float x, float y) { + public boolean press(float x, float y) { pressed = true; return true; } - public boolean touchUp(float x, float y) { + public boolean release(float x, float y) { pressed = false; return true; }