android: Added button sliding to button overlay (#884)

* Added simple button sliding mode

* Added "keep first" button sliding mode

* directly pressed buttons stay active even when sliding off
* further buttons can be triggered via the simple sliding method

* Added button sliding configuration to overlay settings menu

* Updated licences

* Added button sliding activation to dpads and joysticks

* separated handling of buttons, dpads and joysticks needed since they can be activated by moving now

* Adjusted strings

* Changed ButtonSlidingMode values to mirror prior string name changes

* Reverted incorrectly applied language translation

* Nitpicky formatting adjustments

* Shortened string IDs

* hasActiveJoy --> hasActiveJoystick

* showButtonSlidingModeMenu --> showButtonSlidingMenu

* Updated outdated comment relating to `isMotionFirstButton`

Co-authored-by: toksn <toksn@yahoo.de>

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
toksn 2025-07-28 03:15:26 +02:00 committed by GitHub
parent 00c0f01e73
commit c95b942ec2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 225 additions and 62 deletions

View File

@ -796,6 +796,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
true
}
R.id.menu_emulation_button_sliding -> {
showButtonSlidingMenu()
true
}
R.id.menu_emulation_dpad_slide_enable -> {
EmulationMenuSettings.dpadSlide = !EmulationMenuSettings.dpadSlide
true
@ -840,6 +845,28 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show()
}
private fun showButtonSlidingMenu() {
val editor = preferences.edit()
val buttonSlidingModes = mutableListOf<String>()
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_disabled))
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_enabled))
buttonSlidingModes.add(getString(R.string.emulation_button_sliding_alternative))
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.emulation_button_sliding)
.setSingleChoiceItems(
buttonSlidingModes.toTypedArray(),
EmulationMenuSettings.buttonSlide
) { _: DialogInterface?, which: Int ->
EmulationMenuSettings.buttonSlide = which
}
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
editor.apply()
}
.show()
}
private fun showLandscapeScreenLayoutMenu() {
val popupMenu = PopupMenu(
requireContext(),

View File

@ -96,57 +96,115 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
if (isInEditMode) {
return onTouchWhileEditing(event)
}
var shouldUpdateView = false
var hasActiveButtons = false
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
for (button in overlayButtons) {
if (!button.updateStatus(event, this)) {
continue
if (button.trackId == pointerId) {
hasActiveButtons = true
break
}
if (button.id == NativeLibrary.ButtonType.BUTTON_SWAP && button.status == NativeLibrary.ButtonState.PRESSED) {
swapScreen()
}
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
}
for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide, this)) {
continue
var hasActiveDpad = false
if (!hasActiveButtons) {
for (dpad in overlayDpads) {
if (dpad.trackId == pointerId) {
hasActiveDpad = true
break
}
}
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
}
for (joystick in overlayJoysticks) {
if (!joystick.updateStatus(event, this)) {
continue
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
}
val axisID = joystick.joystickId
NativeLibrary.onGamePadMoveEvent(
NativeLibrary.TouchScreenDevice,
axisID,
joystick.xAxis,
joystick.yAxis
)
shouldUpdateView = true
}
if (shouldUpdateView) {
@ -157,10 +215,8 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
return true
}
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,6 +12,20 @@ import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.utils.EmulationMenuSettings
enum class ButtonSlidingMode(val int: Int) {
// Disabled, buttons can only be triggered by pressing them directly.
Disabled(0),
// Additionally to pressing buttons directly, they can be activated and released by sliding into
// and out of their area.
Enabled(1),
// The first button is kept activated until released, further buttons use the simple button
// sliding method.
Alternative(2)
}
/**
* Custom [BitmapDrawable] that is capable
@ -30,6 +44,9 @@ class InputOverlayDrawableButton(
val opacity: Int
) {
var trackId: Int
private var isMotionFirstButton = false // mark the first activated button with the current motion
private var previousTouchX = 0
private var previousTouchY = 0
private var controlPositionX = 0
@ -53,7 +70,8 @@ class InputOverlayDrawableButton(
*
* @return true if value was changed
*/
fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, 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()
@ -67,23 +85,60 @@ class InputOverlayDrawableButton(
if (!bounds.contains(xPosition, yPosition)) {
return false
}
pressedState = true
trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
buttonDown(true, pointerId, overlay)
return true
}
if (isActionUp) {
if (trackId != pointerId) {
return false
}
pressedState = false
trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
buttonUp(overlay)
return true
}
val isActionMoving = motionEvent == MotionEvent.ACTION_MOVE
if (buttonSliding != ButtonSlidingMode.Disabled.int && isActionMoving) {
val inside = bounds.contains(xPosition, yPosition)
if (pressedState) {
// button is already pressed
// check whether we moved out of the button area to update the state
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)
return true
} else {
// button was not yet pressed
// check whether we moved into the button area to update the state
if (!inside) {
return false
}
buttonDown(!hasActiveButtons, pointerId, overlay)
return true
}
}
return false
}
private fun buttonDown(firstBtn: Boolean, pointerId: Int, overlay: InputOverlay) {
pressedState = true
isMotionFirstButton = firstBtn
trackId = pointerId
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
}
private fun buttonUp(overlay: InputOverlay) {
pressedState = false
isMotionFirstButton = false
trackId = -1
overlay.hapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
}
fun onConfigureTouch(event: MotionEvent): Boolean {
val pointerIndex = event.actionIndex
val fingerPositionX = event.getX(pointerIndex).toInt()

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.utils.EmulationMenuSettings
/**
* Custom [BitmapDrawable] that is capable
@ -62,15 +63,19 @@ class InputOverlayDrawableDpad(
trackId = -1
}
fun updateStatus(event: MotionEvent, dpadSlide: Boolean, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, 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)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
var isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) {
isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons
}
val isActionUp =
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown) {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -93,14 +93,18 @@ class InputOverlayDrawableJoystick(
currentStateBitmapDrawable.draw(canvas)
}
fun updateStatus(event: MotionEvent, overlay:InputOverlay): Boolean {
fun updateStatus(event: MotionEvent, hasActiveButtons: Boolean, overlay: InputOverlay): Boolean {
val pointerIndex = event.actionIndex
val xPosition = event.getX(pointerIndex).toInt()
val yPosition = event.getY(pointerIndex).toInt()
val pointerId = event.getPointerId(pointerIndex)
val motionEvent = event.action and MotionEvent.ACTION_MASK
val isActionDown =
var isActionDown =
motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
if (!isActionDown && EmulationMenuSettings.buttonSlide != ButtonSlidingMode.Disabled.int) {
isActionDown = motionEvent == MotionEvent.ACTION_MOVE && !hasActiveButtons
}
val isActionUp =
motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
if (isActionDown) {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,6 +7,7 @@ package org.citra.citra_emu.utils
import androidx.drawerlayout.widget.DrawerLayout
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.overlay.ButtonSlidingMode
object EmulationMenuSettings {
private val preferences =
@ -26,6 +27,13 @@ object EmulationMenuSettings {
.putBoolean("EmulationMenuSettings_DpadSlideEnable", value)
.apply()
}
var buttonSlide: Int
get() = preferences.getInt("EmulationMenuSettings_ButtonSlideMode", ButtonSlidingMode.Disabled.int)
set(value) {
preferences.edit()
.putInt("EmulationMenuSettings_ButtonSlideMode", value)
.apply()
}
var showPerformanceOverlay: Boolean
get() = preferences.getBoolean("EmulationMenuSettings_showPerformanceOverlay", false)

View File

@ -86,6 +86,10 @@
android:id="@+id/menu_emulation_adjust_opacity"
android:title="@string/emulation_control_opacity" />
<item
android:id="@+id/menu_emulation_button_sliding"
android:title="@string/emulation_button_sliding">
</item>
<group android:checkableBehavior="all">
<item
android:id="@+id/menu_emulation_joystick_rel_center"

View File

@ -421,6 +421,10 @@
<string name="emulation_configure_controls">Configure Controls</string>
<string name="emulation_edit_layout">Edit Layout</string>
<string name="emulation_done">Done</string>
<string name="emulation_button_sliding">Button Sliding</string>
<string name="emulation_button_sliding_disabled">Hold originally pressed button</string>
<string name="emulation_button_sliding_enabled">Hold currently pressed button</string>
<string name="emulation_button_sliding_alternative">Hold original and currently pressed button</string>
<string name="emulation_toggle_controls">Toggle Controls</string>
<string name="emulation_control_scale">Adjust Scale</string>
<string name="emulation_control_scale_global">Global Scale</string>