From f1dec91ae1b787dd52d0c7d2ee131a55c8724a96 Mon Sep 17 00:00:00 2001 From: SSimco <37044560+SSimco@users.noreply.github.com> Date: Sun, 26 May 2024 17:51:32 +0300 Subject: [PATCH] Added input overlay --- src/android/app/src/main/AndroidManifest.xml | 11 +- .../src/main/cpp/AndroidEmulatedController.h | 12 + src/android/app/src/main/cpp/EmulationState.h | 5 + src/android/app/src/main/cpp/native-lib.cpp | 12 + .../info/cemu/Cemu/AudioSettingsFragment.java | 4 +- .../src/main/java/info/cemu/Cemu/Button.java | 75 +++ .../cemu/Cemu/ButtonStateChangeListener.java | 5 + .../main/java/info/cemu/Cemu/DPadInput.java | 295 +++++++++ .../Cemu/EmulatedControllerTypeAdapter.java | 2 +- .../info/cemu/Cemu/EmulationFragment.java | 38 +- .../cemu/Cemu/GraphicsSettingsFragment.java | 2 +- .../src/main/java/info/cemu/Cemu/Input.java | 90 +++ .../Cemu/InputOverlayConfigureActivity.java | 53 ++ .../Cemu/InputOverlaySettingsFragment.java | 101 ++++ .../Cemu/InputOverlaySettingsProvider.java | 277 +++++++++ .../cemu/Cemu/InputOverlaySurfaceView.java | 569 ++++++++++++++++++ .../info/cemu/Cemu/InputSettingsFragment.java | 2 + .../main/java/info/cemu/Cemu/Joystick.java | 130 ++++ .../java/info/cemu/Cemu/MainActivity.java | 2 +- .../java/info/cemu/Cemu/NativeLibrary.java | 4 + .../info/cemu/Cemu/ProgressResponseBody.java | 66 ++ .../java/info/cemu/Cemu/RectangleButton.java | 34 ++ .../main/java/info/cemu/Cemu/RoundButton.java | 38 ++ .../java/info/cemu/Cemu/SelectionAdapter.java | 13 +- .../Cemu/SingleSelectionRecyclerViewItem.java | 24 +- .../app/src/main/res/drawable/button_a.xml | 16 + .../main/res/drawable/button_a_pressed.xml | 16 + .../app/src/main/res/drawable/button_b.xml | 16 + .../main/res/drawable/button_b_pressed.xml | 16 + .../app/src/main/res/drawable/button_c.xml | 16 + .../main/res/drawable/button_c_pressed.xml | 16 + .../app/src/main/res/drawable/button_home.xml | 16 + .../main/res/drawable/button_home_pressed.xml | 16 + .../app/src/main/res/drawable/button_l.xml | 15 + .../main/res/drawable/button_l_pressed.xml | 15 + .../src/main/res/drawable/button_minus.xml | 16 + .../res/drawable/button_minus_pressed.xml | 16 + .../app/src/main/res/drawable/button_one.xml | 16 + .../main/res/drawable/button_one_pressed.xml | 16 + .../app/src/main/res/drawable/button_plus.xml | 16 + .../main/res/drawable/button_plus_pressed.xml | 16 + .../app/src/main/res/drawable/button_r.xml | 15 + .../main/res/drawable/button_r_pressed.xml | 15 + .../src/main/res/drawable/button_stick.xml | 22 + .../res/drawable/button_stick_pressed.xml | 22 + .../app/src/main/res/drawable/button_two.xml | 16 + .../main/res/drawable/button_two_pressed.xml | 16 + .../app/src/main/res/drawable/button_x.xml | 16 + .../main/res/drawable/button_x_pressed.xml | 16 + .../app/src/main/res/drawable/button_y.xml | 16 + .../main/res/drawable/button_y_pressed.xml | 16 + .../app/src/main/res/drawable/button_z.xml | 16 + .../main/res/drawable/button_z_pressed.xml | 16 + .../app/src/main/res/drawable/button_zl.xml | 15 + .../main/res/drawable/button_zl_pressed.xml | 15 + .../app/src/main/res/drawable/button_zr.xml | 15 + .../main/res/drawable/button_zr_pressed.xml | 15 + .../src/main/res/drawable/dpad_background.xml | 12 + .../app/src/main/res/drawable/dpad_button.xml | 11 + .../main/res/drawable/dpad_button_pressed.xml | 11 + .../app/src/main/res/drawable/ic_restart.xml | 10 + .../main/res/drawable/stick_background.xml | 16 + .../app/src/main/res/drawable/stick_inner.xml | 12 + .../main/res/drawable/stick_inner_pressed.xml | 12 + .../res/layout/activity_overlay_configure.xml | 38 ++ .../main/res/layout/fragment_emulation.xml | 13 + .../fragment_input_overlay_settings.xml | 16 + .../layout/layout_single_selection_item.xml | 20 +- .../res/navigation/nav_settings_graph.xml | 8 + .../app/src/main/res/values/colors.xml | 2 + .../app/src/main/res/values/strings.xml | 18 + src/input/emulated/EmulatedController.cpp | 24 + src/input/emulated/EmulatedController.h | 7 +- 73 files changed, 2581 insertions(+), 29 deletions(-) create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/Button.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ButtonStateChangeListener.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/DPadInput.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/Input.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputOverlayConfigureActivity.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsFragment.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsProvider.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySurfaceView.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/Joystick.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/ProgressResponseBody.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/RectangleButton.java create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/RoundButton.java create mode 100644 src/android/app/src/main/res/drawable/button_a.xml create mode 100644 src/android/app/src/main/res/drawable/button_a_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_b.xml create mode 100644 src/android/app/src/main/res/drawable/button_b_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_c.xml create mode 100644 src/android/app/src/main/res/drawable/button_c_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_home.xml create mode 100644 src/android/app/src/main/res/drawable/button_home_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_l.xml create mode 100644 src/android/app/src/main/res/drawable/button_l_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_minus.xml create mode 100644 src/android/app/src/main/res/drawable/button_minus_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_one.xml create mode 100644 src/android/app/src/main/res/drawable/button_one_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_plus.xml create mode 100644 src/android/app/src/main/res/drawable/button_plus_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_r.xml create mode 100644 src/android/app/src/main/res/drawable/button_r_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_stick.xml create mode 100644 src/android/app/src/main/res/drawable/button_stick_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_two.xml create mode 100644 src/android/app/src/main/res/drawable/button_two_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_x.xml create mode 100644 src/android/app/src/main/res/drawable/button_x_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_y.xml create mode 100644 src/android/app/src/main/res/drawable/button_y_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_z.xml create mode 100644 src/android/app/src/main/res/drawable/button_z_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_zl.xml create mode 100644 src/android/app/src/main/res/drawable/button_zl_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/button_zr.xml create mode 100644 src/android/app/src/main/res/drawable/button_zr_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/dpad_background.xml create mode 100644 src/android/app/src/main/res/drawable/dpad_button.xml create mode 100644 src/android/app/src/main/res/drawable/dpad_button_pressed.xml create mode 100644 src/android/app/src/main/res/drawable/ic_restart.xml create mode 100644 src/android/app/src/main/res/drawable/stick_background.xml create mode 100644 src/android/app/src/main/res/drawable/stick_inner.xml create mode 100644 src/android/app/src/main/res/drawable/stick_inner_pressed.xml create mode 100644 src/android/app/src/main/res/layout/activity_overlay_configure.xml create mode 100644 src/android/app/src/main/res/layout/fragment_input_overlay_settings.xml diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 11fee32e..85c49e40 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + + android:screenOrientation="userLandscape" + tools:ignore="DiscouragedApi" /> + (new AndroidEmulatedController(index)); return *controller; } + void setButtonValue(uint64 mappingId, bool value) + { + if (!m_emulatedController) + return; + m_emulatedController->setButtonValue(mappingId, value); + } + void setAxisValue(uint64 mappingId, float value) + { + if (!m_emulatedController) + return; + m_emulatedController->setAxisValue(mappingId, value); + } void setType(EmulatedController::Type type) { if (m_emulatedController && m_emulatedController->type() == type) diff --git a/src/android/app/src/main/cpp/EmulationState.h b/src/android/app/src/main/cpp/EmulationState.h index 18335b79..9fd6f0d0 100644 --- a/src/android/app/src/main/cpp/EmulationState.h +++ b/src/android/app/src/main/cpp/EmulationState.h @@ -110,6 +110,11 @@ class EmulationState { return AndroidEmulatedController::getAndroidEmulatedController(index).getMapping(mappingId); } + AndroidEmulatedController& getEmulatedController(size_t index) + { + return AndroidEmulatedController::getAndroidEmulatedController(index); + } + int getVPADControllersCount() { int vpadCount = 0; diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp index 5728f574..293d610b 100644 --- a/src/android/app/src/main/cpp/native-lib.cpp +++ b/src/android/app/src/main/cpp/native-lib.cpp @@ -401,3 +401,15 @@ Java_info_cemu_Cemu_NativeLibrary_getGraphicPackPresets(JNIEnv* env, [[maybe_unu { return getGraphicPresets(env, s_emulationState.getGraphicPack(id), id); } + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onOverlayButton([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint controllerIndex, jint mappingId, jboolean state) +{ + s_emulationState.getEmulatedController(controllerIndex).setButtonValue(mappingId, state); +} + +extern "C" JNIEXPORT void JNICALL +Java_info_cemu_Cemu_NativeLibrary_onOverlayAxis([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint controllerIndex, jint mappingId, jfloat value) +{ + s_emulationState.getEmulatedController(controllerIndex).setAxisValue(mappingId, value); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/AudioSettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/AudioSettingsFragment.java index c4e5bd03..d5da6c13 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/AudioSettingsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/AudioSettingsFragment.java @@ -28,7 +28,7 @@ public class AudioSettingsFragment extends Fragment { genericRecyclerViewAdapter.addRecyclerViewItem(tvDeviceCheckbox); var tvChannelsChoices = Stream.of(NativeLibrary.AUDIO_CHANNELS_MONO, NativeLibrary.AUDIO_CHANNELS_STEREO, NativeLibrary.AUDIO_CHANNELS_SURROUND) - .map(channels -> new SelectionAdapter.ChoiceItem<>(NativeLibrary.channelsToResourceNameId(channels), channels)) + .map(channels -> new SelectionAdapter.ChoiceItem<>(t -> t.setText(NativeLibrary.channelsToResourceNameId(channels)), channels)) .collect(Collectors.toList()); int tvChannels = NativeLibrary.getAudioDeviceChannels(true); SelectionAdapter tvChannelsSelectionAdapter = new SelectionAdapter<>(tvChannelsChoices, tvChannels); @@ -53,7 +53,7 @@ public class AudioSettingsFragment extends Fragment { checked -> NativeLibrary.setAudioDeviceEnabled(checked, true)); genericRecyclerViewAdapter.addRecyclerViewItem(padDeviceCheckbox); - var gamepadChannelsChoices = List.of(new SelectionAdapter.ChoiceItem<>(NativeLibrary.channelsToResourceNameId(NativeLibrary.AUDIO_CHANNELS_STEREO), NativeLibrary.AUDIO_CHANNELS_STEREO)); + var gamepadChannelsChoices = List.of(new SelectionAdapter.ChoiceItem<>(t -> t.setText(NativeLibrary.channelsToResourceNameId(NativeLibrary.AUDIO_CHANNELS_STEREO)), NativeLibrary.AUDIO_CHANNELS_STEREO)); int gamepadChannels = NativeLibrary.getAudioDeviceChannels(false); SelectionAdapter gamepadChannelsSelectionAdapter = new SelectionAdapter<>(gamepadChannelsChoices, gamepadChannels); SingleSelectionRecyclerViewItem gamepadChannelsModeSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.gamepad_channels), diff --git a/src/android/app/src/main/java/info/cemu/Cemu/Button.java b/src/android/app/src/main/java/info/cemu/Cemu/Button.java new file mode 100644 index 00000000..cee9858b --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/Button.java @@ -0,0 +1,75 @@ +package info.cemu.Cemu; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.MotionEvent; + +import androidx.annotation.DrawableRes; +import androidx.core.content.res.ResourcesCompat; + +import java.util.Objects; + +public abstract class Button extends Input { + protected Drawable iconPressed; + protected Drawable iconNotPressed; + protected Drawable icon; + protected int currentPointerId = -1; + private final ButtonStateChangeListener buttonStateChangeListener; + private final InputOverlaySurfaceView.OverlayButton button; + + public Button(Resources resources, @DrawableRes int pressedButtonId, @DrawableRes int notPressedButtonId, ButtonStateChangeListener buttonStateChangeListener, InputOverlaySurfaceView.OverlayButton button, InputOverlaySettingsProvider.InputOverlaySettings settings) { + super(settings); + iconPressed = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, pressedButtonId, null)); + iconPressed.setAlpha(settings.getAlpha()); + iconNotPressed = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, notPressedButtonId, null)); + iconNotPressed.setAlpha(settings.getAlpha()); + icon = iconNotPressed; + this.buttonStateChangeListener = buttonStateChangeListener; + this.button = button; + } + + @Override + public boolean onTouch(MotionEvent event) { + boolean stateUpdated = false; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + int pointerIndex = event.getActionIndex(); + int x = (int) event.getX(pointerIndex); + int y = (int) event.getY(pointerIndex); + if (isInside(x, y)) { + currentPointerId = event.getPointerId(pointerIndex); + updateState(true); + stateUpdated = true; + } + } + case MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + if (event.getPointerId(event.getActionIndex()) == currentPointerId) { + currentPointerId = -1; + updateState(false); + stateUpdated = true; + } + } + } + return stateUpdated; + } + + @Override + protected void drawInput(Canvas canvas) { + icon.draw(canvas); + } + + @Override + protected void resetInput() { + updateState(false); + } + + protected void updateState(boolean state) { + if (state) { + icon = iconPressed; + } else { + icon = iconNotPressed; + } + buttonStateChangeListener.onButtonStateChange(button, state); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ButtonStateChangeListener.java b/src/android/app/src/main/java/info/cemu/Cemu/ButtonStateChangeListener.java new file mode 100644 index 00000000..436a0eec --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ButtonStateChangeListener.java @@ -0,0 +1,5 @@ +package info.cemu.Cemu; + +public interface ButtonStateChangeListener { + void onButtonStateChange(InputOverlaySurfaceView.OverlayButton button, boolean state); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/DPadInput.java b/src/android/app/src/main/java/info/cemu/Cemu/DPadInput.java new file mode 100644 index 00000000..87b0f7c8 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/DPadInput.java @@ -0,0 +1,295 @@ +package info.cemu.Cemu; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.view.MotionEvent; + +import androidx.annotation.DrawableRes; +import androidx.core.content.res.ResourcesCompat; + +import java.util.Objects; +import java.util.function.Supplier; + +public class DPadInput extends Input { + Drawable iconDpadUp; + Drawable iconDpadUpPressed; + Drawable iconDpadUpNotPressed; + + Drawable iconDpadDown; + Drawable iconDpadDownPressed; + Drawable iconDpadDownNotPressed; + + Drawable iconDpadLeft; + Drawable iconDpadLeftPressed; + Drawable iconDpadLeftNotPressed; + + Drawable iconDpadRight; + Drawable iconDpadRightPressed; + Drawable iconDpadRightNotPressed; + Drawable background; + + enum DpadState { + NONE, UP, DOWN, LEFT, RIGHT, UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT, + } + + DpadState dpadState = DpadState.NONE; + + int centreX; + int centreY; + int radius; + int currentPointerId = -1; + + @FunctionalInterface + private interface ConfigureButtonInterface { + void configure(int circleXPos, int circleYPos, Drawable pressedButton, Drawable notPressedButton); + } + + private final ButtonStateChangeListener buttonStateChangeListener; + + @Override + protected void configure() { + var rect = settings.getRect(); + this.centreX = rect.centerX(); + this.centreY = rect.centerY(); + this.radius = Math.min(rect.width(), rect.height()) / 2; + + background.setAlpha(settings.getAlpha()); + background.setBounds( + centreX - radius, + centreY - radius, + centreX + radius, + centreY + radius); + + int buttonSize = (int) (radius * 0.5f); + ConfigureButtonInterface configureButton = (circleXPos, circleYPos, pressedButton, notPressedButton) -> { + pressedButton.setAlpha(settings.getAlpha()); + notPressedButton.setAlpha(settings.getAlpha()); + int left = circleXPos - buttonSize / 2; + int top = circleYPos - buttonSize / 2; + pressedButton.setBounds(left, top, left + buttonSize, top + buttonSize); + notPressedButton.setBounds(left, top, left + buttonSize, top + buttonSize); + }; + configureButton.configure( + centreX, + centreY - 2 * radius / 3, + iconDpadUpPressed, + iconDpadUp + ); + configureButton.configure( + centreX, + centreY + 2 * radius / 3, + iconDpadDownPressed, + iconDpadDownNotPressed + ); + configureButton.configure( + centreX - 2 * radius / 3, + centreY, + iconDpadLeftPressed, + iconDpadLeftNotPressed + ); + configureButton.configure( + centreX + 2 * radius / 3, + centreY, + iconDpadRightPressed, + iconDpadRightNotPressed + ); + Log.i("INPUT", "Dpad reconfigure: " + rect); + } + + public DPadInput(Resources resources, @DrawableRes int backgroundId, @DrawableRes int pressedButtonId, @DrawableRes int notPressedButtonId, ButtonStateChangeListener buttonStateChangeListener, InputOverlaySettingsProvider.InputOverlaySettings settings) { + super(settings); + this.buttonStateChangeListener = buttonStateChangeListener; + + background = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, backgroundId, null)); + + Supplier getPressedButtonIcon = () -> Objects.requireNonNull(ResourcesCompat.getDrawable(resources, pressedButtonId, null)); + Supplier getNotPressedButtonIcon = () -> Objects.requireNonNull(ResourcesCompat.getDrawable(resources, notPressedButtonId, null)); + + iconDpadUpPressed = getPressedButtonIcon.get(); + iconDpadUpNotPressed = getNotPressedButtonIcon.get(); + iconDpadUp = iconDpadUpNotPressed; + + iconDpadDownPressed = getPressedButtonIcon.get(); + iconDpadDownNotPressed = getNotPressedButtonIcon.get(); + iconDpadDown = iconDpadDownNotPressed; + + iconDpadLeftPressed = getPressedButtonIcon.get(); + iconDpadLeftNotPressed = getNotPressedButtonIcon.get(); + iconDpadLeft = iconDpadLeftNotPressed; + + iconDpadRightPressed = getPressedButtonIcon.get(); + iconDpadRightNotPressed = getNotPressedButtonIcon.get(); + iconDpadRight = iconDpadRightNotPressed; + + configure(); + } + + private void updateState(DpadState nextDpadState) { + switch (dpadState) { + case UP -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, false); + iconDpadUp = iconDpadUpNotPressed; + } + case DOWN -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, false); + iconDpadDown = iconDpadDownNotPressed; + } + case LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, false); + iconDpadLeft = iconDpadLeftNotPressed; + } + case RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, false); + iconDpadRight = iconDpadRightNotPressed; + } + case UP_LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, false); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, false); + iconDpadUp = iconDpadUpNotPressed; + iconDpadLeft = iconDpadLeftNotPressed; + } + case UP_RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, false); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, false); + iconDpadUp = iconDpadUpNotPressed; + iconDpadRight = iconDpadRightNotPressed; + } + case DOWN_LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, false); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, false); + iconDpadDown = iconDpadDownNotPressed; + iconDpadLeft = iconDpadLeftNotPressed; + } + case DOWN_RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, false); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, false); + iconDpadDown = iconDpadDownNotPressed; + iconDpadRight = iconDpadRightNotPressed; + } + } + dpadState = nextDpadState; + switch (nextDpadState) { + case UP -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, true); + iconDpadUp = iconDpadUpPressed; + } + case DOWN -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, true); + iconDpadDown = iconDpadDownPressed; + } + case LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, true); + iconDpadLeft = iconDpadLeftPressed; + } + case RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, true); + iconDpadRight = iconDpadRightPressed; + } + case UP_LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, true); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, true); + iconDpadUp = iconDpadUpPressed; + iconDpadLeft = iconDpadLeftPressed; + } + case UP_RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_UP, true); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, true); + iconDpadUp = iconDpadUpPressed; + iconDpadRight = iconDpadRightPressed; + } + case DOWN_LEFT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, true); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_LEFT, true); + iconDpadDown = iconDpadDownPressed; + iconDpadLeft = iconDpadLeftPressed; + } + case DOWN_RIGHT -> { + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_DOWN, true); + buttonStateChangeListener.onButtonStateChange(InputOverlaySurfaceView.OverlayButton.DPAD_RIGHT, true); + iconDpadDown = iconDpadDownPressed; + iconDpadRight = iconDpadRightPressed; + } + } + } + + @Override + public boolean onTouch(MotionEvent event) { + DpadState nextState = dpadState; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + int pointerIndex = event.getActionIndex(); + int x = (int) event.getX(pointerIndex); + int y = (int) event.getY(pointerIndex); + int pointerId = event.getPointerId(pointerIndex); + if (isInside(x, y)) { + currentPointerId = pointerId; + nextState = DpadState.NONE; + } + } + case MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + int pointerId = event.getPointerId(event.getActionIndex()); + if (pointerId == currentPointerId) { + currentPointerId = -1; + nextState = DpadState.NONE; + } + } + case MotionEvent.ACTION_MOVE -> { + if (currentPointerId == -1) { + break; + } + for (int i = 0; i < event.getPointerCount(); i++) { + if (currentPointerId != event.getPointerId(i)) { + continue; + } + int x = (int) event.getX(i); + int y = (int) event.getY(i); + int norm2 = ((x - centreX) * (x - centreX) + (y - centreY) * (y - centreY)); + if (norm2 <= radius * radius * 0.1f) { + nextState = DpadState.NONE; + break; + } + double angle = Math.atan2(y - centreY, x - centreX); + nextState = getStateByAngle(angle); + break; + } + } + } + if (nextState != dpadState) { + updateState(nextState); + return true; + } + return false; + } + + @Override + protected void resetInput() { + updateState(DpadState.NONE); + } + + private DpadState getStateByAngle(double angle) { + if (-0.125 * Math.PI <= angle && angle < 0.125 * Math.PI) return DpadState.RIGHT; + if (0.125 * Math.PI <= angle && angle < 0.375 * Math.PI) return DpadState.DOWN_RIGHT; + if (0.375 * Math.PI <= angle && angle < 0.625 * Math.PI) return DpadState.DOWN; + if (0.625 * Math.PI <= angle && angle < 0.875 * Math.PI) return DpadState.DOWN_LEFT; + if (-0.875 * Math.PI <= angle && angle < -0.625 * Math.PI) return DpadState.UP_LEFT; + if (-0.625 * Math.PI <= angle && angle < -0.375 * Math.PI) return DpadState.UP; + if (-0.375 * Math.PI <= angle && angle < -0.125 * Math.PI) return DpadState.UP_RIGHT; + return DpadState.LEFT; + } + + @Override + public boolean isInside(int x, int y) { + return ((x - centreX) * (x - centreX) + (y - centreY) * (y - centreY)) <= radius * radius; + } + + @Override + protected void drawInput(Canvas canvas) { + background.draw(canvas); + iconDpadUp.draw(canvas); + iconDpadDown.draw(canvas); + iconDpadLeft.draw(canvas); + iconDpadRight.draw(canvas); + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java index 3ede0c71..e86a4a25 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulatedControllerTypeAdapter.java @@ -14,7 +14,7 @@ public class EmulatedControllerTypeAdapter extends SelectionAdapter { NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO, NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC, NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) - .map(type -> new ChoiceItem<>(NativeLibrary.controllerTypeToResourceNameId(type), type)) + .map(type -> new ChoiceItem<>(t->t.setText(NativeLibrary.controllerTypeToResourceNameId(type)), type)) .collect(Collectors.toList()); setSelectedValue(NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java index ed81ef3e..6906a12c 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/EmulationFragment.java @@ -1,12 +1,13 @@ package info.cemu.Cemu; +import android.content.Context; import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -17,6 +18,7 @@ public class EmulationFragment extends Fragment implements SurfaceHolder.Callbac private final long gameTitleId; private boolean isGameRunning; private SurfaceHolder mainCanvasSurfaceHolder; + private Toast toast; public EmulationFragment(long gameTitleId) { this.gameTitleId = gameTitleId; @@ -25,6 +27,40 @@ public class EmulationFragment extends Fragment implements SurfaceHolder.Callbac @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { FragmentEmulationBinding binding = FragmentEmulationBinding.inflate(inflater, container, false); + var context = requireContext(); + var sharedPref = context.getSharedPreferences( + context.getString(R.string.inputoverlaysettings_shared_preferences_name), Context.MODE_PRIVATE); + var inputOverlaySettingsProvider = new InputOverlaySettingsProvider(sharedPref); + var overlaySettings = inputOverlaySettingsProvider.getOverlaySettings(); + InputOverlaySurfaceView surfaceView = binding.inputOverlay; + binding.inputModeButton.setOnClickListener(v -> { + InputOverlaySurfaceView.InputMode nextMode = InputOverlaySurfaceView.InputMode.DEFAULT; + int toastTextResId = R.string.input_mode_default; + switch (surfaceView.getInputMode()) { + case EDIT_POSITION -> { + nextMode = InputOverlaySurfaceView.InputMode.EDIT_SIZE; + toastTextResId = R.string.input_mode_edit_size; + } + case EDIT_SIZE -> { + nextMode = InputOverlaySurfaceView.InputMode.DEFAULT; + toastTextResId = R.string.input_mode_default; + } + case DEFAULT -> { + nextMode = InputOverlaySurfaceView.InputMode.EDIT_POSITION; + toastTextResId = R.string.input_mode_edit_position; + } + } + if (toast != null) { + toast.cancel(); + } + toast = Toast.makeText(requireContext(), toastTextResId, Toast.LENGTH_SHORT); + toast.show(); + surfaceView.setInputMode(nextMode); + }); + if (!overlaySettings.isOverlayEnabled()) { + binding.inputModeButton.setVisibility(View.GONE); + surfaceView.setVisibility(View.GONE); + } SurfaceView mainCanvas = binding.mainCanvas; mainCanvas.getHolder().addCallback(this); return binding.getRoot(); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/GraphicsSettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/GraphicsSettingsFragment.java index 5ce7b96e..8582997d 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/GraphicsSettingsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/GraphicsSettingsFragment.java @@ -26,7 +26,7 @@ public class GraphicsSettingsFragment extends Fragment { int vsyncMode = NativeLibrary.getVSyncMode(); var vsyncChoices = Stream.of(NativeLibrary.VSYNC_MODE_OFF, NativeLibrary.VSYNC_MODE_DOUBLE_BUFFERING, NativeLibrary.VSYNC_MODE_TRIPLE_BUFFERING) - .map(vsync -> new SelectionAdapter.ChoiceItem<>(NativeLibrary.vsyncModeToResourceNameId(vsync), vsync)) + .map(vsync -> new SelectionAdapter.ChoiceItem<>(t -> t.setText(NativeLibrary.vsyncModeToResourceNameId(vsync)), vsync)) .collect(Collectors.toList()); SelectionAdapter vsyncSelectionAdapter = new SelectionAdapter<>(vsyncChoices, vsyncMode); SingleSelectionRecyclerViewItem vsyncModeSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.vsync), diff --git a/src/android/app/src/main/java/info/cemu/Cemu/Input.java b/src/android/app/src/main/java/info/cemu/Cemu/Input.java new file mode 100644 index 00000000..cfbe9b4e --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/Input.java @@ -0,0 +1,90 @@ +package info.cemu.Cemu; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.view.MotionEvent; + +import androidx.annotation.ColorInt; + +public abstract class Input { + protected final InputOverlaySettingsProvider.InputOverlaySettings settings; + private boolean drawBoundingRectangle; + private Rect boundingRectangle; + private final Paint boundingRectanglePaint = new Paint(); + + protected Input(InputOverlaySettingsProvider.InputOverlaySettings settings) { + this.settings = settings; + boundingRectangle = settings.getRect(); + } + + public abstract boolean onTouch(MotionEvent event); + + public void moveInput(int x, int y, int maxWidth, int maxHeight) { + var rect = settings.getRect(); + int width = rect.width(); + int height = rect.height(); + int left = Math.min(Math.max(x - width / 2, 0), maxWidth - width); + int top = Math.min(Math.max(y - height / 2, 0), maxHeight - height); + boundingRectangle = new Rect( + left, + top, + left + width, + top + height + ); + settings.setRect(boundingRectangle); + configure(); + } + + private static final float MIN_WIDTH_HEIGHT = 100; + + public void resize(int diffX, int diffY, int maxWidth, int maxHeight) { + var rect = settings.getRect(); + int newRight = rect.right + diffX; + int newBottom = rect.bottom + diffY; + if (newRight - rect.left < MIN_WIDTH_HEIGHT || newBottom - rect.top < MIN_WIDTH_HEIGHT + || newRight > maxWidth || newBottom > maxHeight) { + return; + } + boundingRectangle = new Rect( + rect.left, + rect.top, + newRight, + newBottom + ); + settings.setRect(boundingRectangle); + configure(); + } + + protected abstract void resetInput(); + + public void reset() { + drawBoundingRectangle = false; + } + + public void saveConfiguration() { + settings.saveSettings(); + } + + protected abstract void configure(); + + protected abstract void drawInput(Canvas canvas); + + public void draw(Canvas canvas) { + if (drawBoundingRectangle) { + canvas.drawRect(boundingRectangle, boundingRectanglePaint); + } + drawInput(canvas); + } + + public void enableDrawingBoundingRect(@ColorInt int color) { + drawBoundingRectangle = true; + boundingRectanglePaint.setColor(color); + } + + public void disableDrawingBoundingRect() { + drawBoundingRectangle = false; + } + + public abstract boolean isInside(int x, int y); +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputOverlayConfigureActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlayConfigureActivity.java new file mode 100644 index 00000000..b72aa380 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlayConfigureActivity.java @@ -0,0 +1,53 @@ +package info.cemu.Cemu; + +import static info.cemu.Cemu.InputOverlaySurfaceView.InputMode.EDIT_POSITION; +import static info.cemu.Cemu.InputOverlaySurfaceView.InputMode.EDIT_SIZE; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; + +import info.cemu.Cemu.databinding.ActivityOverlayConfigureBinding; + +public class InputOverlayConfigureActivity extends AppCompatActivity { + private Toast toast; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setFullscreen(); + ActivityOverlayConfigureBinding binding = ActivityOverlayConfigureBinding.inflate(getLayoutInflater()); + InputOverlaySurfaceView surfaceView = binding.inputConfigureOverlay; + surfaceView.setInputMode(EDIT_POSITION); + binding.configureModeButton.setOnClickListener(v -> { + InputOverlaySurfaceView.InputMode nextMode; + int toastTextResId; + if (surfaceView.getInputMode() == EDIT_POSITION) { + toastTextResId = R.string.input_mode_edit_size; + nextMode = EDIT_SIZE; + } else { + toastTextResId = R.string.input_mode_edit_position; + nextMode = EDIT_POSITION; + } + if (toast != null) { + toast.cancel(); + } + toast = Toast.makeText(InputOverlayConfigureActivity.this, toastTextResId, Toast.LENGTH_SHORT); + toast.show(); + surfaceView.setInputMode(nextMode); + }); + binding.resetOverlayButton.setOnClickListener(v -> surfaceView.resetInputs()); + setContentView(binding.getRoot()); + } + + private void setFullscreen() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowInsetsControllerCompat controller = new WindowInsetsControllerCompat(getWindow(), getWindow().getDecorView()); + controller.hide(WindowInsetsCompat.Type.systemBars()); + controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsFragment.java new file mode 100644 index 00000000..33cbe25f --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsFragment.java @@ -0,0 +1,101 @@ +package info.cemu.Cemu; + + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import info.cemu.Cemu.databinding.FragmentInputOverlaySettingsBinding; + + +public class InputOverlaySettingsFragment extends Fragment { + private InputOverlaySettingsProvider.OverlaySettings overlaySettings; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (overlaySettings == null) { + var context = requireContext(); + var sharedPref = context.getSharedPreferences( + context.getString(R.string.inputoverlaysettings_shared_preferences_name), Context.MODE_PRIVATE); + InputOverlaySettingsProvider inputOverlaySettingsProvider = new InputOverlaySettingsProvider(sharedPref); + overlaySettings = inputOverlaySettingsProvider.getOverlaySettings(); + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + var binding = FragmentInputOverlaySettingsBinding.inflate(inflater, container, false); + GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter(); + + CheckboxRecyclerViewItem inputOverlayCheckbox = new CheckboxRecyclerViewItem( + getString(R.string.input_overlay), + getString(R.string.enable_input_overlay), + overlaySettings.isOverlayEnabled(), + checked -> { + overlaySettings.setOverlayEnabled(checked); + overlaySettings.saveSettings(); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(inputOverlayCheckbox); + + CheckboxRecyclerViewItem vibrateOnTouchCheckbox = new CheckboxRecyclerViewItem( + getString(R.string.vibrate), + getString(R.string.enable_vibrate_on_touch), + overlaySettings.isVibrateOnTouchEnabled(), + checked -> { + overlaySettings.setVibrateOnTouchEnabled(checked); + overlaySettings.saveSettings(); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(vibrateOnTouchCheckbox); + + SliderRecyclerViewItem alphaSlider = new SliderRecyclerViewItem( + getString(R.string.alpha_slider), + 0, + 255, + overlaySettings.getAlpha(), + value -> { + overlaySettings.setAlpha((int) value); + overlaySettings.saveSettings(); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(alphaSlider); + + SelectionAdapter controllerAdapter = new SelectionAdapter<>( + IntStream.range(0, NativeLibrary.MAX_CONTROLLERS) + .mapToObj(i -> new SelectionAdapter.ChoiceItem<>(t -> t.setText(getString(R.string.controller_numbered, i + 1)), i)) + .collect(Collectors.toList()), + overlaySettings.getControllerIndex() + ); + SingleSelectionRecyclerViewItem controllerSelection = new SingleSelectionRecyclerViewItem<>( + getString(R.string.overlay_controller), + getString(R.string.controller_numbered, overlaySettings.getControllerIndex() + 1), + controllerAdapter, + (controllerIndex, selectionRecyclerViewItem) -> { + overlaySettings.setControllerIndex(controllerIndex); + selectionRecyclerViewItem.setDescription(getString(R.string.controller_numbered, controllerIndex + 1)); + overlaySettings.saveSettings(); + }); + genericRecyclerViewAdapter.addRecyclerViewItem(controllerSelection); + + genericRecyclerViewAdapter.addRecyclerViewItem( + new ButtonRecyclerViewItem( + getString(R.string.configure_inputs_label), + getString(R.string.configure_inputs_description), + () -> startActivity(new Intent(requireActivity(), InputOverlayConfigureActivity.class)) + ) + ); + + binding.inputOverlaySettingsRecyclerView.setAdapter(genericRecyclerViewAdapter); + + return binding.getRoot(); + } +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsProvider.java b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsProvider.java new file mode 100644 index 00000000..9c717388 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySettingsProvider.java @@ -0,0 +1,277 @@ +package info.cemu.Cemu; + + +import android.content.SharedPreferences; +import android.graphics.Rect; + +import java.util.Optional; +import java.util.function.Consumer; + +public class InputOverlaySettingsProvider { + public enum Input { + A, + B, + ONE, + TWO, + C, + Z, + HOME, + DPAD, + L, + L_STICK, + MINUS, + PLUS, + R, + R_STICK, + X, + Y, + ZL, + ZR, + NUN_CHUCK_AXIS, + LEFT_AXIS, + RIGHT_AXIS, + } + + private final SharedPreferences sharedPreferences; + + private static final int defaultAlpha = 64; + + public InputOverlaySettingsProvider(SharedPreferences sharedPreferences) { + this.sharedPreferences = sharedPreferences; + } + + public OverlaySettings getOverlaySettings() { + return new OverlaySettings( + isVibrateOnTouchEnabled(), + isOverlayEnabled(), + getControllerIndex(), + getAlpha(), + settings -> { + var editor = sharedPreferences.edit(); + editor.putInt("controllerIndex", settings.getControllerIndex()); + editor.putInt("alpha", settings.getAlpha()); + editor.putBoolean("overlayEnabled", settings.isOverlayEnabled()); + editor.putBoolean("vibrateOnTouchEnabled", settings.isVibrateOnTouchEnabled()); + editor.apply(); + } + ); + } + + public InputOverlaySettings getOverlaySettingsForInput(Input input, int width, int height) { + return new InputOverlaySettings( + getRectangle(input).orElseGet(() -> getDefaultRectangle(input, width, height)), + getAlpha(), + settings -> { + var editor = sharedPreferences.edit(); + var rect = settings.getRect(); + editor.putInt(input + "left", rect.left); + editor.putInt(input + "top", rect.top); + editor.putInt(input + "right", rect.right); + editor.putInt(input + "bottom", rect.bottom); + editor.putInt(input + "alpha", settings.getAlpha()); + editor.apply(); + } + ); + } + + private boolean isOverlayEnabled() { + return sharedPreferences.getBoolean("overlayEnabled", true); + } + + private boolean isVibrateOnTouchEnabled() { + return sharedPreferences.getBoolean("vibrateOnTouchEnabled", true); + } + + private int getControllerIndex() { + return sharedPreferences.getInt("controllerIndex", 0); + } + + private int getAlpha() { + return sharedPreferences.getInt("alpha", defaultAlpha); + } + + private Optional getRectangle(Input input) { + int left = sharedPreferences.getInt(input + "left", -1); + int top = sharedPreferences.getInt(input + "top", -1); + int right = sharedPreferences.getInt(input + "right", -1); + int bottom = sharedPreferences.getInt(input + "bottom", -1); + if (left == -1 || top == -1 || right == -1 || bottom == -1) { + return Optional.empty(); + } + return Optional.of(new Rect(left, top, right, bottom)); + } + + public Rect getDefaultRectangle(Input input, int width, int height) { + int pmButtonRadius = (int) (height * 0.065f); + int pmButtonsCentreX = (int) (width * 0.5f); + int pmButtonsCentreY = (int) (height * 0.875f); + + int roundButtonRadius = (int) (height * 0.065f); + int abxyButtonsCentreX = width - roundButtonRadius * 4; + int abxyButtonsCentreY = height - roundButtonRadius * 4; + + int rectangleButtonsHeight = (int) (height * 0.10f); + int rectangleButtonsWidth = rectangleButtonsHeight * 2; + + int triggersLLeft = (int) (width * 0.05f); + int triggersRLeft = (int) (width * 0.95f - rectangleButtonsWidth); + int triggersTop = (int) (height * 0.05f); + + int dpadRadius = (int) (height * 0.20f); + int dpadCentreX = dpadRadius; + int dpadCentreY = (int) (height * 0.75f); + + int joystickRadius = (int) (height * 0.10f); + int rightJoystickCentreX = (int) (width * 0.75f); + int leftJoystickCentreX = (int) (width * 0.25f); + int joystickCentreY = (int) (height * 0.55f); + int joystickClickRadius = (int) (joystickRadius * 0.7f); + + return switch (input) { + case A -> new Rect(abxyButtonsCentreX + roundButtonRadius, + abxyButtonsCentreY - roundButtonRadius, + abxyButtonsCentreX + roundButtonRadius * 3, + abxyButtonsCentreY + roundButtonRadius); + case B -> new Rect(abxyButtonsCentreX - roundButtonRadius, + abxyButtonsCentreY + roundButtonRadius, + abxyButtonsCentreX + roundButtonRadius, + abxyButtonsCentreY + roundButtonRadius * 3); + case X, ONE -> new Rect(abxyButtonsCentreX - roundButtonRadius, + abxyButtonsCentreY - roundButtonRadius * 3, + abxyButtonsCentreX + roundButtonRadius, + abxyButtonsCentreY - roundButtonRadius); + case Y, TWO -> new Rect(abxyButtonsCentreX - roundButtonRadius * 3, + abxyButtonsCentreY - roundButtonRadius, + abxyButtonsCentreX - roundButtonRadius, + abxyButtonsCentreY + roundButtonRadius); + case MINUS -> new Rect(pmButtonsCentreX - pmButtonRadius * 3, + pmButtonsCentreY - pmButtonRadius, + pmButtonsCentreX - pmButtonRadius, + pmButtonsCentreY + pmButtonRadius); + case PLUS -> new Rect(pmButtonsCentreX + pmButtonRadius, + pmButtonsCentreY - pmButtonRadius, + pmButtonsCentreX + pmButtonRadius * 3, + pmButtonsCentreY + pmButtonRadius); + case L -> new Rect(triggersLLeft, + triggersTop, + triggersLLeft + rectangleButtonsWidth, + triggersTop + rectangleButtonsHeight); + case ZL -> new Rect(triggersLLeft, + (int) (triggersTop + rectangleButtonsHeight * 1.5f), + triggersLLeft + rectangleButtonsWidth, + (int) (triggersTop + rectangleButtonsHeight * 2.5f)); + case R -> new Rect(triggersRLeft, + triggersTop, + triggersRLeft + rectangleButtonsWidth, + triggersTop + rectangleButtonsHeight); + case ZR -> new Rect(triggersRLeft, + (int) (triggersTop + rectangleButtonsHeight * 1.5f), + triggersRLeft + rectangleButtonsWidth, + (int) (triggersTop + rectangleButtonsHeight * 2.5f)); + // TODO: Wiimote settings + case C, HOME, Z, NUN_CHUCK_AXIS -> new Rect(); + case DPAD -> new Rect(dpadCentreX - dpadRadius, + dpadCentreY - dpadRadius, + dpadCentreX + dpadRadius, + dpadCentreY + dpadRadius); + case LEFT_AXIS -> new Rect(leftJoystickCentreX - joystickRadius, + joystickCentreY - joystickRadius, + leftJoystickCentreX + joystickRadius, + joystickCentreY + joystickRadius); + case RIGHT_AXIS -> new Rect(rightJoystickCentreX - joystickRadius, + joystickCentreY - joystickRadius, + rightJoystickCentreX + joystickRadius, + joystickCentreY + joystickRadius); + case L_STICK -> + new Rect((int) (leftJoystickCentreX + joystickRadius * 1.5f - joystickClickRadius), + (int) (joystickCentreY + joystickRadius * 1.5f - joystickClickRadius), + (int) (leftJoystickCentreX + joystickRadius * 1.5f + joystickClickRadius), + (int) (joystickCentreY + joystickRadius * 1.5f + joystickClickRadius)); + case R_STICK -> + new Rect((int) (rightJoystickCentreX - joystickRadius * 1.5f - joystickClickRadius), + (int) (joystickCentreY + joystickRadius * 1.5f - joystickClickRadius), + (int) (rightJoystickCentreX - joystickRadius * 1.5f + joystickClickRadius), + (int) (joystickCentreY + joystickRadius * 1.5f + joystickClickRadius)); + }; + } + + public static class OverlaySettings { + private int controllerIndex; + private boolean overlayEnabled; + private boolean vibrateOnTouchEnabled; + private int alpha; + private final Consumer overlaySettingsConsumer; + + private OverlaySettings(boolean vibrateOnTouchEnabled, boolean overlayEnabled, int controllerIndex, int alpha, Consumer overlaySettingsConsumer) { + setControllerIndex(controllerIndex); + setAlpha(alpha); + setVibrateOnTouchEnabled(vibrateOnTouchEnabled); + setOverlayEnabled(overlayEnabled); + this.overlaySettingsConsumer = overlaySettingsConsumer; + } + + public void setAlpha(int alpha) { + this.alpha = Math.max(0, Math.min(255, alpha)); + } + + public int getAlpha() { + return this.alpha; + } + + public void setControllerIndex(int controllerIndex) { + this.controllerIndex = Math.min(Math.max(controllerIndex, 0), NativeLibrary.MAX_CONTROLLERS - 1); + } + + public int getControllerIndex() { + return this.controllerIndex; + } + + public void saveSettings() { + overlaySettingsConsumer.accept(this); + } + + public boolean isOverlayEnabled() { + return overlayEnabled; + } + + public void setOverlayEnabled(boolean overlayEnabled) { + this.overlayEnabled = overlayEnabled; + } + + public boolean isVibrateOnTouchEnabled() { + return vibrateOnTouchEnabled; + } + + public void setVibrateOnTouchEnabled(boolean vibrateOnTouchEnabled) { + this.vibrateOnTouchEnabled = vibrateOnTouchEnabled; + } + } + + public static class InputOverlaySettings { + private Rect rectangle; + private final Consumer inputOverlaySettingsConsumer; + private final int alpha; + + private InputOverlaySettings(Rect rectangle, int alpha, Consumer inputOverlaySettingsConsumer) { + setRect(rectangle); + this.alpha = alpha; + this.inputOverlaySettingsConsumer = inputOverlaySettingsConsumer; + } + + public int getAlpha() { + return alpha; + } + + public void setRect(Rect rectangle) { + this.rectangle = rectangle; + } + + public Rect getRect() { + return this.rectangle; + } + + public void saveSettings() { + inputOverlaySettingsConsumer.accept(this); + } + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySurfaceView.java b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySurfaceView.java new file mode 100644 index 00000000..51c2f988 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputOverlaySurfaceView.java @@ -0,0 +1,569 @@ +package info.cemu.Cemu; + +import static info.cemu.Cemu.InputOverlaySurfaceView.InputMode.DEFAULT; + +import android.content.Context; +import android.graphics.Canvas; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +public class InputOverlaySurfaceView extends SurfaceView implements View.OnTouchListener { + public enum InputMode { + DEFAULT, + EDIT_POSITION, + EDIT_SIZE, + } + + private InputMode inputMode = DEFAULT; + + public void resetInputs() { + int width = getWidth(); + int height = getHeight(); + for (var input : InputOverlaySettingsProvider.Input.values()) { + var inputSettings = settingsProvider.getOverlaySettingsForInput(input, width, height); + inputSettings.setRect(settingsProvider.getDefaultRectangle(input, width, height)); + inputSettings.saveSettings(); + } + inputs.clear(); + inputs = null; + setInputs(); + invalidate(); + } + + public void setInputMode(InputMode inputMode) { + if (inputs != null) { + if (this.inputMode != DEFAULT) { + for (var input : inputs) { + input.saveConfiguration(); + } + } else { + for (var input : inputs) { + input.reset(); + } + } + } + this.inputMode = inputMode; + } + + public InputMode getInputMode() { + return inputMode; + } + + public enum OverlayButton { + A, + B, + ONE, + TWO, + C, + Z, + HOME, + DPAD_DOWN, + DPAD_LEFT, + DPAD_RIGHT, + DPAD_UP, + L, + L_STICK, + MINUS, + PLUS, + R, + R_STICK, + X, + Y, + ZL, + ZR, + } + + + public enum OverlayJoystick { + LEFT, + RIGHT, + } + + private int nativeControllerType = -1; + int controllerIndex; + + private List inputs; + private final InputOverlaySettingsProvider settingsProvider; + private final Vibrator vibrator; + private final VibrationEffect buttonTouchVibrationEffect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK); + private final boolean vibrateOnTouch; + + public InputOverlaySurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + context.getSystemService(Context.VIBRATOR_SERVICE); + vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + setOnTouchListener(this); + var sharedPref = context.getSharedPreferences( + getContext().getString(R.string.inputoverlaysettings_shared_preferences_name), Context.MODE_PRIVATE); + settingsProvider = new InputOverlaySettingsProvider(sharedPref); + var overlaySettings = settingsProvider.getOverlaySettings(); + controllerIndex = overlaySettings.getControllerIndex(); + vibrateOnTouch = vibrator != null && vibrator.hasVibrator() && overlaySettings.isVibrateOnTouchEnabled(); + } + + int overlayButtonToVPADButton(OverlayButton button) { + return switch (button) { + case A -> NativeLibrary.VPAD_BUTTON_A; + case B -> NativeLibrary.VPAD_BUTTON_B; + case X -> NativeLibrary.VPAD_BUTTON_X; + case Y -> NativeLibrary.VPAD_BUTTON_Y; + case PLUS -> NativeLibrary.VPAD_BUTTON_PLUS; + case MINUS -> NativeLibrary.VPAD_BUTTON_MINUS; + case DPAD_UP -> NativeLibrary.VPAD_BUTTON_UP; + case DPAD_DOWN -> NativeLibrary.VPAD_BUTTON_DOWN; + case DPAD_LEFT -> NativeLibrary.VPAD_BUTTON_LEFT; + case DPAD_RIGHT -> NativeLibrary.VPAD_BUTTON_RIGHT; + case L_STICK -> NativeLibrary.VPAD_BUTTON_STICKL; + case R_STICK -> NativeLibrary.VPAD_BUTTON_STICKR; + case L -> NativeLibrary.VPAD_BUTTON_L; + case R -> NativeLibrary.VPAD_BUTTON_R; + case ZR -> NativeLibrary.VPAD_BUTTON_ZR; + case ZL -> NativeLibrary.VPAD_BUTTON_ZL; + default -> -1; + }; + } + + int overlayButtonToClassicButton(OverlayButton button) { + return switch (button) { + case A -> NativeLibrary.CLASSIC_BUTTON_A; + case B -> NativeLibrary.CLASSIC_BUTTON_B; + case X -> NativeLibrary.CLASSIC_BUTTON_X; + case Y -> NativeLibrary.CLASSIC_BUTTON_Y; + case PLUS -> NativeLibrary.CLASSIC_BUTTON_PLUS; + case MINUS -> NativeLibrary.CLASSIC_BUTTON_MINUS; + case DPAD_UP -> NativeLibrary.CLASSIC_BUTTON_UP; + case DPAD_DOWN -> NativeLibrary.CLASSIC_BUTTON_DOWN; + case DPAD_LEFT -> NativeLibrary.CLASSIC_BUTTON_LEFT; + case DPAD_RIGHT -> NativeLibrary.CLASSIC_BUTTON_RIGHT; + case L -> NativeLibrary.CLASSIC_BUTTON_L; + case R -> NativeLibrary.CLASSIC_BUTTON_R; + case ZR -> NativeLibrary.CLASSIC_BUTTON_ZR; + case ZL -> NativeLibrary.CLASSIC_BUTTON_ZL; + default -> -1; + }; + } + + int overlayButtonToProButton(OverlayButton button) { + return switch (button) { + case A -> NativeLibrary.PRO_BUTTON_A; + case B -> NativeLibrary.PRO_BUTTON_B; + case X -> NativeLibrary.PRO_BUTTON_X; + case Y -> NativeLibrary.PRO_BUTTON_Y; + case PLUS -> NativeLibrary.PRO_BUTTON_PLUS; + case MINUS -> NativeLibrary.PRO_BUTTON_MINUS; + case DPAD_UP -> NativeLibrary.PRO_BUTTON_UP; + case DPAD_DOWN -> NativeLibrary.PRO_BUTTON_DOWN; + case DPAD_LEFT -> NativeLibrary.PRO_BUTTON_LEFT; + case DPAD_RIGHT -> NativeLibrary.PRO_BUTTON_RIGHT; + case L_STICK -> NativeLibrary.PRO_BUTTON_STICKL; + case R_STICK -> NativeLibrary.PRO_BUTTON_STICKR; + case L -> NativeLibrary.PRO_BUTTON_L; + case R -> NativeLibrary.PRO_BUTTON_R; + case ZR -> NativeLibrary.PRO_BUTTON_ZR; + case ZL -> NativeLibrary.PRO_BUTTON_ZL; + default -> -1; + }; + } + + int overlayButtonToWiimoteButton(OverlayButton button) { + return switch (button) { + case A -> NativeLibrary.WIIMOTE_BUTTON_A; + case B -> NativeLibrary.WIIMOTE_BUTTON_B; + case ONE -> NativeLibrary.WIIMOTE_BUTTON_1; + case TWO -> NativeLibrary.WIIMOTE_BUTTON_2; + case PLUS -> NativeLibrary.WIIMOTE_BUTTON_PLUS; + case MINUS -> NativeLibrary.WIIMOTE_BUTTON_MINUS; + case HOME -> NativeLibrary.WIIMOTE_BUTTON_HOME; + case DPAD_UP -> NativeLibrary.WIIMOTE_BUTTON_UP; + case DPAD_DOWN -> NativeLibrary.WIIMOTE_BUTTON_DOWN; + case DPAD_LEFT -> NativeLibrary.WIIMOTE_BUTTON_LEFT; + case DPAD_RIGHT -> NativeLibrary.WIIMOTE_BUTTON_RIGHT; + case C -> NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_C; + case Z -> NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_Z; + default -> -1; + }; + } + + void onButtonStateChange(OverlayButton button, boolean state) { + int nativeButtonId = switch (nativeControllerType) { + case NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD -> overlayButtonToVPADButton(button); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC -> + overlayButtonToClassicButton(button); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO -> overlayButtonToProButton(button); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE -> + overlayButtonToWiimoteButton(button); + default -> -1; + }; + if (nativeButtonId == -1) { + return; + } + if (vibrateOnTouch) { + vibrator.vibrate(buttonTouchVibrationEffect); + } + NativeLibrary.onOverlayButton(controllerIndex, nativeButtonId, state); + } + + void onOverlayAxis(int axis, float value) { + NativeLibrary.onOverlayAxis(controllerIndex, axis, value); + } + + void onVPADJoystickStateChange(OverlayJoystick joystick, float up, float down, float left, float right) { + if (joystick == OverlayJoystick.LEFT) { + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKL_UP, up); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKL_DOWN, down); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKL_LEFT, left); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKL_RIGHT, right); + } else { + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKR_UP, up); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKR_DOWN, down); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKR_LEFT, left); + onOverlayAxis(NativeLibrary.VPAD_BUTTON_STICKR_RIGHT, right); + } + } + + void onProJoystickStateChange(OverlayJoystick joystick, float up, float down, float left, float right) { + if (joystick == OverlayJoystick.LEFT) { + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKL_UP, up); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKL_DOWN, down); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKL_LEFT, left); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKL_RIGHT, right); + } else { + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKR_UP, up); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKR_DOWN, down); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKR_LEFT, left); + onOverlayAxis(NativeLibrary.PRO_BUTTON_STICKR_RIGHT, right); + } + } + + void onClassicJoystickStateChange(OverlayJoystick joystick, float up, float down, float left, float right) { + if (joystick == OverlayJoystick.LEFT) { + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKL_UP, up); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKL_DOWN, down); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKL_LEFT, left); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKL_RIGHT, right); + } else { + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKR_UP, up); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKR_DOWN, down); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKR_LEFT, left); + onOverlayAxis(NativeLibrary.CLASSIC_BUTTON_STICKR_RIGHT, right); + } + } + + void onWiimoteJoystickStateChange(OverlayJoystick joystick, float up, float down, float left, float right) { + if (joystick == OverlayJoystick.LEFT) { + onOverlayAxis(NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_UP, up); + onOverlayAxis(NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_DOWN, down); + onOverlayAxis(NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_LEFT, left); + onOverlayAxis(NativeLibrary.WIIMOTE_BUTTON_NUNCHUCK_RIGHT, right); + } + } + + void onJoystickStateChange(OverlayJoystick joystick, float x, float y) { + float up = y < 0 ? -y : 0; + float down = y > 0 ? y : 0; + float left = x < 0 ? -x : 0; + float right = x > 0 ? x : 0; + switch (nativeControllerType) { + case NativeLibrary.EMULATED_CONTROLLER_TYPE_VPAD -> + onVPADJoystickStateChange(joystick, up, down, left, right); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_PRO -> + onProJoystickStateChange(joystick, up, down, left, right); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC -> + onClassicJoystickStateChange(joystick, up, down, left, right); + case NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE -> + onWiimoteJoystickStateChange(joystick, up, down, left, right); + } + } + + private InputOverlaySettingsProvider.InputOverlaySettings getOverlaySettingsForInput(InputOverlaySettingsProvider.Input input) { + return settingsProvider.getOverlaySettingsForInput(input, getWidth(), getHeight()); + } + + void setInputs() { + if (inputs != null) { + return; + } + inputs = new ArrayList<>(); + if (NativeLibrary.isControllerDisabled(controllerIndex)) { + return; + } + var resources = getResources(); + nativeControllerType = NativeLibrary.getControllerType(controllerIndex); + + inputs.add(new RoundButton( + resources, + R.drawable.button_a_pressed, + R.drawable.button_a, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.A, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.A) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_b_pressed, + R.drawable.button_b, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.B, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.B) + )); + + if (nativeControllerType != NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) { + inputs.add(new RoundButton( + resources, + R.drawable.button_x_pressed, + R.drawable.button_x, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.X, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.X) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_y_pressed, + R.drawable.button_y, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.Y, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.Y) + )); + inputs.add(new RectangleButton( + resources, + R.drawable.button_zl_pressed, + R.drawable.button_zl, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.ZL, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.ZL) + )); + inputs.add(new RectangleButton( + resources, + R.drawable.button_l_pressed, + R.drawable.button_l, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.L, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.L) + )); + inputs.add(new RectangleButton( + resources, + R.drawable.button_zr_pressed, + R.drawable.button_zr, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.ZR, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.ZR) + )); + inputs.add(new RectangleButton( + resources, + R.drawable.button_r_pressed, + R.drawable.button_r, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.R, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.R) + )); + inputs.add(new Joystick( + resources, + R.drawable.stick_background, + R.drawable.stick_inner_pressed, + R.drawable.stick_inner, + InputOverlaySurfaceView.this::onJoystickStateChange, + OverlayJoystick.RIGHT, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.RIGHT_AXIS) + )); + } + + inputs.add(new RoundButton( + resources, + R.drawable.button_minus_pressed, + R.drawable.button_minus, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.MINUS, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.MINUS) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_plus_pressed, + R.drawable.button_plus, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.PLUS, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.PLUS) + )); + + inputs.add(new DPadInput( + resources, + R.drawable.dpad_background, + R.drawable.dpad_button_pressed, + R.drawable.dpad_button, + InputOverlaySurfaceView.this::onButtonStateChange, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.DPAD) + )); + inputs.add(new Joystick( + resources, + R.drawable.stick_background, + R.drawable.stick_inner_pressed, + R.drawable.stick_inner, + InputOverlaySurfaceView.this::onJoystickStateChange, + OverlayJoystick.LEFT, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.LEFT_AXIS) + )); + + if (nativeControllerType == NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) { + inputs.add(new RoundButton( + resources, + R.drawable.button_c_pressed, + R.drawable.button_c, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.C, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.C) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_z_pressed, + R.drawable.button_z, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.Z, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.Z) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_home_pressed, + R.drawable.button_home, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.HOME, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.HOME) + )); + } + if (nativeControllerType != NativeLibrary.EMULATED_CONTROLLER_TYPE_CLASSIC + && nativeControllerType != NativeLibrary.EMULATED_CONTROLLER_TYPE_WIIMOTE) { + inputs.add(new RoundButton( + resources, + R.drawable.button_stick_pressed, + R.drawable.button_stick, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.L_STICK, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.L_STICK) + )); + inputs.add(new RoundButton( + resources, + R.drawable.button_stick_pressed, + R.drawable.button_stick, + InputOverlaySurfaceView.this::onButtonStateChange, + OverlayButton.R_STICK, + getOverlaySettingsForInput(InputOverlaySettingsProvider.Input.R_STICK) + )); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setWillNotDraw(false); + setInputs(); + requestFocus(); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + for (var input : inputs) { + input.draw(canvas); + } + } + + Input currentConfiguredInput = null; + + private boolean onEditPosition(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN -> { + int x = (int) event.getX(); + int y = (int) event.getY(); + for (var input : inputs) { + if (input.isInside(x, y)) { + currentConfiguredInput = input; + input.enableDrawingBoundingRect(getResources().getColor(R.color.purple, getContext().getTheme())); + return true; + } + } + } + case MotionEvent.ACTION_UP -> { + if (currentConfiguredInput != null) { + currentConfiguredInput.disableDrawingBoundingRect(); + currentConfiguredInput = null; + return true; + } + } + case MotionEvent.ACTION_MOVE -> { + if (currentConfiguredInput != null) { + int x = (int) event.getX(); + int y = (int) event.getY(); + currentConfiguredInput.moveInput(x, y, getWidth(), getHeight()); + return true; + } + } + } + return false; + } + + private boolean onEditSize(MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN -> { + int x = (int) event.getX(); + int y = (int) event.getY(); + for (var input : inputs) { + if (input.isInside(x, y)) { + currentConfiguredInput = input; + input.enableDrawingBoundingRect(getResources().getColor(R.color.red, getContext().getTheme())); + return true; + } + } + } + case MotionEvent.ACTION_UP -> { + if (currentConfiguredInput != null) { + currentConfiguredInput.disableDrawingBoundingRect(); + currentConfiguredInput = null; + return true; + } + } + case MotionEvent.ACTION_MOVE -> { + if (currentConfiguredInput != null) { + var histSize = event.getHistorySize(); + if (event.getHistorySize() >= 2) { + var x1 = event.getHistoricalX(0); + var y1 = event.getHistoricalY(0); + var x2 = event.getHistoricalX(histSize - 1); + var y2 = event.getHistoricalY(histSize - 1); + currentConfiguredInput.resize( + (int) (x2 - x1), + (int) (y2 - y1), + getWidth(), + getHeight()); + } + return true; + } + } + } + return false; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + boolean touchEventProcessed = false; + if (inputMode == DEFAULT) { + for (var input : inputs) { + if (input.onTouch(event)) touchEventProcessed = true; + } + } else if (inputMode == InputMode.EDIT_POSITION) { + touchEventProcessed = onEditPosition(event); + } else if (inputMode == InputMode.EDIT_SIZE) { + touchEventProcessed = onEditSize(event); + } + + if (touchEventProcessed) { + invalidate(); + } + + return true; + } +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java index 885bcb4d..d540e0e2 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/InputSettingsFragment.java @@ -21,6 +21,8 @@ public class InputSettingsFragment extends Fragment { FragmentInputSettingsBinding binding = FragmentInputSettingsBinding.inflate(inflater, container, false); GenericRecyclerViewAdapter genericRecyclerViewAdapter = new GenericRecyclerViewAdapter(); + genericRecyclerViewAdapter.addRecyclerViewItem(new SimpleButtonRecyclerViewItem(getString(R.string.input_overlay_settings), () -> NavHostFragment.findNavController(InputSettingsFragment.this).navigate(R.id.action_inputSettingsFragment_to_inputOverlaySettingsFragment))); + for (int index = 0; index < NativeLibrary.MAX_CONTROLLERS; index++) { int controllerIndex = index; int controllerType = NativeLibrary.isControllerDisabled(controllerIndex) ? NativeLibrary.EMULATED_CONTROLLER_TYPE_DISABLED : NativeLibrary.getControllerType(controllerIndex); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/Joystick.java b/src/android/app/src/main/java/info/cemu/Cemu/Joystick.java new file mode 100644 index 00000000..190de4f7 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/Joystick.java @@ -0,0 +1,130 @@ +package info.cemu.Cemu; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.view.MotionEvent; + +import androidx.annotation.DrawableRes; +import androidx.core.content.res.ResourcesCompat; + +import java.util.Objects; + +public class Joystick extends Input { + private final Drawable iconPressed; + private final Drawable iconNotPressed; + private final Drawable joystickBackground; + private Drawable icon; + private int currentPointerId = -1; + private int centreX = 0; + private int centreY = 0; + private int radius = 0; + private int innerRadius = 0; + + public interface StickStateChangeListener { + void onStickStateChange(InputOverlaySurfaceView.OverlayJoystick joystick, float x, float y); + } + + private final StickStateChangeListener stickStateChangeListener; + private final InputOverlaySurfaceView.OverlayJoystick joystick; + + public Joystick(Resources resources, @DrawableRes int joystickBackgroundId, @DrawableRes int pressedStickId, @DrawableRes int notPressedStickId, StickStateChangeListener stickStateChangeListener, InputOverlaySurfaceView.OverlayJoystick joystick, InputOverlaySettingsProvider.InputOverlaySettings settings) { + super(settings); + joystickBackground = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, joystickBackgroundId, null)); + iconPressed = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, pressedStickId, null)); + iconNotPressed = Objects.requireNonNull(ResourcesCompat.getDrawable(resources, notPressedStickId, null)); + icon = iconNotPressed; + this.stickStateChangeListener = stickStateChangeListener; + this.joystick = joystick; + configure(); + } + + private void updateState(boolean pressed, float x, float y) { + if (pressed) { + icon = iconPressed; + } else { + icon = iconNotPressed; + } + stickStateChangeListener.onStickStateChange(joystick, x, y); + int newCentreX = (int) (centreX + radius * x); + int newCentreY = (int) (centreY + radius * y); + iconPressed.setBounds(newCentreX - innerRadius, newCentreY - innerRadius, newCentreX + innerRadius, newCentreY + innerRadius); + } + + @Override + protected void configure() { + var rect = settings.getRect(); + this.centreX = rect.centerX(); + this.centreY = rect.centerY(); + this.radius = Math.min(rect.width(), rect.height()) / 2; + this.innerRadius = (int) (radius * 0.65f); + joystickBackground.setBounds(centreX - radius, centreY - radius, centreX + radius, centreY + radius); + joystickBackground.setAlpha(settings.getAlpha()); + iconPressed.setAlpha(settings.getAlpha()); + iconPressed.setBounds(centreX - innerRadius, centreY - innerRadius, centreX + innerRadius, centreY + innerRadius); + iconNotPressed.setAlpha(settings.getAlpha()); + iconNotPressed.setBounds(centreX - innerRadius, centreY - innerRadius, centreX + innerRadius, centreY + innerRadius); + } + + @Override + public boolean onTouch(MotionEvent event) { + boolean stateUpdated = false; + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + int pointerIndex = event.getActionIndex(); + int x = (int) event.getX(pointerIndex); + int y = (int) event.getY(pointerIndex); + if (isInside(x, y)) { + currentPointerId = event.getPointerId(pointerIndex); + updateState(true, 0.0f, 0.0f); + stateUpdated = true; + } + } + case MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + if (currentPointerId == event.getPointerId(event.getActionIndex())) { + currentPointerId = -1; + updateState(false, 0.0f, 0.0f); + stateUpdated = true; + } + } + case MotionEvent.ACTION_MOVE -> { + if (currentPointerId == -1) { + break; + } + for (int i = 0; i < event.getPointerCount(); i++) { + if (currentPointerId != event.getPointerId(i)) { + continue; + } + float x = (event.getX(i) - centreX) / radius; + float y = (event.getY(i) - centreY) / radius; + float norm = (float) Math.sqrt(x * x + y * y); + if (norm > 1.0f) { + x /= norm; + y /= norm; + } + updateState(true, x, y); + stateUpdated = true; + break; + } + } + } + return stateUpdated; + } + + @Override + protected void resetInput() { + updateState(false, 0, 0); + } + + @Override + protected void drawInput(Canvas canvas) { + joystickBackground.draw(canvas); + icon.draw(canvas); + } + + @Override + public boolean isInside(int x, int y) { + return ((x - centreX) * (x - centreX) + (y - centreY) * (y - centreY)) <= radius * radius; + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java index 8b5ae856..9d0e9915 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/MainActivity.java @@ -16,7 +16,7 @@ public class MainActivity extends AppCompatActivity { setContentView(binding.getRoot()); binding.fab.setOnClickListener( view -> { - Intent intent = new Intent(this, SettingsActivity.class); + Intent intent = new Intent(MainActivity.this, SettingsActivity.class); startActivity(intent); } ); diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java index 5475474f..0aa1e735 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java @@ -379,4 +379,8 @@ public class NativeLibrary { public static native void setGraphicPackActivePreset(long id, String category, String preset); public static native ArrayList getGraphicPackPresets(long id); + + public static native void onOverlayButton(int controllerIndex, int mappingId, boolean value); + + public static native void onOverlayAxis(int controllerIndex, int mappingId, float value); } diff --git a/src/android/app/src/main/java/info/cemu/Cemu/ProgressResponseBody.java b/src/android/app/src/main/java/info/cemu/Cemu/ProgressResponseBody.java new file mode 100644 index 00000000..d3bf43f3 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/ProgressResponseBody.java @@ -0,0 +1,66 @@ +package info.cemu.Cemu; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.MediaType; +import okhttp3.ResponseBody; +import okio.Buffer; +import okio.BufferedSource; +import okio.ForwardingSource; +import okio.Okio; +import okio.Source; + +class ProgressResponseBody extends ResponseBody { + + private final ResponseBody responseBody; + private final ProgressListener progressListener; + private BufferedSource bufferedSource; + + ProgressResponseBody(ResponseBody responseBody, ProgressListener progressListener) { + this.responseBody = responseBody; + this.progressListener = progressListener; + } + + @Override + public MediaType contentType() { + return responseBody.contentType(); + } + + @Override + public long contentLength() { + return responseBody.contentLength(); + } + + @NonNull + @Override + public BufferedSource source() { + if (bufferedSource == null) { + bufferedSource = Okio.buffer(source(responseBody.source())); + } + return bufferedSource; + } + + private Source source(Source source) { + return new ForwardingSource(source) { + long totalBytesRead = 0L; + + @Override + public long read(@NonNull Buffer sink, long byteCount) throws IOException { + long bytesRead = super.read(sink, byteCount); + sink.flush(); + if (bytesRead == -1) + return 100; + totalBytesRead += bytesRead; + progressListener.update((int) (100f * totalBytesRead / responseBody.contentLength())); + return bytesRead; + } + }; + } + + public interface ProgressListener { + void update(int progress); + } +} + diff --git a/src/android/app/src/main/java/info/cemu/Cemu/RectangleButton.java b/src/android/app/src/main/java/info/cemu/Cemu/RectangleButton.java new file mode 100644 index 00000000..58a0d8a9 --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/RectangleButton.java @@ -0,0 +1,34 @@ +package info.cemu.Cemu; + +import android.content.res.Resources; + +import androidx.annotation.DrawableRes; + +public class RectangleButton extends Button { + int left; + int top; + int right; + int bottom; + + public RectangleButton(Resources resources, @DrawableRes int pressedButtonId, @DrawableRes int notPressedButtonId, ButtonStateChangeListener buttonStateChangeListener, InputOverlaySurfaceView.OverlayButton overlayButton, InputOverlaySettingsProvider.InputOverlaySettings settings) { + super(resources, pressedButtonId, notPressedButtonId, buttonStateChangeListener, overlayButton, settings); + configure(); + } + + @Override + protected void configure() { + var rect = settings.getRect(); + this.left = rect.left; + this.top = rect.top; + this.right = rect.right; + this.bottom = rect.bottom; + iconPressed.setBounds(left, top, right, bottom); + iconNotPressed.setBounds(left, top, right, bottom); + } + + @Override + public boolean isInside(int x, int y) { + return x >= left && x < right && y >= top && y < bottom; + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/RoundButton.java b/src/android/app/src/main/java/info/cemu/Cemu/RoundButton.java new file mode 100644 index 00000000..fb98bc4c --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/Cemu/RoundButton.java @@ -0,0 +1,38 @@ +package info.cemu.Cemu; + +import android.content.res.Resources; +import android.graphics.Rect; + +import androidx.annotation.DrawableRes; + +public class RoundButton extends Button { + int centreX; + int centreY; + int radius; + + RoundButton(Resources resources, @DrawableRes int pressedButtonId, @DrawableRes int notPressedButtonId, ButtonStateChangeListener buttonStateChangeListener, InputOverlaySurfaceView.OverlayButton overlayButton, InputOverlaySettingsProvider.InputOverlaySettings settings) { + super(resources, pressedButtonId, notPressedButtonId, buttonStateChangeListener, overlayButton, settings); + configure(); + } + + @Override + protected void configure() { + var rect = settings.getRect(); + centreX = rect.centerX(); + centreY = rect.centerY(); + radius = Math.min(rect.width(), rect.height()) / 2; + var iconRect = new Rect( + centreX - radius, + centreY - radius, + centreX + radius, + centreY + radius); + iconPressed.setBounds(iconRect); + iconNotPressed.setBounds(iconRect); + } + + @Override + public boolean isInside(int x, int y) { + return ((x - centreX) * (x - centreX) + (y - centreY) * (y - centreY)) <= radius * radius; + } + +} diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java index f98f4eaf..12b320a7 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SelectionAdapter.java @@ -4,21 +4,26 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; +import android.widget.TextView; + +import androidx.annotation.StringRes; import com.google.android.material.radiobutton.MaterialRadioButton; import java.util.ArrayList; import java.util.List; import java.util.OptionalInt; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.IntStream; public class SelectionAdapter extends BaseSelectionAdapter { public static class ChoiceItem { - int choiceTextResourceId; + Consumer setTextForChoice; T choiceValue; - public ChoiceItem(int choiceTextResourceId, T choiceValue) { - this.choiceTextResourceId = choiceTextResourceId; + public ChoiceItem(Consumer setTextForChoice, T choiceValue) { + this.setTextForChoice = setTextForChoice; this.choiceValue = choiceValue; } } @@ -50,7 +55,7 @@ public class SelectionAdapter extends BaseSelectionAdapter { @Override protected void setRadioButtonText(MaterialRadioButton radioButton, int position) { - radioButton.setText(choiceItems.get(position).choiceTextResourceId); + choiceItems.get(position).setTextForChoice.accept(radioButton); } @Override diff --git a/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java b/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java index b4aa8fa6..bb8e4b65 100644 --- a/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java +++ b/src/android/app/src/main/java/info/cemu/Cemu/SingleSelectionRecyclerViewItem.java @@ -5,6 +5,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -31,6 +32,7 @@ public class SingleSelectionRecyclerViewItem implements RecyclerViewItem { private final OnItemSelectedListener onItemSelectedListener; private SingleSelectionViewHolder singleSelectionViewHolder; private RecyclerView.Adapter recyclerViewAdapter; + private AlertDialog selectAlertDialog; public SingleSelectionRecyclerViewItem(String label, String description, BaseSelectionAdapter selectionAdapter, OnItemSelectedListener onItemSelectedListener) { this.label = label; @@ -56,16 +58,20 @@ public class SingleSelectionRecyclerViewItem implements RecyclerViewItem { singleSelectionViewHolder.label.setText(label); singleSelectionViewHolder.description.setText(description); singleSelectionViewHolder.itemView.setOnClickListener(view -> { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(viewHolder.itemView.getContext()); - builder.setTitle(label).setAdapter(selectionAdapter, (dialogInterface, position) -> { - if (!selectionAdapter.isEnabled(position)) - return; - T selectedValue = selectionAdapter.getItem(position); - selectionAdapter.setSelectedValue(selectedValue); - if (onItemSelectedListener != null) - onItemSelectedListener.onItemSelected(selectedValue, SingleSelectionRecyclerViewItem.this); + if (selectAlertDialog == null) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(viewHolder.itemView.getContext()); + selectAlertDialog = builder.setTitle(label).setAdapter(selectionAdapter, (dialogInterface, position) -> { + if (!selectionAdapter.isEnabled(position)) + return; + T selectedValue = selectionAdapter.getItem(position); + selectionAdapter.setSelectedValue(selectedValue); + if (onItemSelectedListener != null) + onItemSelectedListener.onItemSelected(selectedValue, SingleSelectionRecyclerViewItem.this); - }).setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()).show(); + }).setNegativeButton(R.string.cancel, (dialogInterface, i) -> dialogInterface.dismiss()).show(); + return; + } + selectAlertDialog.show(); }); } } diff --git a/src/android/app/src/main/res/drawable/button_a.xml b/src/android/app/src/main/res/drawable/button_a.xml new file mode 100644 index 00000000..d81250c5 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_a.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_a_pressed.xml b/src/android/app/src/main/res/drawable/button_a_pressed.xml new file mode 100644 index 00000000..828399af --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_a_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_b.xml b/src/android/app/src/main/res/drawable/button_b.xml new file mode 100644 index 00000000..f16c1506 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_b.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_b_pressed.xml b/src/android/app/src/main/res/drawable/button_b_pressed.xml new file mode 100644 index 00000000..4f70709b --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_b_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_c.xml b/src/android/app/src/main/res/drawable/button_c.xml new file mode 100644 index 00000000..f80f90be --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_c.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_c_pressed.xml b/src/android/app/src/main/res/drawable/button_c_pressed.xml new file mode 100644 index 00000000..6a46d539 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_c_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_home.xml b/src/android/app/src/main/res/drawable/button_home.xml new file mode 100644 index 00000000..f519e6d7 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_home.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_home_pressed.xml b/src/android/app/src/main/res/drawable/button_home_pressed.xml new file mode 100644 index 00000000..3d9f52a6 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_home_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_l.xml b/src/android/app/src/main/res/drawable/button_l.xml new file mode 100644 index 00000000..a6b54c61 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_l.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_l_pressed.xml b/src/android/app/src/main/res/drawable/button_l_pressed.xml new file mode 100644 index 00000000..a1e99d4b --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_l_pressed.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_minus.xml b/src/android/app/src/main/res/drawable/button_minus.xml new file mode 100644 index 00000000..de1b123a --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_minus.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_minus_pressed.xml b/src/android/app/src/main/res/drawable/button_minus_pressed.xml new file mode 100644 index 00000000..6b0f46b7 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_minus_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_one.xml b/src/android/app/src/main/res/drawable/button_one.xml new file mode 100644 index 00000000..c7bdc4a0 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_one.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_one_pressed.xml b/src/android/app/src/main/res/drawable/button_one_pressed.xml new file mode 100644 index 00000000..372c9688 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_one_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_plus.xml b/src/android/app/src/main/res/drawable/button_plus.xml new file mode 100644 index 00000000..00a892ee --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_plus.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_plus_pressed.xml b/src/android/app/src/main/res/drawable/button_plus_pressed.xml new file mode 100644 index 00000000..e0d72814 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_plus_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_r.xml b/src/android/app/src/main/res/drawable/button_r.xml new file mode 100644 index 00000000..4ac99b4e --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_r.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_r_pressed.xml b/src/android/app/src/main/res/drawable/button_r_pressed.xml new file mode 100644 index 00000000..08c958f6 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_r_pressed.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_stick.xml b/src/android/app/src/main/res/drawable/button_stick.xml new file mode 100644 index 00000000..684e43a2 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_stick.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/android/app/src/main/res/drawable/button_stick_pressed.xml b/src/android/app/src/main/res/drawable/button_stick_pressed.xml new file mode 100644 index 00000000..432203b8 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_stick_pressed.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/src/android/app/src/main/res/drawable/button_two.xml b/src/android/app/src/main/res/drawable/button_two.xml new file mode 100644 index 00000000..280ecb41 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_two.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_two_pressed.xml b/src/android/app/src/main/res/drawable/button_two_pressed.xml new file mode 100644 index 00000000..099f1d47 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_two_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_x.xml b/src/android/app/src/main/res/drawable/button_x.xml new file mode 100644 index 00000000..9410fd0a --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_x.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_x_pressed.xml b/src/android/app/src/main/res/drawable/button_x_pressed.xml new file mode 100644 index 00000000..9b97d645 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_x_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_y.xml b/src/android/app/src/main/res/drawable/button_y.xml new file mode 100644 index 00000000..ccfabb62 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_y.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_y_pressed.xml b/src/android/app/src/main/res/drawable/button_y_pressed.xml new file mode 100644 index 00000000..3ea23f18 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_y_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_z.xml b/src/android/app/src/main/res/drawable/button_z.xml new file mode 100644 index 00000000..f509ecff --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_z.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_z_pressed.xml b/src/android/app/src/main/res/drawable/button_z_pressed.xml new file mode 100644 index 00000000..7c682cd9 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_z_pressed.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_zl.xml b/src/android/app/src/main/res/drawable/button_zl.xml new file mode 100644 index 00000000..754fc47e --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_zl.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_zl_pressed.xml b/src/android/app/src/main/res/drawable/button_zl_pressed.xml new file mode 100644 index 00000000..3f57b428 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_zl_pressed.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_zr.xml b/src/android/app/src/main/res/drawable/button_zr.xml new file mode 100644 index 00000000..03ef5b9d --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_zr.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/button_zr_pressed.xml b/src/android/app/src/main/res/drawable/button_zr_pressed.xml new file mode 100644 index 00000000..44df81d6 --- /dev/null +++ b/src/android/app/src/main/res/drawable/button_zr_pressed.xml @@ -0,0 +1,15 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/dpad_background.xml b/src/android/app/src/main/res/drawable/dpad_background.xml new file mode 100644 index 00000000..bbe39bb9 --- /dev/null +++ b/src/android/app/src/main/res/drawable/dpad_background.xml @@ -0,0 +1,12 @@ + + + diff --git a/src/android/app/src/main/res/drawable/dpad_button.xml b/src/android/app/src/main/res/drawable/dpad_button.xml new file mode 100644 index 00000000..e4e49338 --- /dev/null +++ b/src/android/app/src/main/res/drawable/dpad_button.xml @@ -0,0 +1,11 @@ + + + diff --git a/src/android/app/src/main/res/drawable/dpad_button_pressed.xml b/src/android/app/src/main/res/drawable/dpad_button_pressed.xml new file mode 100644 index 00000000..186a63f6 --- /dev/null +++ b/src/android/app/src/main/res/drawable/dpad_button_pressed.xml @@ -0,0 +1,11 @@ + + + diff --git a/src/android/app/src/main/res/drawable/ic_restart.xml b/src/android/app/src/main/res/drawable/ic_restart.xml new file mode 100644 index 00000000..dbd38d71 --- /dev/null +++ b/src/android/app/src/main/res/drawable/ic_restart.xml @@ -0,0 +1,10 @@ + + + diff --git a/src/android/app/src/main/res/drawable/stick_background.xml b/src/android/app/src/main/res/drawable/stick_background.xml new file mode 100644 index 00000000..347c1b3d --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_background.xml @@ -0,0 +1,16 @@ + + + + diff --git a/src/android/app/src/main/res/drawable/stick_inner.xml b/src/android/app/src/main/res/drawable/stick_inner.xml new file mode 100644 index 00000000..39e76644 --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_inner.xml @@ -0,0 +1,12 @@ + + + diff --git a/src/android/app/src/main/res/drawable/stick_inner_pressed.xml b/src/android/app/src/main/res/drawable/stick_inner_pressed.xml new file mode 100644 index 00000000..0c5f7c96 --- /dev/null +++ b/src/android/app/src/main/res/drawable/stick_inner_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/src/android/app/src/main/res/layout/activity_overlay_configure.xml b/src/android/app/src/main/res/layout/activity_overlay_configure.xml new file mode 100644 index 00000000..aba9b093 --- /dev/null +++ b/src/android/app/src/main/res/layout/activity_overlay_configure.xml @@ -0,0 +1,38 @@ + + + + + + + +