mirror of
https://github.com/izzy2lost/xemu.git
synced 2026-07-06 00:20:22 -07:00
beginning of touch controls
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# On-Screen Controller
|
||||
|
||||
This implementation provides a virtual Xbox controller overlay for touchscreen devices.
|
||||
|
||||
## Features
|
||||
|
||||
### Controller Layout
|
||||
Based on the original Xbox controller, the on-screen controller includes:
|
||||
|
||||
**Face Buttons (Right Side)**
|
||||
- A Button (Green) - Bottom
|
||||
- B Button (Red) - Right
|
||||
- X Button (Blue) - Left
|
||||
- Y Button (Yellow) - Top
|
||||
|
||||
**D-Pad (Left Side)**
|
||||
- Up, Down, Left, Right directional buttons
|
||||
|
||||
**Analog Sticks**
|
||||
- Left Stick - Lower left position
|
||||
- Right Stick - Lower right position
|
||||
- Both sticks support 360-degree movement with dead zone
|
||||
- Click-able (L3/R3 functionality)
|
||||
|
||||
**Shoulder Buttons**
|
||||
- Left Trigger (LT) - Top left
|
||||
- Right Trigger (RT) - Top right
|
||||
- Left Bumper (LB) - Below left trigger
|
||||
- Right Bumper (RB) - Below right trigger
|
||||
|
||||
**Center Buttons**
|
||||
- Start Button - Right of center
|
||||
- Back Button - Left of center
|
||||
|
||||
**Original Xbox Buttons**
|
||||
- Black Button - Upper right area
|
||||
- White Button - Upper right area
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Behavior
|
||||
|
||||
The on-screen controller is **enabled by default** and will:
|
||||
- ✅ Show automatically when no physical controller is connected
|
||||
- ✅ Hide automatically when a physical controller is plugged in
|
||||
- ✅ Reappear automatically when the physical controller is disconnected
|
||||
|
||||
This provides a seamless experience where users always have a way to control the game.
|
||||
|
||||
### Physical Controller Detection
|
||||
|
||||
The system monitors for:
|
||||
- USB game controllers
|
||||
- Bluetooth game controllers
|
||||
- Any device with `SOURCE_GAMEPAD` or `SOURCE_JOYSTICK` input sources
|
||||
|
||||
When any physical controller is detected, the on-screen controller automatically hides to avoid cluttering the screen.
|
||||
|
||||
### Manual Control
|
||||
|
||||
You can still manually control the on-screen controller if needed:
|
||||
|
||||
```kotlin
|
||||
// In MainActivity
|
||||
val mainActivity = this as MainActivity
|
||||
|
||||
// Show controller
|
||||
mainActivity.showOnScreenController()
|
||||
|
||||
// Hide controller
|
||||
mainActivity.hideOnScreenController()
|
||||
|
||||
// Toggle visibility
|
||||
mainActivity.toggleOnScreenController()
|
||||
|
||||
// Force recheck for physical controllers
|
||||
mainActivity.forceUpdateControllerVisibility()
|
||||
```
|
||||
|
||||
### User Preference Override
|
||||
|
||||
Users can override the automatic behavior through settings:
|
||||
|
||||
Controller preferences are managed through `ControllerSettings`:
|
||||
|
||||
```kotlin
|
||||
val settings = ControllerSettings(context)
|
||||
|
||||
// Enable/disable on-screen controller (default: true)
|
||||
settings.showOnScreenController = true
|
||||
|
||||
// Adjust opacity (0.0 - 1.0)
|
||||
settings.controllerOpacity = 0.7f
|
||||
|
||||
// Adjust scale (0.5 - 2.0)
|
||||
settings.controllerScale = 1.0f
|
||||
|
||||
// Enable/disable vibration feedback
|
||||
settings.vibrationEnabled = true
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Automatic Controller Detection
|
||||
|
||||
The `MainActivity` implements `InputManager.InputDeviceListener` to monitor for controller connections:
|
||||
|
||||
1. **On Startup**: Scans for existing connected controllers
|
||||
2. **On Device Added**: Detects when a controller is plugged in (USB/Bluetooth)
|
||||
3. **On Device Removed**: Detects when a controller is disconnected
|
||||
4. **On Device Changed**: Handles configuration changes
|
||||
|
||||
The detection logic identifies game controllers by checking for:
|
||||
- `InputDevice.SOURCE_GAMEPAD` - Standard gamepad devices
|
||||
- `InputDevice.SOURCE_JOYSTICK` - Joystick devices
|
||||
|
||||
### Components
|
||||
|
||||
1. **OnScreenController.kt**
|
||||
- Custom View that renders the controller overlay
|
||||
- Handles multi-touch input for simultaneous button presses
|
||||
- Provides visual feedback for button states
|
||||
- Manages analog stick positioning with dead zones
|
||||
|
||||
2. **ControllerInputBridge.kt**
|
||||
- Bridges on-screen controller events to SDL input system
|
||||
- Maps virtual buttons to SDL controller buttons
|
||||
- Converts analog stick positions to axis events
|
||||
- Translates to Android KeyEvent codes
|
||||
|
||||
3. **ControllerSettings.kt**
|
||||
- Manages user preferences for controller behavior
|
||||
- Persists settings using SharedPreferences
|
||||
|
||||
### Input Mapping
|
||||
|
||||
The controller maps to standard SDL controller inputs:
|
||||
|
||||
| On-Screen Button | SDL Button | KeyEvent |
|
||||
|-----------------|------------|----------|
|
||||
| A | SDL_CONTROLLER_BUTTON_A | KEYCODE_BUTTON_A |
|
||||
| B | SDL_CONTROLLER_BUTTON_B | KEYCODE_BUTTON_B |
|
||||
| X | SDL_CONTROLLER_BUTTON_X | KEYCODE_BUTTON_X |
|
||||
| Y | SDL_CONTROLLER_BUTTON_Y | KEYCODE_BUTTON_Y |
|
||||
| D-Pad Up | SDL_CONTROLLER_BUTTON_DPAD_UP | KEYCODE_DPAD_UP |
|
||||
| D-Pad Down | SDL_CONTROLLER_BUTTON_DPAD_DOWN | KEYCODE_DPAD_DOWN |
|
||||
| D-Pad Left | SDL_CONTROLLER_BUTTON_DPAD_LEFT | KEYCODE_DPAD_LEFT |
|
||||
| D-Pad Right | SDL_CONTROLLER_BUTTON_DPAD_RIGHT | KEYCODE_DPAD_RIGHT |
|
||||
| Left Bumper | SDL_CONTROLLER_BUTTON_LEFTSHOULDER | KEYCODE_BUTTON_L1 |
|
||||
| Right Bumper | SDL_CONTROLLER_BUTTON_RIGHTSHOULDER | KEYCODE_BUTTON_R1 |
|
||||
| Left Trigger | Axis Event | KEYCODE_BUTTON_L2 |
|
||||
| Right Trigger | Axis Event | KEYCODE_BUTTON_R2 |
|
||||
| Start | SDL_CONTROLLER_BUTTON_START | KEYCODE_BUTTON_START |
|
||||
| Back | SDL_CONTROLLER_BUTTON_BACK | KEYCODE_BUTTON_SELECT |
|
||||
| Left Stick | Axis Events (X/Y) | - |
|
||||
| Right Stick | Axis Events (X/Y) | - |
|
||||
| L3 (Left Stick Click) | SDL_CONTROLLER_BUTTON_LEFTSTICK | KEYCODE_BUTTON_THUMBL |
|
||||
| R3 (Right Stick Click) | SDL_CONTROLLER_BUTTON_RIGHTSTICK | KEYCODE_BUTTON_THUMBR |
|
||||
|
||||
### Analog Stick Behavior
|
||||
|
||||
- **Dead Zone**: 20% of stick radius (configurable)
|
||||
- **Range**: -1.0 to 1.0 for both X and Y axes
|
||||
- **Clamping**: Movement is constrained to circular boundary
|
||||
- **Multi-touch**: Each stick can be controlled independently
|
||||
|
||||
## Customization
|
||||
|
||||
### Adjusting Button Positions
|
||||
|
||||
Edit `initializeControls()` in `OnScreenController.kt` to modify button positions:
|
||||
|
||||
```kotlin
|
||||
// Example: Move A button
|
||||
buttons[Button.A] = ButtonState(
|
||||
PointF(w * 0.85f, h * 0.5f), // x, y as percentage of screen
|
||||
faceButtonRadius
|
||||
)
|
||||
```
|
||||
|
||||
### Changing Button Colors
|
||||
|
||||
Modify `getButtonColor()` and `getButtonPressedColor()` methods:
|
||||
|
||||
```kotlin
|
||||
private fun getButtonColor(button: Button): Int {
|
||||
return when (button) {
|
||||
Button.A -> Color.argb(150, 100, 200, 100) // ARGB values
|
||||
// ... other buttons
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adjusting Sizes
|
||||
|
||||
Button and stick sizes are calculated as percentages of screen width:
|
||||
|
||||
```kotlin
|
||||
val faceButtonRadius = w * 0.04f // 4% of screen width
|
||||
val stickRadius = w * 0.08f // 8% of screen width
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- Haptic feedback on button press
|
||||
- Customizable button layouts
|
||||
- Save/load custom controller configurations
|
||||
- Opacity and scale adjustments via UI
|
||||
- Button remapping
|
||||
- Portrait/landscape specific layouts
|
||||
- Gesture support (swipe for quick actions)
|
||||
|
||||
## Testing
|
||||
|
||||
To test the on-screen controller:
|
||||
|
||||
1. **Default Behavior**
|
||||
- Build and run the app on a touchscreen device
|
||||
- On-screen controller should be visible by default
|
||||
- Test each button for visual feedback and input
|
||||
|
||||
2. **Physical Controller Detection**
|
||||
- Connect a USB or Bluetooth game controller
|
||||
- On-screen controller should automatically hide
|
||||
- Disconnect the controller
|
||||
- On-screen controller should automatically reappear
|
||||
|
||||
3. **Multi-touch Testing**
|
||||
- Test analog sticks for smooth movement
|
||||
- Test multi-touch (e.g., move both sticks simultaneously)
|
||||
- Press multiple buttons at once
|
||||
|
||||
4. **Game Integration**
|
||||
- Verify input is received by the game/emulator
|
||||
- Test all buttons map correctly to game actions
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Controller not visible on startup**
|
||||
- Check that no physical controller is connected
|
||||
- Verify the view is added to the layout hierarchy
|
||||
- Check `ControllerSettings.showOnScreenController` preference
|
||||
|
||||
**Controller doesn't hide when physical controller connected**
|
||||
- Verify the physical controller is recognized as a gamepad
|
||||
- Check logcat for InputDevice events
|
||||
- Call `forceUpdateControllerVisibility()` to manually trigger detection
|
||||
|
||||
**Controller doesn't reappear when physical controller disconnected**
|
||||
- Check that `InputDeviceListener` is properly registered
|
||||
- Verify `onInputDeviceRemoved()` is being called
|
||||
- Check for any exceptions in the listener callbacks
|
||||
|
||||
**Buttons not responding**
|
||||
- Ensure `ControllerInputBridge` is properly connected
|
||||
- Check that SDL input system is initialized
|
||||
- Verify touch events are not being intercepted by other views
|
||||
|
||||
**Analog sticks not working**
|
||||
- Confirm axis events are being sent to SDL
|
||||
- Check dead zone settings
|
||||
- Verify stick position calculations
|
||||
|
||||
**Performance issues**
|
||||
- Reduce controller opacity
|
||||
- Optimize `onDraw()` method
|
||||
- Consider using hardware acceleration
|
||||
@@ -76,6 +76,10 @@ __attribute__((used)) static void *xemu_android_force_smbus_storage_ref =
|
||||
extern void xemu_android_force_usb_hub_link(void);
|
||||
__attribute__((used)) static void *xemu_android_force_usb_hub_ref =
|
||||
(void *)&xemu_android_force_usb_hub_link;
|
||||
/* Force-link the Xbox USB gamepad registration unit from the static lib. */
|
||||
extern void xemu_android_force_xid_gamepad_link(void);
|
||||
__attribute__((used)) static void *xemu_android_force_xid_gamepad_ref =
|
||||
(void *)&xemu_android_force_xid_gamepad_link;
|
||||
|
||||
#define XEMU_ANDROID_ACCEL_CPU_TYPE "accel-i386-cpu"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "qemu/osdep.h"
|
||||
|
||||
#include <SDL_filesystem.h>
|
||||
#include <SDL_gamecontroller.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -337,14 +338,127 @@ void remove_net_nat_forward_ports(unsigned int index)
|
||||
bool xemu_settings_load_gamepad_mapping(const char *guid,
|
||||
GamepadMappings **mapping)
|
||||
{
|
||||
(void)guid;
|
||||
if (!mapping) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*mapping = NULL;
|
||||
if (!guid || *guid == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count;
|
||||
for (unsigned int i = 0; i < gamepad_mappings_count; ++i) {
|
||||
GamepadMappings *entry = &g_config.input.gamepad_mappings[i];
|
||||
if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preserve old behavior: global vibration off disables rumble.
|
||||
if (!g_config.input.allow_vibration) {
|
||||
entry->enable_rumble = false;
|
||||
}
|
||||
|
||||
*mapping = entry;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto apply_default_controller_mapping = [](GamepadMappings *entry) {
|
||||
entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A;
|
||||
entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B;
|
||||
entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X;
|
||||
entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y;
|
||||
entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK;
|
||||
entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE;
|
||||
entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START;
|
||||
entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK;
|
||||
entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
|
||||
entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
entry->controller_mapping.rshoulder =
|
||||
SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
entry->controller_mapping.dpad_right =
|
||||
SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX;
|
||||
entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY;
|
||||
entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX;
|
||||
entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY;
|
||||
entry->controller_mapping.axis_trigger_left =
|
||||
SDL_CONTROLLER_AXIS_TRIGGERLEFT;
|
||||
entry->controller_mapping.axis_trigger_right =
|
||||
SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
|
||||
entry->controller_mapping.invert_axis_left_x = false;
|
||||
entry->controller_mapping.invert_axis_left_y = false;
|
||||
entry->controller_mapping.invert_axis_right_x = false;
|
||||
entry->controller_mapping.invert_axis_right_y = false;
|
||||
};
|
||||
|
||||
const unsigned int old_count = g_config.input.gamepad_mappings_count;
|
||||
const unsigned int new_count = old_count + 1;
|
||||
GamepadMappings *new_mappings = static_cast<GamepadMappings *>(realloc(
|
||||
g_config.input.gamepad_mappings, sizeof(GamepadMappings) * new_count));
|
||||
if (!new_mappings) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "xemu-android",
|
||||
"Failed to allocate gamepad mapping for %s", guid);
|
||||
return false;
|
||||
}
|
||||
|
||||
g_config.input.gamepad_mappings = new_mappings;
|
||||
g_config.input.gamepad_mappings_count = new_count;
|
||||
|
||||
GamepadMappings *entry = &g_config.input.gamepad_mappings[old_count];
|
||||
memset(entry, 0, sizeof(*entry));
|
||||
entry->gamepad_id = strdup(guid);
|
||||
entry->enable_rumble = g_config.input.allow_vibration;
|
||||
apply_default_controller_mapping(entry);
|
||||
|
||||
*mapping = entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
void xemu_settings_reset_controller_mapping(const char *guid)
|
||||
{
|
||||
(void)guid;
|
||||
if (!guid || *guid == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned int gamepad_mappings_count = g_config.input.gamepad_mappings_count;
|
||||
for (unsigned int i = 0; i < gamepad_mappings_count; ++i) {
|
||||
GamepadMappings *entry = &g_config.input.gamepad_mappings[i];
|
||||
if (!entry->gamepad_id || strcmp(entry->gamepad_id, guid) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry->enable_rumble = g_config.input.allow_vibration;
|
||||
entry->controller_mapping.a = SDL_CONTROLLER_BUTTON_A;
|
||||
entry->controller_mapping.b = SDL_CONTROLLER_BUTTON_B;
|
||||
entry->controller_mapping.x = SDL_CONTROLLER_BUTTON_X;
|
||||
entry->controller_mapping.y = SDL_CONTROLLER_BUTTON_Y;
|
||||
entry->controller_mapping.back = SDL_CONTROLLER_BUTTON_BACK;
|
||||
entry->controller_mapping.guide = SDL_CONTROLLER_BUTTON_GUIDE;
|
||||
entry->controller_mapping.start = SDL_CONTROLLER_BUTTON_START;
|
||||
entry->controller_mapping.lstick_btn = SDL_CONTROLLER_BUTTON_LEFTSTICK;
|
||||
entry->controller_mapping.rstick_btn = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
|
||||
entry->controller_mapping.lshoulder = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
|
||||
entry->controller_mapping.rshoulder = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
|
||||
entry->controller_mapping.dpad_up = SDL_CONTROLLER_BUTTON_DPAD_UP;
|
||||
entry->controller_mapping.dpad_down = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
|
||||
entry->controller_mapping.dpad_left = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
|
||||
entry->controller_mapping.dpad_right = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
|
||||
entry->controller_mapping.axis_left_x = SDL_CONTROLLER_AXIS_LEFTX;
|
||||
entry->controller_mapping.axis_left_y = SDL_CONTROLLER_AXIS_LEFTY;
|
||||
entry->controller_mapping.axis_right_x = SDL_CONTROLLER_AXIS_RIGHTX;
|
||||
entry->controller_mapping.axis_right_y = SDL_CONTROLLER_AXIS_RIGHTY;
|
||||
entry->controller_mapping.axis_trigger_left = SDL_CONTROLLER_AXIS_TRIGGERLEFT;
|
||||
entry->controller_mapping.axis_trigger_right = SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
|
||||
entry->controller_mapping.invert_axis_left_x = false;
|
||||
entry->controller_mapping.invert_axis_left_y = false;
|
||||
entry->controller_mapping.invert_axis_right_x = false;
|
||||
entry->controller_mapping.invert_axis_right_y = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void xemu_settings_reset_keyboard_mapping(void)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.view.KeyEvent
|
||||
import org.libsdl.app.SDLControllerManager
|
||||
|
||||
/**
|
||||
* Bridge between on-screen controller and SDL input system
|
||||
*/
|
||||
class ControllerInputBridge : OnScreenController.ControllerListener {
|
||||
|
||||
companion object {
|
||||
// Virtual device ID for on-screen controller
|
||||
const val VIRTUAL_DEVICE_ID = -2
|
||||
|
||||
// Axis indices for SDL
|
||||
const val AXIS_LEFT_X = 0
|
||||
const val AXIS_LEFT_Y = 1
|
||||
const val AXIS_RIGHT_X = 2
|
||||
const val AXIS_RIGHT_Y = 3
|
||||
const val AXIS_LEFT_TRIGGER = 4
|
||||
const val AXIS_RIGHT_TRIGGER = 5
|
||||
}
|
||||
|
||||
override fun onButtonPressed(button: OnScreenController.Button) {
|
||||
try {
|
||||
val keyCode = getKeyCodeForButton(button)
|
||||
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on button press: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onButtonReleased(button: OnScreenController.Button) {
|
||||
try {
|
||||
val keyCode = getKeyCodeForButton(button)
|
||||
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on button release: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStickMoved(stick: OnScreenController.Stick, x: Float, y: Float) {
|
||||
try {
|
||||
when (stick) {
|
||||
OnScreenController.Stick.LEFT -> {
|
||||
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_X, x)
|
||||
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_LEFT_Y, y)
|
||||
}
|
||||
OnScreenController.Stick.RIGHT -> {
|
||||
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_X, x)
|
||||
SDLControllerManager.onNativeJoy(VIRTUAL_DEVICE_ID, AXIS_RIGHT_Y, y)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick move: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStickPressed(stick: OnScreenController.Stick) {
|
||||
try {
|
||||
val keyCode = when (stick) {
|
||||
OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL
|
||||
OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR
|
||||
}
|
||||
SDLControllerManager.onNativePadDown(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick press: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStickReleased(stick: OnScreenController.Stick) {
|
||||
try {
|
||||
val keyCode = when (stick) {
|
||||
OnScreenController.Stick.LEFT -> KeyEvent.KEYCODE_BUTTON_THUMBL
|
||||
OnScreenController.Stick.RIGHT -> KeyEvent.KEYCODE_BUTTON_THUMBR
|
||||
}
|
||||
SDLControllerManager.onNativePadUp(VIRTUAL_DEVICE_ID, keyCode)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ControllerBridge", "Error on stick release: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getKeyCodeForButton(button: OnScreenController.Button): Int {
|
||||
return when (button) {
|
||||
OnScreenController.Button.A -> KeyEvent.KEYCODE_BUTTON_A
|
||||
OnScreenController.Button.B -> KeyEvent.KEYCODE_BUTTON_B
|
||||
OnScreenController.Button.X -> KeyEvent.KEYCODE_BUTTON_X
|
||||
OnScreenController.Button.Y -> KeyEvent.KEYCODE_BUTTON_Y
|
||||
OnScreenController.Button.DPAD_UP -> KeyEvent.KEYCODE_DPAD_UP
|
||||
OnScreenController.Button.DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_DOWN
|
||||
OnScreenController.Button.DPAD_LEFT -> KeyEvent.KEYCODE_DPAD_LEFT
|
||||
OnScreenController.Button.DPAD_RIGHT -> KeyEvent.KEYCODE_DPAD_RIGHT
|
||||
OnScreenController.Button.LEFT_BUMPER -> KeyEvent.KEYCODE_BUTTON_L1
|
||||
OnScreenController.Button.RIGHT_BUMPER -> KeyEvent.KEYCODE_BUTTON_R1
|
||||
OnScreenController.Button.LEFT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_L2
|
||||
OnScreenController.Button.RIGHT_TRIGGER -> KeyEvent.KEYCODE_BUTTON_R2
|
||||
OnScreenController.Button.START -> KeyEvent.KEYCODE_BUTTON_START
|
||||
OnScreenController.Button.BACK -> KeyEvent.KEYCODE_BUTTON_SELECT
|
||||
OnScreenController.Button.LEFT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBL
|
||||
OnScreenController.Button.RIGHT_STICK_BUTTON -> KeyEvent.KEYCODE_BUTTON_THUMBR
|
||||
OnScreenController.Button.BLACK -> KeyEvent.KEYCODE_BUTTON_L2
|
||||
OnScreenController.Button.WHITE -> KeyEvent.KEYCODE_BUTTON_R2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class ControllerSettings(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(
|
||||
"controller_settings",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val KEY_SHOW_ON_SCREEN_CONTROLLER = "show_on_screen_controller"
|
||||
private const val KEY_CONTROLLER_OPACITY = "controller_opacity"
|
||||
private const val KEY_CONTROLLER_SCALE = "controller_scale"
|
||||
private const val KEY_VIBRATION_ENABLED = "vibration_enabled"
|
||||
}
|
||||
|
||||
var showOnScreenController: Boolean
|
||||
get() = prefs.getBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, true)
|
||||
set(value) = prefs.edit().putBoolean(KEY_SHOW_ON_SCREEN_CONTROLLER, value).apply()
|
||||
|
||||
var controllerOpacity: Float
|
||||
get() = prefs.getFloat(KEY_CONTROLLER_OPACITY, 0.7f)
|
||||
set(value) = prefs.edit().putFloat(KEY_CONTROLLER_OPACITY, value.coerceIn(0f, 1f)).apply()
|
||||
|
||||
var controllerScale: Float
|
||||
get() = prefs.getFloat(KEY_CONTROLLER_SCALE, 1.0f)
|
||||
set(value) = prefs.edit().putFloat(KEY_CONTROLLER_SCALE, value.coerceIn(0.5f, 2.0f)).apply()
|
||||
|
||||
var vibrationEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_VIBRATION_ENABLED, true)
|
||||
set(value) = prefs.edit().putBoolean(KEY_VIBRATION_ENABLED, value).apply()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
@@ -208,10 +209,13 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
private fun renderCoverGrid(games: List<GameEntry>) {
|
||||
val inflater = LayoutInflater.from(this)
|
||||
var row: LinearLayout? = null
|
||||
val columns = resolveCoverGridColumns()
|
||||
val spacingPx = dp(8)
|
||||
val halfSpacingPx = spacingPx / 2
|
||||
|
||||
for ((index, game) in games.withIndex()) {
|
||||
if (index % 2 == 0) {
|
||||
val columnIndex = index % columns
|
||||
if (columnIndex == 0) {
|
||||
row = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
}
|
||||
@@ -219,7 +223,7 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
if (index > 0) {
|
||||
if (index >= columns) {
|
||||
rowLp.topMargin = dp(12)
|
||||
}
|
||||
gamesGridContainer.addView(row, rowLp)
|
||||
@@ -227,11 +231,8 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
|
||||
val item = inflater.inflate(R.layout.item_game_cover, row, false)
|
||||
val itemLp = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
if (index % 2 == 0) {
|
||||
itemLp.marginEnd = spacingPx
|
||||
} else {
|
||||
itemLp.marginStart = spacingPx
|
||||
}
|
||||
itemLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx
|
||||
itemLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx
|
||||
row!!.addView(item, itemLp)
|
||||
|
||||
val nameText = item.findViewById<TextView>(R.id.game_cover_name_text)
|
||||
@@ -244,11 +245,25 @@ class GameLibraryActivity : AppCompatActivity() {
|
||||
bindCoverArt(coverImage, game)
|
||||
}
|
||||
|
||||
if (games.size % 2 != 0) {
|
||||
val filler = Space(this)
|
||||
val fillerLp = LinearLayout.LayoutParams(0, 0, 1f)
|
||||
fillerLp.marginStart = spacingPx
|
||||
row?.addView(filler, fillerLp)
|
||||
val remainder = games.size % columns
|
||||
if (remainder != 0) {
|
||||
for (columnIndex in remainder until columns) {
|
||||
val filler = Space(this)
|
||||
val fillerLp = LinearLayout.LayoutParams(0, 0, 1f)
|
||||
fillerLp.marginStart = if (columnIndex == 0) 0 else halfSpacingPx
|
||||
fillerLp.marginEnd = if (columnIndex == columns - 1) 0 else halfSpacingPx
|
||||
row?.addView(filler, fillerLp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveCoverGridColumns(): Int {
|
||||
val widthDp = resources.configuration.screenWidthDp
|
||||
val suggested = (widthDp / 180).coerceIn(2, 4)
|
||||
return if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
maxOf(3, suggested)
|
||||
} else {
|
||||
suggested
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,198 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.input.InputManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.InputDevice
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.widget.FrameLayout
|
||||
import org.libsdl.app.SDLActivity
|
||||
|
||||
class MainActivity : SDLActivity() {
|
||||
class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
|
||||
private var onScreenController: OnScreenController? = null
|
||||
private var controllerBridge: ControllerInputBridge? = null
|
||||
private var isControllerVisible = false
|
||||
private var inputManager: InputManager? = null
|
||||
private var hasPhysicalController = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupOnScreenController()
|
||||
setupControllerDetection()
|
||||
hideSystemUI()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
hideSystemUI()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11 (API 30) and above
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
window.insetsController?.let { controller ->
|
||||
controller.hide(WindowInsets.Type.systemBars())
|
||||
controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
// Android 10 and below
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupOnScreenController() {
|
||||
// Create on-screen controller
|
||||
onScreenController = OnScreenController(this).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
// Create input bridge
|
||||
controllerBridge = ControllerInputBridge()
|
||||
onScreenController?.setControllerListener(controllerBridge!!)
|
||||
|
||||
// Add to layout
|
||||
mLayout?.addView(onScreenController)
|
||||
|
||||
// Check for existing controllers and show/hide accordingly
|
||||
updateControllerVisibility()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Register virtual controller after SDL is initialized
|
||||
// Use a delay to ensure SDL is fully ready
|
||||
mLayout?.postDelayed({
|
||||
registerVirtualController()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
private fun registerVirtualController() {
|
||||
try {
|
||||
// Register the virtual on-screen controller as a joystick device
|
||||
// Device ID: -2, Name: "On-Screen Controller"
|
||||
org.libsdl.app.SDLControllerManager.nativeAddJoystick(
|
||||
-2, // device_id
|
||||
"On-Screen Controller", // name
|
||||
"Virtual touchscreen controller", // desc
|
||||
0x045e, // vendor_id (Microsoft)
|
||||
0x028e, // product_id (Xbox 360 Controller)
|
||||
false, // is_accelerometer
|
||||
0xFFFF, // button_mask (all buttons)
|
||||
6, // naxes (left X/Y, right X/Y, left trigger, right trigger)
|
||||
0x3F, // axis_mask (6 axes)
|
||||
0, // nhats
|
||||
0 // nballs
|
||||
)
|
||||
android.util.Log.d("MainActivity", "Virtual controller registered successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to register virtual controller: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupControllerDetection() {
|
||||
inputManager = getSystemService(Context.INPUT_SERVICE) as InputManager
|
||||
inputManager?.registerInputDeviceListener(this, null)
|
||||
|
||||
// Check for already connected controllers
|
||||
checkForPhysicalControllers()
|
||||
}
|
||||
|
||||
private fun checkForPhysicalControllers() {
|
||||
val deviceIds = inputManager?.inputDeviceIds ?: return
|
||||
hasPhysicalController = deviceIds.any { deviceId ->
|
||||
val device = inputManager?.getInputDevice(deviceId)
|
||||
isGameController(device)
|
||||
}
|
||||
updateControllerVisibility()
|
||||
}
|
||||
|
||||
private fun isGameController(device: InputDevice?): Boolean {
|
||||
if (device == null) return false
|
||||
|
||||
val sources = device.sources
|
||||
|
||||
// Check if device is a gamepad or joystick
|
||||
return ((sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) ||
|
||||
((sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)
|
||||
}
|
||||
|
||||
private fun updateControllerVisibility() {
|
||||
// Show on-screen controller only if no physical controller is connected
|
||||
val shouldShow = !hasPhysicalController
|
||||
|
||||
if (shouldShow != isControllerVisible) {
|
||||
isControllerVisible = shouldShow
|
||||
onScreenController?.visibility = if (shouldShow) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
// InputDeviceListener callbacks
|
||||
override fun onInputDeviceAdded(deviceId: Int) {
|
||||
val device = inputManager?.getInputDevice(deviceId)
|
||||
if (isGameController(device)) {
|
||||
hasPhysicalController = true
|
||||
updateControllerVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInputDeviceRemoved(deviceId: Int) {
|
||||
// Recheck all devices to see if any controllers remain
|
||||
checkForPhysicalControllers()
|
||||
}
|
||||
|
||||
override fun onInputDeviceChanged(deviceId: Int) {
|
||||
// Recheck all devices in case configuration changed
|
||||
checkForPhysicalControllers()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
// Unregister virtual controller
|
||||
try {
|
||||
org.libsdl.app.SDLControllerManager.nativeRemoveJoystick(-2)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Failed to unregister virtual controller: ${e.message}")
|
||||
}
|
||||
|
||||
inputManager?.unregisterInputDeviceListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
// Manual control methods (for settings/preferences)
|
||||
fun toggleOnScreenController() {
|
||||
isControllerVisible = !isControllerVisible
|
||||
onScreenController?.visibility = if (isControllerVisible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun showOnScreenController() {
|
||||
isControllerVisible = true
|
||||
onScreenController?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
fun hideOnScreenController() {
|
||||
isControllerVisible = false
|
||||
onScreenController?.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun forceUpdateControllerVisibility() {
|
||||
checkForPhysicalControllers()
|
||||
}
|
||||
|
||||
override fun getLibraries(): Array<String> = arrayOf(
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
package com.izzy2lost.x1box
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class OnScreenController @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val buttons = mutableMapOf<Button, ButtonState>()
|
||||
private val sticks = mutableMapOf<Stick, StickState>()
|
||||
|
||||
private var controllerListener: ControllerListener? = null
|
||||
|
||||
enum class Button {
|
||||
A, B, X, Y,
|
||||
DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT,
|
||||
LEFT_TRIGGER, RIGHT_TRIGGER,
|
||||
LEFT_BUMPER, RIGHT_BUMPER,
|
||||
START, BACK,
|
||||
LEFT_STICK_BUTTON, RIGHT_STICK_BUTTON,
|
||||
BLACK, WHITE
|
||||
}
|
||||
|
||||
enum class Stick {
|
||||
LEFT, RIGHT
|
||||
}
|
||||
|
||||
data class ButtonState(
|
||||
val center: PointF,
|
||||
val radius: Float,
|
||||
var isPressed: Boolean = false
|
||||
)
|
||||
|
||||
data class StickState(
|
||||
val center: PointF,
|
||||
val radius: Float,
|
||||
val deadZone: Float = 0.2f,
|
||||
var currentPos: PointF = PointF(0f, 0f),
|
||||
var isPressed: Boolean = false,
|
||||
var activePointerId: Int = -1
|
||||
)
|
||||
|
||||
interface ControllerListener {
|
||||
fun onButtonPressed(button: Button)
|
||||
fun onButtonReleased(button: Button)
|
||||
fun onStickMoved(stick: Stick, x: Float, y: Float)
|
||||
fun onStickPressed(stick: Stick)
|
||||
fun onStickReleased(stick: Stick)
|
||||
}
|
||||
|
||||
init {
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
}
|
||||
|
||||
fun setControllerListener(listener: ControllerListener) {
|
||||
this.controllerListener = listener
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
initializeControls(w, h)
|
||||
}
|
||||
|
||||
private fun initializeControls(width: Int, height: Int) {
|
||||
val w = width.toFloat()
|
||||
val h = height.toFloat()
|
||||
|
||||
// Button sizes - made D-pad smaller
|
||||
val faceButtonRadius = w * 0.035f
|
||||
val dpadButtonRadius = w * 0.025f
|
||||
val shoulderButtonRadius = w * 0.042f
|
||||
val smallButtonRadius = w * 0.022f
|
||||
val stickRadius = w * 0.07f
|
||||
|
||||
// Face buttons (right side) - A, B, X, Y in diamond formation
|
||||
val faceButtonCenterX = w * 0.88f
|
||||
val faceButtonCenterY = h * 0.55f
|
||||
val faceButtonSpacing = w * 0.07f
|
||||
|
||||
buttons[Button.A] = ButtonState(
|
||||
PointF(faceButtonCenterX, faceButtonCenterY + faceButtonSpacing),
|
||||
faceButtonRadius
|
||||
)
|
||||
buttons[Button.B] = ButtonState(
|
||||
PointF(faceButtonCenterX + faceButtonSpacing, faceButtonCenterY),
|
||||
faceButtonRadius
|
||||
)
|
||||
buttons[Button.X] = ButtonState(
|
||||
PointF(faceButtonCenterX - faceButtonSpacing, faceButtonCenterY),
|
||||
faceButtonRadius
|
||||
)
|
||||
buttons[Button.Y] = ButtonState(
|
||||
PointF(faceButtonCenterX, faceButtonCenterY - faceButtonSpacing),
|
||||
faceButtonRadius
|
||||
)
|
||||
|
||||
// D-Pad (bottom left corner) - smaller buttons
|
||||
val dpadCenterX = w * 0.12f
|
||||
val dpadCenterY = h * 0.85f
|
||||
val dpadSpacing = w * 0.045f
|
||||
|
||||
buttons[Button.DPAD_UP] = ButtonState(
|
||||
PointF(dpadCenterX, dpadCenterY - dpadSpacing),
|
||||
dpadButtonRadius
|
||||
)
|
||||
buttons[Button.DPAD_DOWN] = ButtonState(
|
||||
PointF(dpadCenterX, dpadCenterY + dpadSpacing),
|
||||
dpadButtonRadius
|
||||
)
|
||||
buttons[Button.DPAD_LEFT] = ButtonState(
|
||||
PointF(dpadCenterX - dpadSpacing, dpadCenterY),
|
||||
dpadButtonRadius
|
||||
)
|
||||
buttons[Button.DPAD_RIGHT] = ButtonState(
|
||||
PointF(dpadCenterX + dpadSpacing, dpadCenterY),
|
||||
dpadButtonRadius
|
||||
)
|
||||
|
||||
// Shoulder buttons (triggers and bumpers)
|
||||
buttons[Button.LEFT_TRIGGER] = ButtonState(
|
||||
PointF(w * 0.12f, h * 0.08f),
|
||||
shoulderButtonRadius
|
||||
)
|
||||
buttons[Button.RIGHT_TRIGGER] = ButtonState(
|
||||
PointF(w * 0.88f, h * 0.08f),
|
||||
shoulderButtonRadius
|
||||
)
|
||||
buttons[Button.LEFT_BUMPER] = ButtonState(
|
||||
PointF(w * 0.12f, h * 0.16f),
|
||||
shoulderButtonRadius
|
||||
)
|
||||
buttons[Button.RIGHT_BUMPER] = ButtonState(
|
||||
PointF(w * 0.88f, h * 0.16f),
|
||||
shoulderButtonRadius
|
||||
)
|
||||
|
||||
// Center buttons
|
||||
buttons[Button.START] = ButtonState(
|
||||
PointF(w * 0.58f, h * 0.4f),
|
||||
smallButtonRadius
|
||||
)
|
||||
buttons[Button.BACK] = ButtonState(
|
||||
PointF(w * 0.42f, h * 0.4f),
|
||||
smallButtonRadius
|
||||
)
|
||||
|
||||
// Analog sticks - right stick moved down
|
||||
sticks[Stick.LEFT] = StickState(
|
||||
PointF(w * 0.18f, h * 0.45f),
|
||||
stickRadius
|
||||
)
|
||||
sticks[Stick.RIGHT] = StickState(
|
||||
PointF(w * 0.62f, h * 0.82f),
|
||||
stickRadius
|
||||
)
|
||||
|
||||
// Black and White buttons - moved to right of right analog stick
|
||||
buttons[Button.WHITE] = ButtonState(
|
||||
PointF(w * 0.75f, h * 0.78f),
|
||||
smallButtonRadius
|
||||
)
|
||||
buttons[Button.BLACK] = ButtonState(
|
||||
PointF(w * 0.75f, h * 0.86f),
|
||||
smallButtonRadius
|
||||
)
|
||||
|
||||
// Stick buttons
|
||||
buttons[Button.LEFT_STICK_BUTTON] = ButtonState(
|
||||
sticks[Stick.LEFT]!!.center,
|
||||
stickRadius * 0.3f
|
||||
)
|
||||
buttons[Button.RIGHT_STICK_BUTTON] = ButtonState(
|
||||
sticks[Stick.RIGHT]!!.center,
|
||||
stickRadius * 0.3f
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Draw analog sticks
|
||||
sticks.forEach { (stick, state) ->
|
||||
// Outer circle
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeWidth = 4f
|
||||
paint.color = Color.argb(100, 255, 255, 255)
|
||||
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
|
||||
|
||||
// Dead zone circle
|
||||
paint.color = Color.argb(50, 255, 255, 255)
|
||||
canvas.drawCircle(state.center.x, state.center.y, state.radius * state.deadZone, paint)
|
||||
|
||||
// Stick position
|
||||
val stickX = state.center.x + state.currentPos.x * state.radius
|
||||
val stickY = state.center.y + state.currentPos.y * state.radius
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = if (state.isPressed) {
|
||||
Color.argb(200, 100, 150, 255)
|
||||
} else {
|
||||
Color.argb(150, 200, 200, 200)
|
||||
}
|
||||
canvas.drawCircle(stickX, stickY, state.radius * 0.4f, paint)
|
||||
}
|
||||
|
||||
// Draw buttons
|
||||
buttons.forEach { (button, state) ->
|
||||
// Skip stick buttons as they're drawn with sticks
|
||||
if (button == Button.LEFT_STICK_BUTTON || button == Button.RIGHT_STICK_BUTTON) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = when {
|
||||
state.isPressed -> getButtonPressedColor(button)
|
||||
else -> getButtonColor(button)
|
||||
}
|
||||
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
|
||||
|
||||
// Button outline
|
||||
paint.style = Paint.Style.STROKE
|
||||
paint.strokeWidth = 3f
|
||||
paint.color = Color.argb(150, 255, 255, 255)
|
||||
canvas.drawCircle(state.center.x, state.center.y, state.radius, paint)
|
||||
|
||||
// Button labels
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = Color.WHITE
|
||||
paint.textSize = state.radius * 0.8f
|
||||
paint.textAlign = Paint.Align.CENTER
|
||||
val label = getButtonLabel(button)
|
||||
canvas.drawText(label, state.center.x, state.center.y + state.radius * 0.3f, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonColor(button: Button): Int {
|
||||
return when (button) {
|
||||
Button.A -> Color.argb(150, 100, 200, 100)
|
||||
Button.B -> Color.argb(150, 200, 100, 100)
|
||||
Button.X -> Color.argb(150, 100, 150, 255)
|
||||
Button.Y -> Color.argb(150, 255, 255, 100)
|
||||
Button.BLACK -> Color.argb(150, 50, 50, 50)
|
||||
Button.WHITE -> Color.argb(150, 220, 220, 220)
|
||||
else -> Color.argb(120, 150, 150, 150)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonPressedColor(button: Button): Int {
|
||||
return when (button) {
|
||||
Button.A -> Color.argb(255, 100, 255, 100)
|
||||
Button.B -> Color.argb(255, 255, 100, 100)
|
||||
Button.X -> Color.argb(255, 100, 150, 255)
|
||||
Button.Y -> Color.argb(255, 255, 255, 100)
|
||||
Button.BLACK -> Color.argb(255, 80, 80, 80)
|
||||
Button.WHITE -> Color.argb(255, 255, 255, 255)
|
||||
else -> Color.argb(200, 200, 200, 200)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getButtonLabel(button: Button): String {
|
||||
return when (button) {
|
||||
Button.A -> "A"
|
||||
Button.B -> "B"
|
||||
Button.X -> "X"
|
||||
Button.Y -> "Y"
|
||||
Button.DPAD_UP -> "↑"
|
||||
Button.DPAD_DOWN -> "↓"
|
||||
Button.DPAD_LEFT -> "←"
|
||||
Button.DPAD_RIGHT -> "→"
|
||||
Button.LEFT_TRIGGER -> "LT"
|
||||
Button.RIGHT_TRIGGER -> "RT"
|
||||
Button.LEFT_BUMPER -> "LB"
|
||||
Button.RIGHT_BUMPER -> "RB"
|
||||
Button.START -> "▶"
|
||||
Button.BACK -> "◀"
|
||||
Button.BLACK -> "BK"
|
||||
Button.WHITE -> "WH"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
val x = event.getX(pointerIndex)
|
||||
val y = event.getY(pointerIndex)
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
handleTouchDown(x, y, pointerId)
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
for (i in 0 until event.pointerCount) {
|
||||
handleTouchMove(
|
||||
event.getX(i),
|
||||
event.getY(i),
|
||||
event.getPointerId(i)
|
||||
)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
handleTouchUp(pointerId)
|
||||
}
|
||||
}
|
||||
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleTouchDown(x: Float, y: Float, pointerId: Int) {
|
||||
// Check sticks first
|
||||
sticks.forEach { (stick, state) ->
|
||||
if (state.activePointerId == -1 && isPointInCircle(x, y, state.center, state.radius)) {
|
||||
state.activePointerId = pointerId
|
||||
updateStickPosition(stick, state, x, y)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check buttons
|
||||
buttons.forEach { (button, state) ->
|
||||
if (isPointInCircle(x, y, state.center, state.radius)) {
|
||||
state.isPressed = true
|
||||
controllerListener?.onButtonPressed(button)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTouchMove(x: Float, y: Float, pointerId: Int) {
|
||||
sticks.forEach { (stick, state) ->
|
||||
if (state.activePointerId == pointerId) {
|
||||
updateStickPosition(stick, state, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTouchUp(pointerId: Int) {
|
||||
// Release sticks
|
||||
sticks.forEach { (stick, state) ->
|
||||
if (state.activePointerId == pointerId) {
|
||||
state.activePointerId = -1
|
||||
state.currentPos = PointF(0f, 0f)
|
||||
controllerListener?.onStickMoved(stick, 0f, 0f)
|
||||
if (state.isPressed) {
|
||||
state.isPressed = false
|
||||
controllerListener?.onStickReleased(stick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Release buttons
|
||||
buttons.forEach { (button, state) ->
|
||||
if (state.isPressed) {
|
||||
state.isPressed = false
|
||||
controllerListener?.onButtonReleased(button)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStickPosition(stick: Stick, state: StickState, x: Float, y: Float) {
|
||||
val dx = x - state.center.x
|
||||
val dy = y - state.center.y
|
||||
val distance = sqrt(dx.pow(2) + dy.pow(2))
|
||||
|
||||
if (distance > state.radius) {
|
||||
// Clamp to circle boundary
|
||||
state.currentPos.x = (dx / distance)
|
||||
state.currentPos.y = (dy / distance)
|
||||
} else {
|
||||
// Normalize to -1..1 range
|
||||
state.currentPos.x = dx / state.radius
|
||||
state.currentPos.y = dy / state.radius
|
||||
}
|
||||
|
||||
// Apply dead zone
|
||||
val magnitude = sqrt(state.currentPos.x.pow(2) + state.currentPos.y.pow(2))
|
||||
if (magnitude < state.deadZone) {
|
||||
state.currentPos.x = 0f
|
||||
state.currentPos.y = 0f
|
||||
}
|
||||
|
||||
controllerListener?.onStickMoved(stick, state.currentPos.x, state.currentPos.y)
|
||||
}
|
||||
|
||||
private fun isPointInCircle(x: Float, y: Float, center: PointF, radius: Float): Boolean {
|
||||
val dx = x - center.x
|
||||
val dy = y - center.y
|
||||
return sqrt(dx.pow(2) + dy.pow(2)) <= radius
|
||||
}
|
||||
|
||||
fun setVisibility(visible: Boolean) {
|
||||
visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
@@ -12,38 +12,48 @@
|
||||
app:strokeColor="@color/xemu_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/game_cover_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="190dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/setup_wizard_path_background"
|
||||
android:contentDescription="@string/library_open_game"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@android:drawable/ic_menu_report_image" />
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@android:drawable/ic_menu_report_image"
|
||||
app:layout_constraintDimensionRatio="2:3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/game_cover_name_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/game_cover_image" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/game_cover_size_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="@color/xemu_text_muted" />
|
||||
</LinearLayout>
|
||||
android:textColor="@color/xemu_text_muted"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/game_cover_name_text" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -290,4 +290,10 @@ static void usb_xid_register_types(void)
|
||||
type_register_static(&usb_xbox_gamepad_s_info);
|
||||
}
|
||||
|
||||
type_init(usb_xid_register_types)
|
||||
type_init(usb_xid_register_types)
|
||||
|
||||
#ifdef __ANDROID__
|
||||
void xemu_android_force_xid_gamepad_link(void)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
|
||||
+22
-4
@@ -167,6 +167,10 @@ static void check_and_reset_in_range(int *btn, int min, int max,
|
||||
|
||||
static void xemu_input_bindings_set_in_range(ControllerState *con)
|
||||
{
|
||||
if (!con->controller_map) {
|
||||
return;
|
||||
}
|
||||
|
||||
#define CHECK_RESET_BUTTON(btn) \
|
||||
check_and_reset_in_range(&con->controller_map->controller_mapping.btn, \
|
||||
SDL_CONTROLLER_BUTTON_INVALID, \
|
||||
@@ -213,7 +217,14 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
|
||||
|
||||
char guid[35] = { 0 };
|
||||
SDL_JoystickGetGUIDString(con->sdl_joystick_guid, guid, sizeof(guid));
|
||||
if (!xemu_settings_load_gamepad_mapping(guid, &con->controller_map)) {
|
||||
bool added_mapping =
|
||||
xemu_settings_load_gamepad_mapping(guid, &con->controller_map);
|
||||
if (!con->controller_map) {
|
||||
fprintf(stderr, "Failed to load gamepad mapping for %s\n", guid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!added_mapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,7 +232,7 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
|
||||
// have been reallocated. Any gamepad mapping pointers for other controllers
|
||||
// are now invalid, and need to be reloaded.
|
||||
ControllerState *iter, *next;
|
||||
bool is_new_mapping;
|
||||
bool iter_is_new_mapping;
|
||||
QTAILQ_FOREACH_SAFE (iter, &available_controllers, entry, next) {
|
||||
if (iter == con || iter->type != INPUT_DEVICE_SDL_GAMECONTROLLER) {
|
||||
continue;
|
||||
@@ -230,9 +241,9 @@ static void xemu_input_bindings_reload_map(ControllerState *con)
|
||||
memset(guid, 0, sizeof(guid));
|
||||
SDL_JoystickGetGUIDString(iter->sdl_joystick_guid, guid, sizeof(guid));
|
||||
|
||||
is_new_mapping =
|
||||
iter_is_new_mapping =
|
||||
xemu_settings_load_gamepad_mapping(guid, &iter->controller_map);
|
||||
assert(!is_new_mapping &&
|
||||
assert(!iter_is_new_mapping &&
|
||||
"Existing controller GUIDs should exist in the config");
|
||||
|
||||
xemu_input_bindings_set_in_range(iter);
|
||||
@@ -566,6 +577,9 @@ void xemu_input_update_sdl_controller_state(ControllerState *state)
|
||||
{
|
||||
state->buttons = 0;
|
||||
memset(state->axis, 0, sizeof(state->axis));
|
||||
if (!state->controller_map) {
|
||||
return;
|
||||
}
|
||||
|
||||
#define SDL_MASK_BUTTON(state, btn, idx) \
|
||||
(SDL_GameControllerGetButton( \
|
||||
@@ -636,6 +650,10 @@ void xemu_input_update_rumble(ControllerState *state)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state->controller_map) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state->controller_map->enable_rumble) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user