Feat[touch_input]: unified gesture system, rclick/lclick/hotbar drop gestures reimplemented

This commit is contained in:
artdeell
2024-01-19 11:04:14 +03:00
committed by Maksim Belov
parent 959d4c4edd
commit 7ab3510d4d
7 changed files with 238 additions and 89 deletions

View File

@@ -28,7 +28,7 @@ import androidx.annotation.RequiresApi;
import net.kdt.pojavlaunch.customcontrols.ControlLayout;
import net.kdt.pojavlaunch.customcontrols.gamepad.Gamepad;
import net.kdt.pojavlaunch.customcontrols.mouse.InGUIEventProcessor;
import net.kdt.pojavlaunch.customcontrols.mouse.IngameEventProcessor;
import net.kdt.pojavlaunch.customcontrols.mouse.InGameEventProcessor;
import net.kdt.pojavlaunch.customcontrols.mouse.TouchEventProcessor;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.JREUtils;
@@ -75,7 +75,7 @@ public class MinecraftGLSurface extends View implements GrabListener {
/* View holding the surface, either a SurfaceView or a TextureView */
View mSurface;
private final TouchEventProcessor mIngameProcessor = new IngameEventProcessor(mScaleFactor, mSensitivityFactor);
private final TouchEventProcessor mIngameProcessor = new InGameEventProcessor(mScaleFactor, mSensitivityFactor);
private final TouchEventProcessor mInGUIProcessor = new InGUIEventProcessor(mScaleFactor);
private boolean mLastGrabState = false;

View File

@@ -0,0 +1,28 @@
package net.kdt.pojavlaunch.customcontrols.mouse;
import static org.lwjgl.glfw.CallbackBridge.sendKeyPress;
import android.os.Handler;
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
public class DropGesture extends ValidatorGesture {
public DropGesture(Handler mHandler) {
super(mHandler, 250);
}
boolean mGuiBarHit;
public void submit(boolean hasGuiBarHit) {
submit();
mGuiBarHit = hasGuiBarHit;
}
@Override
public boolean checkAndTrigger() {
if(mGuiBarHit) sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_Q);
return true;
}
@Override
public void onGestureCancelled(boolean isSwitching) {}
}

View File

