From eb5309c0505e9a92321a977046153eb30e67f401 Mon Sep 17 00:00:00 2001 From: andreas Date: Fri, 4 Sep 2026 21:57:01 +1000 Subject: [PATCH] Improve handling of android touch events when virtual joystick is being used. Previously it was not possible to move using the joystick AND rotate the camera using the touch screen at the same time. You could do one or the other at any one time. This change fixes that so the user can move forward/back/left/right AND rotate the camera at the same time; just like using a mouse and keyboard. --- .../input/android/AndroidInputHandler.java | 38 +++++++- .../jme3/input/android/AndroidJoyInput.java | 14 +++ .../jme3/input/android/AndroidTouchInput.java | 89 +++++++++++++++++-- .../jme3/input/virtual/VirtualJoystick.java | 24 +++++ 4 files changed, 152 insertions(+), 13 deletions(-) diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java index 6db3c95c6b..81058ed2c4 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidInputHandler.java @@ -63,6 +63,11 @@ public class AndroidInputHandler implements View.OnTouchListener, protected AndroidTouchInput touchInput; protected AndroidJoyInput joyInput; protected MouseInput mouseInput; + /** + * Pointer ids owned by the on-screen virtual joystick for the duration of the MotionEvent + * being dispatched - see {@link #isPointerCapturedByJoystick(int)}. + */ + private long joystickPointerMask; public AndroidInputHandler() { touchInput = new AndroidTouchInput(this); @@ -208,17 +213,42 @@ public boolean onTouch(View view, MotionEvent event) { // logger.log(Level.INFO, "onTouch source: {0}, isTouch: {1}", // new Object[]{source, isTouch}); - if (isTouch && joyInput != null && joyInput.onTouch(event)) { - return true; + boolean joyConsumed = false; + joystickPointerMask = 0L; + if (isTouch && joyInput != null) { + // The union of the captures before and after the event, so that the pointer a + // DOWN captures (only in the "after" set) and the pointer an UP releases (only + // in the "before" set) are both hidden from touchInput below. + long capturedBefore = joyInput.getCapturedPointerMask(); + joyConsumed = joyInput.onTouch(event); + joystickPointerMask = capturedBefore | joyInput.getCapturedPointerMask(); } if (isTouch && touchInput != null) { - // send the event to the touch processor + // The virtual joystick doesn't get to swallow the whole MotionEvent: it only owns + // the pointers it has captured, and touchInput skips exactly those. Dropping the + // event outright would mean a finger resting on the on-screen stick blocked every + // other finger from being reported at all - so no looking around while moving, and + // a jump in accumulated drag as soon as the stick was released. consumed = touchInput.onTouch(event); } - return consumed; + return consumed || joyConsumed; + + } + /** + * Returns whether the given pointer is driving an on-screen virtual joystick control in + * the MotionEvent currently being dispatched, and so must not also be reported as a touch + * (or emulated mouse) event. Only meaningful while {@link #onTouch(View, MotionEvent)} is + * on the stack. + * + * @param pointerId the Android pointer id to test + * @return true if the virtual joystick owns this pointer + */ + public boolean isPointerCapturedByJoystick(int pointerId) { + return pointerId >= 0 && pointerId < Long.SIZE + && (joystickPointerMask & (1L << pointerId)) != 0; } @Override diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java index ad6c1388ba..eb75bf7b37 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidJoyInput.java @@ -225,6 +225,17 @@ public Joystick[] loadJoysticks(InputManager inputManager) { return joystickList.toArray( new Joystick[joystickList.size()] ); } + /** + * Returns a bit mask of the pointer ids currently captured by the on-screen virtual + * joystick - see {@link VirtualJoystick#getCapturedPointerMask()}. + * + * @return the captured pointer ids as a bit mask, 0 if there is no virtual joystick + */ + public long getCapturedPointerMask() { + VirtualJoystick joystick = virtualJoystick; + return joystick == null ? 0L : joystick.getCapturedPointerMask(); + } + public boolean onTouch(MotionEvent event) { VirtualJoystick joystick = virtualJoystick; if (joystick == null || inputHandler.getView() == null) { @@ -240,6 +251,9 @@ public boolean onTouch(MotionEvent event) { switch (action) { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: + // Touch is back in use, so a keyboard/gamepad seen earlier no longer + // justifies keeping the AUTO-mode virtual joystick suppressed. + keyboardSuppressedAutoJoystick = false; consumed = joystick.onPointerDown(event.getPointerId(pointerIndex), toJmeX(event.getX(pointerIndex)), toJmeY(event.getY(pointerIndex)), time); break; diff --git a/jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java b/jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java index 47e687dc52..13796c48cf 100644 --- a/jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java +++ b/jme3-android/src/main/java/com/jme3/input/android/AndroidTouchInput.java @@ -170,13 +170,16 @@ public boolean onTouch(MotionEvent event) { float jmeX; float jmeY; - numPointers = event.getPointerCount(); + numPointers = countReportedPointers(event); // final int historySize = event.getHistorySize(); //final int pointerCount = event.getPointerCount(); switch (getAction(event)) { case MotionEvent.ACTION_POINTER_DOWN: case MotionEvent.ACTION_DOWN: + if (isJoystickPointer(pointerId)) { + break; + } jmeX = getJmeX(event.getX(pointerIndex)); jmeY = invertY(getJmeY(event.getY(pointerIndex))); touch = getFreeTouchEvent(); @@ -193,9 +196,19 @@ public boolean onTouch(MotionEvent event) { bWasHandled = true; break; - case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_CANCEL: + // A cancelled gesture ends every pointer at once, not just the one the event + // names - which, with the virtual joystick owning some of them, may well be a + // pointer that was never reported here to begin with. + bWasHandled = releaseAllPointers(event); + break; + case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: + if (lastPositions.remove(pointerId) == null) { + // Never reported as DOWN - eg. it went to the virtual joystick instead - + // so releasing it here would be an UP with no matching press. + break; + } jmeX = getJmeX(event.getX(pointerIndex)); jmeY = invertY(getJmeY(event.getY(pointerIndex))); touch = getFreeTouchEvent(); @@ -203,7 +216,6 @@ public boolean onTouch(MotionEvent event) { touch.setPointerId(pointerId); touch.setTime(event.getEventTime()); touch.setPressure(event.getPressure(pointerIndex)); - lastPositions.remove(pointerId); addEvent(touch); addEvent(generateMouseEvent(touch)); @@ -213,6 +225,9 @@ public boolean onTouch(MotionEvent event) { case MotionEvent.ACTION_MOVE: // Convert all pointers into events for (int p = 0; p < event.getPointerCount(); p++) { + if (isJoystickPointer(event.getPointerId(p))) { + continue; + } jmeX = getJmeX(event.getX(p)); jmeY = invertY(getJmeY(event.getY(p))); lastPos = lastPositions.get(event.getPointerId(p)); @@ -243,17 +258,73 @@ public boolean onTouch(MotionEvent event) { } - // Try to detect gestures - if (gestureDetector != null) { - gestureDetector.onTouchEvent(event); - } - if (scaleDetector != null) { - scaleDetector.onTouchEvent(event); + // Try to detect gestures - but not for events the virtual joystick has a hand in, + // where a gesture spanning both the stick and another finger would be meaningless + // (and the detectors have no way to be told to ignore individual pointers). + if (numPointers == event.getPointerCount()) { + if (gestureDetector != null) { + gestureDetector.onTouchEvent(event); + } + if (scaleDetector != null) { + scaleDetector.onTouchEvent(event); + } } return bWasHandled; } + /** + * Emits an UP for every pointer currently being tracked, used when Android cancels the + * whole gesture. + * + * @param event the cancelling MotionEvent + * @return true if at least one pointer was released + */ + private boolean releaseAllPointers(MotionEvent event) { + boolean released = false; + for (int p = 0; p < event.getPointerCount(); p++) { + int pointerId = event.getPointerId(p); + if (lastPositions.remove(pointerId) == null) { + continue; + } + TouchEvent touch = getFreeTouchEvent(); + touch.set(TouchEvent.Type.UP, getJmeX(event.getX(p)), invertY(getJmeY(event.getY(p))), 0, 0); + touch.setPointerId(pointerId); + touch.setTime(event.getEventTime()); + touch.setPressure(event.getPressure(p)); + + addEvent(touch); + addEvent(generateMouseEvent(touch)); + + released = true; + } + return released; + } + + /** + * Counts the pointers in the event that this class actually reports, ie. those not being + * used to drive an on-screen virtual joystick control. Those are hidden from touch and + * mouse emulation entirely, so they mustn't count towards the multi-touch check in + * {@link #generateMouseEvent(TouchEvent)} either - otherwise holding the on-screen stick + * would suppress the emulated mouse events of the finger looking around. + * + * @param event the MotionEvent being dispatched + * @return the number of pointers reported by this class + */ + private int countReportedPointers(MotionEvent event) { + int count = 0; + for (int p = 0; p < event.getPointerCount(); p++) { + if (!isJoystickPointer(event.getPointerId(p))) { + count++; + } + } + return count; + } + + private boolean isJoystickPointer(int pointerId) { + return androidInput != null && androidInput.isPointerCapturedByJoystick(pointerId); + } + // TODO: Ring Buffer for mouse events? public InputEvent generateMouseEvent(TouchEvent event) { InputEvent inputEvent = null; diff --git a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java index e364ef9bd7..3561979b0d 100644 --- a/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java +++ b/jme3-core/src/main/java/com/jme3/input/virtual/VirtualJoystick.java @@ -245,6 +245,30 @@ public boolean onPointerUp(int pointerId, float x, float y, long time) { } } + /** + * Returns a bit mask of the pointer ids currently captured by an on-screen control, + * bit n being set when pointer id n is captured. + * + *

Backends use this to tell apart the pointers this joystick owns from the ones that + * should still reach the rest of the input pipeline, so that a finger on the on-screen + * stick doesn't stop a second finger from being reported as an ordinary touch. Pointer + * ids of 64 or above cannot be represented and are omitted; no platform jME supports + * produces them. + * + * @return the captured pointer ids as a bit mask, 0 if none are captured + */ + public long getCapturedPointerMask() { + synchronized (inputLock) { + long mask = 0L; + for (Integer pointerId : captures.keySet()) { + if (pointerId >= 0 && pointerId < Long.SIZE) { + mask |= 1L << pointerId; + } + } + return mask; + } + } + /** * Releases all active pointer captures. *