android: input: Add support for gamepads.

This commit is contained in:
bunnei 2019-08-20 19:39:09 -04:00
parent 4425c74548
commit 655fb2da7b
4 changed files with 438 additions and 31 deletions

View File

@ -390,6 +390,7 @@ public final class NativeLibrary {
public static final int STICK_C_RIGHT = 772;
public static final int TRIGGER_L = 773;
public static final int TRIGGER_R = 774;
public static final int DPAD = 780;
}
/**

View File

@ -32,6 +32,7 @@ import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.R;
import org.citra.citra_android.fragments.EmulationFragment;
import org.citra.citra_android.fragments.MenuFragment;
import org.citra.citra_android.model.settings.view.InputBindingSetting;
import org.citra.citra_android.ui.main.MainPresenter;
import org.citra.citra_android.utils.Animations;
import org.citra.citra_android.utils.ControllerMappingHelper;
@ -462,6 +463,7 @@ public final class EmulationActivity extends AppCompatActivity {
}
int action;
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
@ -481,7 +483,7 @@ public final class EmulationActivity extends AppCompatActivity {
return false;
}
InputDevice input = event.getDevice();
return NativeLibrary.onGamePadEvent(input.getDescriptor(), event.getKeyCode(), action);
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
}
private void toggleControls() {
@ -586,37 +588,88 @@ public final class EmulationActivity extends AppCompatActivity {
}
// Don't attempt to do anything if we are disconnecting a device.
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL)
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
return true;
}
InputDevice input = event.getDevice();
List<InputDevice.MotionRange> motions = input.getMotionRanges();
float[] axisValues = {0.0f, 0.0f};
float[] axisValuesCirclePad = {0.0f, 0.0f};
float[] axisValuesCStick = {0.0f, 0.0f};
float[] axisValuesDPad = {0.0f, 0.0f};
boolean isTriggerPressedL = false;
boolean isTriggerPressedR = false;
boolean isTriggerPressedZL = false;
boolean isTriggerPressedZR = false;
for (InputDevice.MotionRange range : motions) {
boolean consumed = false;
int axis = range.getAxis();
float origValue = event.getAxisValue(axis);
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
if (axis == AXIS_X || axis == AXIS_Z) {
axisValues[0] = value;
} else if (axis == AXIS_Y || axis == AXIS_RZ) {
axisValues[1] = value;
if (nextMapping == -1 || guestOrientation == -1) {
// Axis is unmapped
continue;
}
// If the input is still in the "flat" area, that means it's really zero.
// This is used to compensate for imprecision in joysticks.
if (Math.abs(axisValues[0]) > range.getFlat() || Math.abs(axisValues[1]) > range.getFlat()) {
consumed = NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, axisValues[0], axisValues[1]);
} else {
consumed = NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, 0.0f, 0.0f);
if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
// Skip joystick wobble
value = 0.f;
}
return NativeLibrary.onGamePadAxisEvent(input.getDescriptor(), axis, value) || consumed;
if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
axisValuesCirclePad[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
axisValuesCStick[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
axisValuesDPad[guestOrientation] = value;
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L && value != 0.f) {
isTriggerPressedL = true;
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R && value != 0.f) {
isTriggerPressedR = true;
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL && value != 0.f) {
isTriggerPressedZL = true;
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR && value != 0.f) {
isTriggerPressedZR = true;
}
}
return false;
// Circle-Pad and C-Stick status
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
// Triggers L/R and ZL/ZR
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
// Work-around to allow D-pad axis to be bound to emulated buttons
if (axisValuesDPad[0] == 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
} if (axisValuesDPad[0] < 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
} if (axisValuesDPad[0] > 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
}
if (axisValuesDPad[1] == 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
} if (axisValuesDPad[1] < 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
} if (axisValuesDPad[1] > 0.f) {
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
}
return true;
}
public boolean isActivityRecreated() {

View File

@ -1,9 +1,21 @@
package org.citra.citra_android.model.settings.view;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.widget.Toast;
import org.citra.citra_android.NativeLibrary;
import org.citra.citra_android.R;
import org.citra.citra_android.DolphinApplication;
import org.citra.citra_android.model.settings.Setting;
import org.citra.citra_android.model.settings.StringSetting;
import org.citra.citra_android.utils.SettingsFile;
public final class InputBindingSetting extends SettingsItem {
private static final String INPUT_MAPPING_PREFIX = "InputMapping";
public InputBindingSetting(String key, String section, int file, int titleId, Setting setting) {
super(key, section, file, setting, titleId, 0);
}
@ -18,20 +30,355 @@ public final class InputBindingSetting extends SettingsItem {
}
/**
* Write a value to the backing string. If that string was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param bind The input that will be bound
* @return null if overwritten successfully; otherwise, a newly created StringSetting.
* Returns true if this key is for the 3DS Circle Pad
*/
public StringSetting setValue(String bind) {
private boolean IsCirclePad()
{
switch (getKey()) {
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
*/
public boolean IsHorizontalOrientation()
{
switch (getKey()) {
case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS C-Stick
*/
private boolean IsCStick()
{
switch (getKey()) {
case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS D-Pad
*/
private boolean IsDPad()
{
switch (getKey()) {
case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
return true;
}
return false;
}
/**
* Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
* triggers on the 3DS, but we support them as such on a physical gamepad.
*/
public boolean IsTrigger() {
switch (getKey()) {
case SettingsFile.KEY_BUTTON_L:
case SettingsFile.KEY_BUTTON_R:
case SettingsFile.KEY_BUTTON_ZL:
case SettingsFile.KEY_BUTTON_ZR:
return true;
}
return false;
}
/**
* Returns true if a gamepad axis can be used to map this key.
*/
public boolean IsAxisMappingSupported()
{
return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
}
/**
* Returns true if a gamepad button can be used to map this key.
*/
private boolean IsButtonMappingSupported()
{
return !IsAxisMappingSupported() || IsTrigger();
}
/**
* Returns the Citra button code for the settings key.
*/
private int getButtonCode() {
switch (getKey()) {
case SettingsFile.KEY_BUTTON_A:
return NativeLibrary.ButtonType.BUTTON_A;
case SettingsFile.KEY_BUTTON_B:
return NativeLibrary.ButtonType.BUTTON_B;
case SettingsFile.KEY_BUTTON_X:
return NativeLibrary.ButtonType.BUTTON_X;
case SettingsFile.KEY_BUTTON_Y:
return NativeLibrary.ButtonType.BUTTON_Y;
case SettingsFile.KEY_BUTTON_L:
return NativeLibrary.ButtonType.TRIGGER_L;
case SettingsFile.KEY_BUTTON_R:
return NativeLibrary.ButtonType.TRIGGER_R;
case SettingsFile.KEY_BUTTON_ZL:
return NativeLibrary.ButtonType.BUTTON_ZL;
case SettingsFile.KEY_BUTTON_ZR:
return NativeLibrary.ButtonType.BUTTON_ZR;
case SettingsFile.KEY_BUTTON_SELECT:
return NativeLibrary.ButtonType.BUTTON_SELECT;
case SettingsFile.KEY_BUTTON_START:
return NativeLibrary.ButtonType.BUTTON_START;
case SettingsFile.KEY_BUTTON_UP:
return NativeLibrary.ButtonType.DPAD_UP;
case SettingsFile.KEY_BUTTON_DOWN:
return NativeLibrary.ButtonType.DPAD_DOWN;
case SettingsFile.KEY_BUTTON_LEFT:
return NativeLibrary.ButtonType.DPAD_LEFT;
case SettingsFile.KEY_BUTTON_RIGHT:
return NativeLibrary.ButtonType.DPAD_RIGHT;
}
return -1;
}
/**
* Returns the settings key for the specified Citra button code.
*/
private static String getButtonKey(int buttonCode) {
switch (buttonCode) {
case NativeLibrary.ButtonType.BUTTON_A:
return SettingsFile.KEY_BUTTON_A;
case NativeLibrary.ButtonType.BUTTON_B:
return SettingsFile.KEY_BUTTON_B;
case NativeLibrary.ButtonType.BUTTON_X:
return SettingsFile.KEY_BUTTON_X;
case NativeLibrary.ButtonType.BUTTON_Y:
return SettingsFile.KEY_BUTTON_Y;
case NativeLibrary.ButtonType.TRIGGER_L:
return SettingsFile.KEY_BUTTON_L;
case NativeLibrary.ButtonType.TRIGGER_R:
return SettingsFile.KEY_BUTTON_R;
case NativeLibrary.ButtonType.BUTTON_ZL:
return SettingsFile.KEY_BUTTON_ZL;
case NativeLibrary.ButtonType.BUTTON_ZR:
return SettingsFile.KEY_BUTTON_ZR;
case NativeLibrary.ButtonType.BUTTON_SELECT:
return SettingsFile.KEY_BUTTON_SELECT;
case NativeLibrary.ButtonType.BUTTON_START:
return SettingsFile.KEY_BUTTON_START;
case NativeLibrary.ButtonType.DPAD_UP:
return SettingsFile.KEY_BUTTON_UP;
case NativeLibrary.ButtonType.DPAD_DOWN:
return SettingsFile.KEY_BUTTON_DOWN;
case NativeLibrary.ButtonType.DPAD_LEFT:
return SettingsFile.KEY_BUTTON_LEFT;
case NativeLibrary.ButtonType.DPAD_RIGHT:
return SettingsFile.KEY_BUTTON_RIGHT;
}
return "";
}
/**
* Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
* settings on re-mapping or clearing of a setting.
*/
private String getReverseKey() {
String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
if (IsAxisMappingSupported() && !IsTrigger()) {
// Triggers are the only axis-supported mappings without orientation
reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
}
return reverseKey;
}
/**
* Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
*/
public void removeOldMapping() {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Try remove all possible keys we wrote for this setting
String oldKey = preferences.getString(getReverseKey(), "");
if (oldKey != "") {
editor.remove(getKey()); // Used for ui text
editor.remove(oldKey); // Used for button mapping
editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
editor.remove(oldKey + "_GuestButton"); // Used for axis button
}
// Apply changes
editor.apply();
}
/**
* Helper function to get the settings key for an gamepad button.
*/
public static String getInputButtonKey(int keyCode) {
return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
}
/**
* Helper function to get the settings key for an gamepad axis.
*/
public static String getInputAxisKey(int axis) {
return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
}
/**
* Helper function to get the settings key for an gamepad axis button (stick or trigger).
*/
public static String getInputAxisButtonKey(int axis) {
return getInputAxisKey(axis) + "_GuestButton";
}
/**
* Helper function to get the settings key for an gamepad axis orientation.
*/
public static String getInputAxisOrientationKey(int axis) {
return getInputAxisKey(axis) + "_GuestOrientation";
}
/**
* Helper function to write a gamepad button mapping for the setting.
*/
private void WriteButtonMapping(String key) {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Remove mapping for another setting using this input
int oldButtonCode = preferences.getInt(key, -1);
if (oldButtonCode != -1) {
String oldKey = getButtonKey(oldButtonCode);
editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
}
// Cleanup old mapping for this setting
removeOldMapping();
// Write new mapping
editor.putInt(key, getButtonCode());
// Write next reverse mapping for future cleanup
editor.putString(getReverseKey(), key);
// Apply changes
editor.apply();
}
/**
* Helper function to write a gamepad axis mapping for the setting.
*/
private void WriteAxisMapping(int axis, int value) {
// Get preferences editor
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
// Cleanup old mapping
removeOldMapping();
// Write new mapping
editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
editor.putInt(getInputAxisButtonKey(axis), value);
// Write next reverse mapping for future cleanup
editor.putString(getReverseKey(), getInputAxisKey(axis));
// Apply changes
editor.apply();
}
/**
* Saves the provided key input setting as an Android preference.
*
* @param keyEvent KeyEvent of this key press.
*/
public void onKeyInput(KeyEvent keyEvent)
{
if (!IsButtonMappingSupported()) {
Toast.makeText(DolphinApplication.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
return;
}
InputDevice device = keyEvent.getDevice();
WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
setUiString(uiString);
}
/**
* Saves the provided motion input setting as an Android preference.
*
* @param device InputDevice from which the input event originated.
* @param motionRange MotionRange of the movement
* @param axisDir Either '-' or '+' (currently unused)
*/
public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
char axisDir)
{
if (!IsAxisMappingSupported()) {
Toast.makeText(DolphinApplication.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
return;
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
int button;
if (IsCirclePad()) {
button = NativeLibrary.ButtonType.STICK_LEFT;
} else if (IsCStick()) {
button = NativeLibrary.ButtonType.STICK_C;
} else if (IsDPad()){
button = NativeLibrary.ButtonType.DPAD;
} else {
button = getButtonCode();
}
WriteAxisMapping(motionRange.getAxis(), button);
String uiString = device.getName() + ": Axis " + motionRange.getAxis();
setUiString(uiString);
editor.apply();
}
/**
* Sets the string to use in the configuration UI for the gamepad input.
*/
private StringSetting setUiString(String ui) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(DolphinApplication.getAppContext());
SharedPreferences.Editor editor = preferences.edit();
if (getSetting() == null) {
StringSetting setting = new StringSetting(getKey(), getSection(), getFile(), bind);
StringSetting setting = new StringSetting(getKey(), getSection(), getFile(), "");
setSetting(setting);
editor.putString(setting.getKey(), ui);
editor.apply();
return setting;
} else {
StringSetting setting = (StringSetting) getSetting();
setting.setValue(bind);
editor.putString(setting.getKey(), ui);
editor.apply();
return null;
}
}

View File

@ -250,16 +250,22 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
public void onInputBindingClick(final InputBindingSetting item, final int position) {
final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
dialog.setTitle(R.string.input_binding);
dialog.setMessage(String.format(mContext.getString(R.string.input_binding_description), mContext.getString(item.getNameId())));
int messageResId = R.string.input_binding_description;
if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
// Use specialized message for axis left/right or up/down
if (item.IsHorizontalOrientation()) {
messageResId = R.string.input_binding_description_horizontal_axis;
} else {
messageResId = R.string.input_binding_description_vertical_axis;
}
}
dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(R.string.cancel), this);
dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
{
item.setValue("");
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(item.getKey());
editor.apply();
item.removeOldMapping();
});
dialog.setOnDismissListener(dialog1 ->
{