@@ -21,7 +21,7 @@ public class InGUIEventProcessor implements TouchEventProcessor {
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mTracker.startTracking(motionEvent);
sendTouchCoords(motionEvent.getX(), motionEvent.getY());
sendTouchCoordinates(motionEvent.getX(), motionEvent.getY());
enableMouse();
break;
case MotionEvent.ACTION_MOVE:
@@ -30,7 +30,7 @@ public class InGUIEventProcessor implements TouchEventProcessor {
float mainPointerX = motionEvent.getX(pointerIndex);
float mainPointerY = motionEvent.getY(pointerIndex);
if(pointerCount == 1 || LauncherPreferences.PREF_DISABLE_GESTURES) {
sendTouchCoords(mainPointerX, mainPointerY);
sendTouchCoordinates(mainPointerX, mainPointerY);
if(!mIsMouseDown) enableMouse();
}else {
float[] motionVector = mTracker.getMotionVector();
@@ -45,7 +45,7 @@ public class InGUIEventProcessor implements TouchEventProcessor {
return true;
}
private void sendTouchCoords(float x, float y) {
private void sendTouchCoordinates(float x, float y) {
CallbackBridge.mouseX = x * mScaleFactor;
CallbackBridge.mouseY = y * mScaleFactor;
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);

View File

@@ -2,50 +2,37 @@ package net.kdt.pojavlaunch.customcontrols.mouse;
import static net.kdt.pojavlaunch.utils.MCOptionUtils.getMcScale;
import static org.lwjgl.glfw.CallbackBridge.sendKeyPress;
import static org.lwjgl.glfw.CallbackBridge.sendMouseButton;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.TapDetector;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.MCOptionUtils;
import net.kdt.pojavlaunch.utils.MathUtils;
import org.lwjgl.glfw.CallbackBridge;
public class IngameEventProcessor implements TouchEventProcessor {
public class InGameEventProcessor implements TouchEventProcessor {
private int mGuiScale;
@SuppressWarnings("FieldCanBeLocal") // it can't, otherwise the weak reference will disappear
private final MCOptionUtils.MCOptionListener mGuiScaleListener = () -> mGuiScale = getMcScale();
public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9);
public static final int MSG_LEFT_MOUSE_BUTTON_CHECK = 1028;
public static final int MSG_DROP_ITEM_BUTTON_CHECK = 1029;
private final Handler mGestureHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(@NonNull Message msg) {IngameEventProcessor.this.handleMessage(msg);}
};
private final Handler mGestureHandler = new Handler(Looper.getMainLooper());
private static final int[] HOTBAR_KEYS = {
LwjglGlfwKeycode.GLFW_KEY_1, LwjglGlfwKeycode.GLFW_KEY_2, LwjglGlfwKeycode.GLFW_KEY_3,
LwjglGlfwKeycode.GLFW_KEY_4, LwjglGlfwKeycode.GLFW_KEY_5, LwjglGlfwKeycode.GLFW_KEY_6,
LwjglGlfwKeycode.GLFW_KEY_7, LwjglGlfwKeycode.GLFW_KEY_8, LwjglGlfwKeycode.GLFW_KEY_9};
private float mGestureStartX, mGestureStartY;
private boolean mHasPendingLongpressGesture;
private boolean mHasPendingDropGesture;
private boolean mGestureTriggered;
private int mLastHudKey;
private final float mScaleFactor;
private final double mSensitivity;
private final PointerTracker mTracker = new PointerTracker();
private final LeftClickGesture mLeftClickGesture = new LeftClickGesture(mGestureHandler);
private final RightClickGesture mRightClickGesture = new RightClickGesture(mGestureHandler);
private final DropGesture mDropGesture = new DropGesture(mGestureHandler);
private final TapDetector mDoubleTapDetector = new TapDetector(2, TapDetector.DETECTION_METHOD_DOWN);
public IngameEventProcessor(float scaleFactor, double sensitivity) {
public InGameEventProcessor(float scaleFactor, double sensitivity) {
MCOptionUtils.addMCOptionListener(mGuiScaleListener);
mScaleFactor = scaleFactor;
mSensitivity = sensitivity;
@@ -53,9 +40,12 @@ public class IngameEventProcessor implements TouchEventProcessor {
@Override
public boolean processTouchEvent(MotionEvent motionEvent) {
boolean hasDoubleTapped = mDoubleTapDetector.onTouchEvent(motionEvent);
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mTracker.startTracking(motionEvent);
if(LauncherPreferences.PREF_DISABLE_GESTURES) break;
checkGestures(handleGuiBar(motionEvent), hasDoubleTapped);
break;
case MotionEvent.ACTION_MOVE:
mTracker.trackEvent(motionEvent);
@@ -65,24 +55,35 @@ public class IngameEventProcessor implements TouchEventProcessor {
CallbackBridge.sendCursorPos(CallbackBridge.mouseX, CallbackBridge.mouseY);
boolean hasGuiBarHit = handleGuiBar(motionEvent);
if(LauncherPreferences.PREF_DISABLE_GESTURES) break;
checkLongpressGesture();
checkGuiBarGesture(hasGuiBarHit);
checkGestures(hasGuiBarHit, hasDoubleTapped);
break;
case MotionEvent.ACTION_UP:
cancelGestures();
case MotionEvent.ACTION_CANCEL:
cancelGestures(false);
}
return true;
}
@Override
public void cancelPendingActions() {
cancelGestures();
cancelGestures(true);
}
private void cancelGestures() {
cancelLongpressGesture();
cancelDropGesture();
if(mGestureTriggered) sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, false);
private void checkGestures(boolean hasGuiBarHit, boolean hasDoubleTapped) {
if(!hasGuiBarHit) {
mLeftClickGesture.inputEvent();
mRightClickGesture.inputEvent();
}
mDropGesture.submit(hasGuiBarHit);
if(hasGuiBarHit && hasDoubleTapped && !LauncherPreferences.PREF_DISABLE_SWAP_HAND) {
CallbackBridge.sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_F);
}
}
private void cancelGestures(boolean isSwitching) {
mLeftClickGesture.cancel(isSwitching);
mRightClickGesture.cancel(isSwitching);
mDropGesture.cancel(isSwitching);
}
private boolean handleGuiBar(MotionEvent motionEvent) {
@@ -101,51 +102,15 @@ public class IngameEventProcessor implements TouchEventProcessor {
return hasGuiBarHit;
}
private void checkGuiBarGesture(boolean hasGuiBarHit) {
if(hasGuiBarHit && !mHasPendingDropGesture) submitDropGesture();
if(!hasGuiBarHit) cancelDropGesture();
}
private void checkLongpressGesture() {
if(mHasPendingLongpressGesture &&
MathUtils.dist(CallbackBridge.mouseX, CallbackBridge.mouseY, mGestureStartX, mGestureStartY)
>= FINGER_STILL_THRESHOLD) {
cancelLongpressGesture();
}
if(!mHasPendingLongpressGesture) submitLongpressGesture();
}
private void cancelLongpressGesture() {
mGestureHandler.removeMessages(MSG_LEFT_MOUSE_BUTTON_CHECK);
mHasPendingLongpressGesture = false;
}
private void submitLongpressGesture() {
mGestureStartX = CallbackBridge.mouseX;
mGestureStartY = CallbackBridge.mouseY;
mGestureHandler.sendEmptyMessageDelayed(MSG_LEFT_MOUSE_BUTTON_CHECK, LauncherPreferences.PREF_LONGPRESS_TRIGGER);
mHasPendingLongpressGesture = true;
}
private void cancelDropGesture() {
mGestureHandler.removeMessages(MSG_DROP_ITEM_BUTTON_CHECK);
mHasPendingDropGesture = false;
}
private void submitDropGesture() {
mGestureHandler.sendEmptyMessageDelayed(MSG_DROP_ITEM_BUTTON_CHECK, 350);
mHasPendingDropGesture = true;
}
/** @return the hotbar key, given the position. -1 if no key are pressed */
public int handleGuiBar(int x, int y) {
if (!CallbackBridge.isGrabbing()) return -1;
int barHeight = mcscale(20);
int barHeight = mcScale(20);
int barY = CallbackBridge.physicalHeight - barHeight;
if(y < barY) return -1;
int barWidth = mcscale(180);
int barWidth = mcScale(180);
int barX = (CallbackBridge.physicalWidth / 2) - (barWidth / 2);
if(x < barX || x >= barX + barWidth) return -1;
@@ -153,23 +118,7 @@ public class IngameEventProcessor implements TouchEventProcessor {
}
/** Return the size, given the UI scale size */
private int mcscale(int input) {
private int mcScale(int input) {
return (int)((mGuiScale * input)/ mScaleFactor);
}
private void handleMessage(Message message) {
switch (message.what) {
case MSG_LEFT_MOUSE_BUTTON_CHECK:
float x = CallbackBridge.mouseX;
float y = CallbackBridge.mouseY;
if (MathUtils.dist(x, y, mGestureStartX, mGestureStartY) < FINGER_STILL_THRESHOLD) {
sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true);
mGestureTriggered = true;
}
return;
case MSG_DROP_ITEM_BUTTON_CHECK:
sendKeyPress(LwjglGlfwKeycode.GLFW_KEY_Q);
mGestureHandler.sendEmptyMessageDelayed(MSG_DROP_ITEM_BUTTON_CHECK, 600);
}
}
}

View File

@@ -0,0 +1,55 @@
package net.kdt.pojavlaunch.customcontrols.mouse;
import static org.lwjgl.glfw.CallbackBridge.sendMouseButton;
import android.os.Handler;
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.MathUtils;
import org.lwjgl.glfw.CallbackBridge;
public class LeftClickGesture extends ValidatorGesture {
public static final int FINGER_STILL_THRESHOLD = (int) Tools.dpToPx(9);
private float mGestureStartX, mGestureStartY;
public LeftClickGesture(Handler handler) {
super(handler, LauncherPreferences.PREF_LONGPRESS_TRIGGER);
}
public final void inputEvent() {
if(submit()) {
mGestureStartX = CallbackBridge.mouseX;
mGestureStartY = CallbackBridge.mouseY;
}
}
@Override
public boolean checkAndTrigger() {
boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY);
if(fingerStill) sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, true);
return fingerStill;
}
@Override
public void onGestureCancelled(boolean isSwitching) {
sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_LEFT, false);
}
/**
* Check if the finger is still when compared to mouseX/mouseY in CallbackBridge.
* @param startX the starting X of the gesture
* @param startY the starting Y of the gesture
* @return whether the finger's position counts as "still" or not
*/
public static boolean isFingerStill(float startX, float startY) {
return MathUtils.dist(
CallbackBridge.mouseX,
CallbackBridge.mouseY,
startX,
startY
) <= FINGER_STILL_THRESHOLD;
}
}

View File

@@ -0,0 +1,46 @@
package net.kdt.pojavlaunch.customcontrols.mouse;
import android.os.Handler;
import net.kdt.pojavlaunch.LwjglGlfwKeycode;
import org.lwjgl.glfw.CallbackBridge;
public class RightClickGesture extends ValidatorGesture{
private boolean mGestureEnabled = true;
private boolean mGestureValid = true;
private float mGestureStartX, mGestureStartY;
public RightClickGesture(Handler mHandler) {
super(mHandler, 150);
}
public final void inputEvent() {
if(!mGestureEnabled) return;
if(submit()) {
mGestureStartX = CallbackBridge.mouseX;
mGestureStartY = CallbackBridge.mouseY;
mGestureEnabled = false;
mGestureValid = true;
}
}
@Override
public boolean checkAndTrigger() {
// If the validate() method was called, it means that the user held on for too long. The cancellation should be ignored.
mGestureValid = false;
// Never call onGestureCancelled. This way we will be able to reserve that only for when
// the gesture is stopped in the code (when the user lets go of the screen or the tap was
// cancelled by turning on the grab)
return true;
}
@Override
public void onGestureCancelled(boolean isSwitching) {
mGestureEnabled = true;
if(!mGestureValid || isSwitching) return;
boolean fingerStill = LeftClickGesture.isFingerStill(mGestureStartX, mGestureStartY);
if(!fingerStill) return;
CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, true);
CallbackBridge.sendMouseButton(LwjglGlfwKeycode.GLFW_MOUSE_BUTTON_RIGHT, false);
}
}

View File

@@ -0,0 +1,71 @@
package net.kdt.pojavlaunch.customcontrols.mouse;
import android.os.Handler;
/**
* This class implements an abstract "validator gesture", meant as a base for implementation of
* more complex gestures with pinger position tracking and such.
*/
public abstract class ValidatorGesture implements Runnable{
private final Handler mHandler;
private boolean mGestureActive;
private final int mRequiredDuration;
/**
* @param mHandler the Handler that will be used for calling back the checkAndTrigger() method.
* This Handler should run on the same thread as the callee of submit()/cancel()
* @param mRequiredDuration the duration after which the class will call checkAndTrigger().
*/
public ValidatorGesture(Handler mHandler, int mRequiredDuration) {
this.mHandler = mHandler;
this.mRequiredDuration = mRequiredDuration;
}
/**
* Submit the gesture, starting the timer and marking this gesture as "active".
* If the gesture was already active, this call will be ignored
* @return true if the gesture was submitted, false if the call was ignored
*/
public final boolean submit() {
if(mGestureActive) return false;
mHandler.postDelayed(this, mRequiredDuration);
mGestureActive = true;
return true;
}
/**
* Cancel the gesture, stopping the timer and marking this gesture as "inactive".
* If the gesture was already inactive, this call will be ignored.
* @param isSwitching true if this gesture was cancelled due to user interaction (the user let go of the finger)
* false if this gesture is cancelled due a request from the programmer or the OS.
* Note that returning false from checkAndTrigger() counts as user interaction.
*/
public final void cancel(boolean isSwitching) {
if(!mGestureActive) return;
mHandler.removeCallbacks(this);
onGestureCancelled(isSwitching);
mGestureActive = false;
}
@Override
public final void run() {
if(checkAndTrigger()) return;
mGestureActive = false;
onGestureCancelled(false);
}
/**
* This method will be called after mRequiredDuration milliseconds, if the gesture was not cancelled.
* @return false if you want to mark this gesture as "inactive"
* true otherwise
*/
public abstract boolean checkAndTrigger();
/**
* This method will be called if the gesture was cancelled using the cancel() method or by returning false
* from checkAndTrigger().
* @param isSwitching true if this gesture was cancelled due to user interaction (the user let go of the finger)
* false if this gesture is cancelled due a request from the programmer or the OS.
*/
public abstract void onGestureCancelled(boolean isSwitching);
}