From 2f6e8b575683550789a44d44c37a0e2711c18280 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 11 Aug 2025 01:14:32 +0100 Subject: [PATCH] android: Reimplement multitouch code correctly --- .../citra/citra_emu/overlay/InputOverlay.kt | 327 ++++++++++-------- .../overlay/InputOverlayDrawableButton.kt | 16 +- .../overlay/InputOverlayDrawableDpad.kt | 3 +- .../overlay/InputOverlayDrawableJoystick.kt | 3 +- 4 files changed, 189 insertions(+), 160 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt index 23dcddc78..9828dd046 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt @@ -97,169 +97,196 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex return onTouchWhileEditing(event) } - var hasActiveButtons = false - val pointerIndex = event.actionIndex - val pointerId = event.getPointerId(pointerIndex) - for (button in overlayButtons) { - if (button.trackId == pointerId) { - hasActiveButtons = true - break - } - } - var hasActiveDpad = false - if (!hasActiveButtons) { - for (dpad in overlayDpads) { - if (dpad.trackId == pointerId) { - hasActiveDpad = true - break - } - } - } - - var hasActiveJoystick = false - if(!hasActiveButtons && !hasActiveDpad){ - for (joystick in overlayJoysticks) { - if (joystick.trackId == pointerId) { - hasActiveJoystick = true - break - } - } - } - - var shouldUpdateView = false - if(!hasActiveDpad && !hasActiveJoystick) { - for (button in overlayButtons) { - val stateChanged = button.updateStatus(event, hasActiveButtons, this) - if (!stateChanged) { - continue - } - - if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { - swapScreen() - } - else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) { - TurboHelper.toggleTurbo(true) - } - - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - button.id, - button.status - ) - - shouldUpdateView = true - } - } - - if(!hasActiveButtons && !hasActiveJoystick) { - for (dpad in overlayDpads) { - val stateChanged = dpad.updateStatus( - event, - hasActiveDpad, - EmulationMenuSettings.dpadSlide, - this - ) - if (!stateChanged) { - continue - } - - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.upId, - dpad.upStatus - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.downId, - dpad.downStatus - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.leftId, - dpad.leftStatus - ) - NativeLibrary.onGamePadEvent( - NativeLibrary.TouchScreenDevice, - dpad.rightId, - dpad.rightStatus - ) - - shouldUpdateView = true - } - } - - if(!hasActiveDpad && !hasActiveButtons) { - for (joystick in overlayJoysticks) { - val stateChanged = joystick.updateStatus(event, hasActiveJoystick, this) - if (!stateChanged) { - continue - } - - val axisID = joystick.joystickId - NativeLibrary.onGamePadMoveEvent( - NativeLibrary.TouchScreenDevice, - axisID, - joystick.xAxis, - joystick.yAxis - ) - - shouldUpdateView = true - } - } - - if (shouldUpdateView) { - invalidate() - } - - if (!preferences.getBoolean("isTouchEnabled", true)) { - return true - } - - val xPosition = event.getX(pointerIndex).toInt() - val yPosition = event.getY(pointerIndex).toInt() val motionEvent = event.action and MotionEvent.ACTION_MASK val isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN val isActionMove = motionEvent == MotionEvent.ACTION_MOVE val isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP - if (isActionDown && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchEvent(xPosition.toFloat(), yPosition.toFloat(), true) - } - if (isActionMove) { - for (i in 0 until event.pointerCount) { - val fingerId = event.getPointerId(i) - if (isTouchInputConsumed(fingerId)) { + + val pointerList = (0 until event.pointerCount).toMutableList() + // Move the pointer that triggered the most recent event to the front + // of the list so that it is processed first + val currentActionPointer = event.actionIndex + pointerList.remove(pointerList.indexOf(currentActionPointer)) + pointerList.add(0, currentActionPointer) + + // Set up a loop for if we need to check touches other than the most recent one + // (Only happens if we're dragging the touch) + for (pointerIndex in pointerList) { + val pointerId = event.getPointerId(pointerIndex) + + val xPosition = event.getX(pointerIndex).toInt() + val yPosition = event.getY(pointerIndex).toInt() + + var hasActiveButtons = false + for (button in overlayButtons) { + if (button.trackId == pointerId) { + hasActiveButtons = true + break + } + } + + var hasActiveDpad = false + if (!hasActiveButtons) { + for (dpad in overlayDpads) { + if (dpad.trackId == pointerId) { + hasActiveDpad = true + break + } + } + } + + var hasActiveJoystick = false + if(!hasActiveButtons && !hasActiveDpad){ + for (joystick in overlayJoysticks) { + if (joystick.trackId == pointerId) { + hasActiveJoystick = true + break + } + } + } + + val hasActiveOverlay = hasActiveButtons || hasActiveDpad || hasActiveJoystick + + if (preferences.getBoolean("isTouchEnabled", true) && !hasActiveOverlay) { + if (isActionMove) { + NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat()) continue } - NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat()) + else if (isActionUp) { + NativeLibrary.onTouchEvent(0f, 0f, false) + break // Up and down actions shouldn't loop + } } - } - if (isActionUp && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchEvent(0f, 0f, false) + + var anyOverlayStateChanged = false + var shouldUpdateView = false + if(!hasActiveDpad && !hasActiveJoystick) { + for (button in overlayButtons) { + val stateChanged = button.updateStatus(event, pointerIndex, hasActiveButtons, this) + if (!stateChanged) { + continue + } + anyOverlayStateChanged = true + + if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) { + swapScreen() + } + else if (button.id == NativeLibrary.ButtonType.BUTTON_TURBO && button.status == NativeLibrary.ButtonState.PRESSED) { + TurboHelper.toggleTurbo(true) + } + + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + button.id, + button.status + ) + + shouldUpdateView = true + } + } + + if(!hasActiveButtons && !hasActiveJoystick) { + for (dpad in overlayDpads) { + val stateChanged = dpad.updateStatus( + event, + pointerIndex, + hasActiveDpad, + EmulationMenuSettings.dpadSlide, + this + ) + if (!stateChanged) { + continue + } + anyOverlayStateChanged = true + + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.upId, + dpad.upStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.downId, + dpad.downStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.leftId, + dpad.leftStatus + ) + NativeLibrary.onGamePadEvent( + NativeLibrary.TouchScreenDevice, + dpad.rightId, + dpad.rightStatus + ) + + shouldUpdateView = true + } + } + + if(!hasActiveDpad && !hasActiveButtons) { + for (joystick in overlayJoysticks) { + val stateChanged = joystick.updateStatus(event, pointerIndex, hasActiveJoystick, this) + if (!stateChanged) { + continue + } + anyOverlayStateChanged = true + + val axisID = joystick.joystickId + NativeLibrary.onGamePadMoveEvent( + NativeLibrary.TouchScreenDevice, + axisID, + joystick.xAxis, + joystick.yAxis + ) + + shouldUpdateView = true + } + } + + if (shouldUpdateView) { + invalidate() + } + + if (preferences.getBoolean("isTouchEnabled", true) && + isActionDown && + !anyOverlayStateChanged + ) { + // These need to be recalculated because touching the area + // right in the middle of the dpad (between the "buttons") or + // tapping a joystick in a certain way both don't cause + // `anyOverlayStateChanged` to be set to true + var isDpadPressed = false + for (dpad in overlayDpads) { + if (dpad.trackId == pointerId) { + isDpadPressed = true + break + } + } + var isJoystickPressed = false + for (joystick in overlayJoysticks) { + if (joystick.trackId == pointerId) { + isJoystickPressed = true + break + } + } + + if (!isDpadPressed && !isJoystickPressed) { + NativeLibrary.onTouchEvent(xPosition.toFloat(), yPosition.toFloat(), true) + } + } + + // We should only loop here if touch is being dragged + if (!isActionMove) { + break + } + } return true } - private fun isTouchInputConsumed(trackId: Int): Boolean { - overlayButtons.forEach { - if (it.trackId == trackId) { - return true - } - } - overlayDpads.forEach { - if (it.trackId == trackId) { - return true - } - } - overlayJoysticks.forEach { - if (it.trackId == trackId) { - return true - } - } - return false - } - fun onTouchWhileEditing(event: MotionEvent): Boolean { val pointerIndex = event.actionIndex val fingerPositionX = event.getX(pointerIndex).toInt() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt index 0e9b0185d..0899074fe 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt @@ -70,9 +70,8 @@ class InputOverlayDrawableButton( * * @return true if value was changed */ - fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { + fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { val buttonSliding = EmulationMenuSettings.buttonSlide - val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() val pointerId = event.getPointerId(pointerIndex) @@ -92,7 +91,7 @@ class InputOverlayDrawableButton( if (trackId != pointerId) { return false } - buttonUp(overlay) + buttonUp(overlay, false) return true } @@ -105,11 +104,14 @@ class InputOverlayDrawableButton( if (inside || trackId != pointerId) { return false } + // prevent the first (directly pressed) button to deactivate when sliding off if (buttonSliding == ButtonSlidingMode.Alternative.int && isMotionFirstButton) { return false } - buttonUp(overlay) + + val preserveTrackId = (buttonSliding != ButtonSlidingMode.Disabled.int) + buttonUp(overlay, preserveTrackId) return true } else { // button was not yet pressed @@ -132,10 +134,12 @@ class InputOverlayDrawableButton( overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) } - private fun buttonUp(overlay: InputOverlay) { + private fun buttonUp(overlay: InputOverlay, preserveTrackId: Boolean) { pressedState = false isMotionFirstButton = false - trackId = -1 + if (!preserveTrackId) { + trackId = -1 + } overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt index c032ee3bc..5e902c99b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt @@ -63,9 +63,8 @@ class InputOverlayDrawableDpad( trackId = -1 } - fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean { + fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, dpadSlide: Boolean, overlay: InputOverlay): Boolean { var isDown = false - val pointerIndex = event.actionIndex val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() val pointerId = event.getPointerId(pointerIndex) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt index b66118c90..3cc871d30 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt @@ -93,8 +93,7 @@ class InputOverlayDrawableJoystick( currentStateBitmapDrawable.draw(canvas) } - fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { - val pointerIndex = event.actionIndex + fun updateStatus(event: MotionEvent, pointerIndex: Int, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean { val xPosition = event.getX(pointerIndex).toInt() val yPosition = event.getY(pointerIndex).toInt() val pointerId = event.getPointerId(pointerIndex)