From f535d4d8a293235d9c8e179a37f5a928ffbd2e89 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 25 Aug 2026 10:13:35 +0200 Subject: [PATCH] fix: Correct inverted Android stick Y axis and map the RX/RY right stick Follow-up to #128, which fixed the same double inversion for the d-pad hat axis. gamepads_android's EventListener already inverts AXIS_Y, AXIS_RZ and AXIS_RY, so a physical stick-up reaches Dart as +1.0. AndroidMapping negated it a second time and emitted leftStickY = -1.0 for up, the opposite of every other platform and of the documented convention. AXIS_RX and AXIS_RY were reported by EventListener but missing from the axis map, so they fell through to normalizeDpadAxis and were dropped. Controllers that report the right stick on RX/RY, such as the DJI RC Pro, emitted no normalized right stick events at all. Claude-Session: https://claude.ai/code/session_019aEP4Np5vqDw5BkgGYyHe7 --- .../lib/src/mappings/android_mapping.dart | 16 +++++++------ .../lib/src/mappings/platform_mapping.dart | 8 ++++--- .../test/gamepad_normalizer_test.dart | 4 ++-- packages/gamepads/test/mappings_test.dart | 23 ++++++++++++++----- .../gamepads_android/EventListener.kt | 6 +++++ 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/packages/gamepads/lib/src/mappings/android_mapping.dart b/packages/gamepads/lib/src/mappings/android_mapping.dart index f57344b6e..97b155fb5 100644 --- a/packages/gamepads/lib/src/mappings/android_mapping.dart +++ b/packages/gamepads/lib/src/mappings/android_mapping.dart @@ -37,6 +37,10 @@ class AndroidMapping extends PlatformMapping { 'AXIS_RTRIGGER': GamepadAxis.rightTrigger, 'AXIS_BRAKE': GamepadAxis.leftTrigger, 'AXIS_GAS': GamepadAxis.rightTrigger, + // Right-stick axes for non-Xbox layouts, which report the right stick on + // RX/RY instead of Z/RZ. E.g. the DJI RC Pro. + 'AXIS_RX': GamepadAxis.rightStickX, + 'AXIS_RY': GamepadAxis.rightStickY, }; // D-pad hat axes on Android. @@ -59,10 +63,9 @@ class AndroidMapping extends PlatformMapping { return const []; } // Android reports sticks in -1.0 to 1.0 and triggers in 0.0 to 1.0. - // Y-axis is inverted on Android (up = negative). - if (axis == GamepadAxis.leftStickY || axis == GamepadAxis.rightStickY) { - return [NormalizedAxis(axis, -value)]; - } + // gamepads_android's EventListener already inverts the stick Y axes + // (AXIS_Y, AXIS_RZ and AXIS_RY), so the values reaching here follow + // up = +1.0, down = -1.0 and must not be negated again. See issue #123. return [NormalizedAxis(axis, value)]; } @@ -78,9 +81,8 @@ class AndroidMapping extends PlatformMapping { ]; } if (key == _dpadYAxis) { - // gamepads_android's EventListener already inverts AXIS_HAT_Y, so the - // value reaching here follows up = +1.0, down = -1.0 (the opposite of - // Android's native AXIS_HAT_Y convention). See issue #123. + // As with the stick Y axes above, EventListener already inverts + // AXIS_HAT_Y, so up = +1.0 and down = -1.0 here. return [ NormalizedButton( GamepadButton.dpadDown, diff --git a/packages/gamepads/lib/src/mappings/platform_mapping.dart b/packages/gamepads/lib/src/mappings/platform_mapping.dart index d02c271b2..dca653fbe 100644 --- a/packages/gamepads/lib/src/mappings/platform_mapping.dart +++ b/packages/gamepads/lib/src/mappings/platform_mapping.dart @@ -25,10 +25,12 @@ class NormalizedAxis { /// /// **Y-axis convention**: Normalized stick Y values use up = +1.0, /// down = -1.0. Platforms where the native API reports the opposite -/// (e.g., iOS, Android, Web) must negate Y in their [normalizeAxis]. +/// (e.g., iOS, Web) must negate Y in their [normalizeAxis]. /// macOS GCController already reports up = positive natively, so no -/// inversion is needed there. Linux/Windows handle inversion via the -/// `yAxisInverted` flag in `ControllerMapping`. +/// inversion is needed there. Android is inverted by the +/// `gamepads_android` plugin before the event reaches Dart, so its +/// mapping must not negate Y again. Linux/Windows handle inversion via +/// the `yAxisInverted` flag in `ControllerMapping`. abstract class PlatformMapping { /// Attempts to normalize a button event. /// diff --git a/packages/gamepads/test/gamepad_normalizer_test.dart b/packages/gamepads/test/gamepad_normalizer_test.dart index e11e300e3..704bcfb12 100644 --- a/packages/gamepads/test/gamepad_normalizer_test.dart +++ b/packages/gamepads/test/gamepad_normalizer_test.dart @@ -94,7 +94,7 @@ void main() { expect(results.first.button, GamepadButton.a); }); - test('normalizes Android axis with Y inversion', () { + test('keeps the Android Y axis inverted by the plugin', () { final event = GamepadEvent( gamepadId: 'pad1', timestamp: 1000, @@ -105,7 +105,7 @@ void main() { final results = normalizer.normalize(event); expect(results.first.axis, GamepadAxis.leftStickY); - expect(results.first.value, -1.0); + expect(results.first.value, 1.0); }); }); diff --git a/packages/gamepads/test/mappings_test.dart b/packages/gamepads/test/mappings_test.dart index 4e836456e..9c18d8b57 100644 --- a/packages/gamepads/test/mappings_test.dart +++ b/packages/gamepads/test/mappings_test.dart @@ -202,18 +202,29 @@ void main() { ); }); - test('normalizes stick axes with Y inversion', () { + test('normalizes stick axes without inverting Y again', () { final lx = mapping.normalizeAxis('AXIS_X', 0.5); expect(lx.first.axis, GamepadAxis.leftStickX); expect(lx.first.value, 0.5); - // Y-axis should be inverted + // EventListener already inverted AXIS_Y and AXIS_RZ, so a positive + // value here means up and must be passed through unchanged. final ly = mapping.normalizeAxis('AXIS_Y', 0.5); expect(ly.first.axis, GamepadAxis.leftStickY); - expect(ly.first.value, -0.5); + expect(ly.first.value, 0.5); final ry = mapping.normalizeAxis('AXIS_RZ', -1.0); expect(ry.first.axis, GamepadAxis.rightStickY); + expect(ry.first.value, -1.0); + }); + + test('normalizes the alternate right stick axes', () { + final rx = mapping.normalizeAxis('AXIS_RX', 0.5); + expect(rx.first.axis, GamepadAxis.rightStickX); + expect(rx.first.value, 0.5); + + final ry = mapping.normalizeAxis('AXIS_RY', 1.0); + expect(ry.first.axis, GamepadAxis.rightStickY); expect(ry.first.value, 1.0); }); @@ -245,9 +256,9 @@ void main() { expect(right[1].button, GamepadButton.dpadRight); expect(right[1].value, 1.0); - // gamepads_android's EventListener inverts AXIS_HAT_Y before it reaches - // the mapping, so +1.0 = up and -1.0 = down here (regression for #123: - // pressing up must emit dpadUp, not dpadDown). + // EventListener inverts AXIS_HAT_Y before it reaches the mapping, so + // +1.0 = up and -1.0 = down here (regression for #123: pressing up + // must emit dpadUp, not dpadDown). final up = mapping.normalizeDpadAxis('AXIS_HAT_Y', 1.0); expect(up[0].button, GamepadButton.dpadDown); expect(up[0].value, 0.0); diff --git a/packages/gamepads_android/android/src/main/kotlin/org/flame_engine/gamepads_android/EventListener.kt b/packages/gamepads_android/android/src/main/kotlin/org/flame_engine/gamepads_android/EventListener.kt index 3ab9b90f7..be5c9717d 100644 --- a/packages/gamepads_android/android/src/main/kotlin/org/flame_engine/gamepads_android/EventListener.kt +++ b/packages/gamepads_android/android/src/main/kotlin/org/flame_engine/gamepads_android/EventListener.kt @@ -18,6 +18,12 @@ class EventListener { } private val lastAxisValue = mutableMapOf() // Reference: https://developer.android.com/reference/android/view/MotionEvent + // + // The vertical axes are inverted here so that the values sent to Dart + // follow up = +1.0, down = -1.0 instead of Android's native convention. + // AndroidMapping in the gamepads package relies on that and does not + // invert them again, so removing an `invert` below silently flips the + // corresponding stick or d-pad direction. See issue #123. private val supportedAxes = listOf( SupportedAxis(MotionEvent.AXIS_X), SupportedAxis(MotionEvent.AXIS_Y, invert = true),