Added input overlay

This commit is contained in:
SSimco
2024-05-26 17:51:32 +03:00
parent 8a4b99b3e1
commit f1dec91ae1
73 changed files with 2581 additions and 29 deletions
+10 -1
View File
@@ -4,6 +4,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<application
android:name="info.cemu.Cemu.CemuApplication"
android:allowBackup="true"
@@ -20,7 +22,14 @@
android:name=".EmulationActivity"
android:launchMode="singleTop"
android:parentActivityName=".MainActivity"
android:screenOrientation="userLandscape" />
android:screenOrientation="userLandscape"
tools:ignore="DiscouragedApi" />
<activity
android:name=".InputOverlayConfigureActivity"
android:launchMode="singleTop"
android:parentActivityName=".SettingsActivity"
android:screenOrientation="userLandscape"
tools:ignore="DiscouragedApi" />
<activity
android:name=".SettingsActivity"
@@ -25,6 +25,18 @@ class AndroidEmulatedController {
controller = std::unique_ptr<AndroidEmulatedController>(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)
@@ -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;
@@ -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);
}
@@ -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<Integer> 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<Integer> gamepadChannelsSelectionAdapter = new SelectionAdapter<>(gamepadChannelsChoices, gamepadChannels);
SingleSelectionRecyclerViewItem<Integer> gamepadChannelsModeSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.gamepad_channels),
@@ -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);
}
}
@@ -0,0 +1,5 @@
package info.cemu.Cemu;
public interface ButtonStateChangeListener {
void onButtonStateChange(InputOverlaySurfaceView.OverlayButton button, boolean state);
}
@@ -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<Drawable> getPressedButtonIcon = () -> Objects.requireNonNull(ResourcesCompat.getDrawable(resources, pressedButtonId, null));
Supplier<Drawable> 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);
}
}
@@ -14,7 +14,7 @@ public class EmulatedControllerTypeAdapter extends SelectionAdapter<Integer> {
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);
}
@@ -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();
@@ -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<Integer> vsyncSelectionAdapter = new SelectionAdapter<>(vsyncChoices, vsyncMode);
SingleSelectionRecyclerViewItem<Integer> vsyncModeSelection = new SingleSelectionRecyclerViewItem<>(getString(R.string.vsync),
@@ -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);
}
@@ -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);
}
}
@@ -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<Integer> 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<Integer> 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();
}
}
@@ -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<Rect> 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<OverlaySettings> overlaySettingsConsumer;
private OverlaySettings(boolean vibrateOnTouchEnabled, boolean overlayEnabled, int controllerIndex, int alpha, Consumer<OverlaySettings> 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<InputOverlaySettings> inputOverlaySettingsConsumer;
private final int alpha;
private InputOverlaySettings(Rect rectangle, int alpha, Consumer<InputOverlaySettings> 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);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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);
@@ -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;
}
}
@@ -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);
}
);
@@ -379,4 +379,8 @@ public class NativeLibrary {
public static native void setGraphicPackActivePreset(long id, String category, String preset);
public static native ArrayList<GraphicPackPreset> 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);
}

Some files were not shown because too many files have changed in this diff Show More