From a1c57de466855bf826262a1613699ce9c468cadc Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Wed, 8 Mar 2023 17:12:47 -0500 Subject: [PATCH] android: Convert InputOverlay to Kotlin --- .../yuzu/yuzu_emu/overlay/InputOverlay.java | 656 ------------- .../org/yuzu/yuzu_emu/overlay/InputOverlay.kt | 886 ++++++++++++++++++ 2 files changed, 886 insertions(+), 656 deletions(-) delete mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java deleted file mode 100644 index 74119c398c..0000000000 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java +++ /dev/null @@ -1,656 +0,0 @@ -/** - * Copyright 2013 Dolphin Emulator Project - * Licensed under GPLv2+ - * Refer to the license.txt file included. - */ - -package org.yuzu.yuzu_emu.overlay; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.VectorDrawable; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.preference.PreferenceManager; -import android.util.AttributeSet; -import android.util.DisplayMetrics; -import android.view.Display; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.View; -import android.view.View.OnTouchListener; - -import androidx.core.content.ContextCompat; - -import org.yuzu.yuzu_emu.NativeLibrary; -import org.yuzu.yuzu_emu.NativeLibrary.ButtonType; -import org.yuzu.yuzu_emu.NativeLibrary.StickType; -import org.yuzu.yuzu_emu.R; -import org.yuzu.yuzu_emu.utils.EmulationMenuSettings; - -import java.util.HashSet; -import java.util.Set; - -/** - * Draws the interactive input overlay on top of the - * {@link SurfaceView} that is rendering emulation. - */ -public final class InputOverlay extends SurfaceView implements OnTouchListener, SensorEventListener { - private final Set overlayButtons = new HashSet<>(); - private final Set overlayDpads = new HashSet<>(); - private final Set overlayJoysticks = new HashSet<>(); - - private boolean mIsInEditMode = false; - - private SharedPreferences mPreferences; - - private float[] gyro = new float[3]; - private float[] accel = new float[3]; - - private long motionTimestamp; - - /** - * Constructor - * - * @param context The current {@link Context}. - * @param attrs {@link AttributeSet} for parsing XML attributes. - */ - public InputOverlay(Context context, AttributeSet attrs) { - super(context, attrs); - - mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!mPreferences.getBoolean("OverlayInit", false)) { - defaultOverlay(); - } - - // Load the controls. - refreshControls(); - - // Set the on motion sensor listener. - setMotionSensorListener(context); - - // Set the on touch listener. - setOnTouchListener(this); - - // Force draw - setWillNotDraw(false); - - // Request focus for the overlay so it has priority on presses. - requestFocus(); - } - - private void setMotionSensorListener(Context context) { - SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - Sensor gyro_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - Sensor accel_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - - if (gyro_sensor != null) { - sensorManager.registerListener(this, gyro_sensor, SensorManager.SENSOR_DELAY_GAME); - } - if (accel_sensor != null) { - sensorManager.registerListener(this, accel_sensor, SensorManager.SENSOR_DELAY_GAME); - } - } - - - /** - * Resizes a {@link Bitmap} by a given scale factor - * - * @param vectorDrawable The {@link Bitmap} to scale. - * @param scale The scale factor for the bitmap. - * @return The scaled {@link Bitmap} - */ - private static Bitmap getBitmap(VectorDrawable vectorDrawable, float scale) { - Bitmap bitmap = Bitmap.createBitmap((int) (vectorDrawable.getIntrinsicWidth() * scale), - (int) (vectorDrawable.getIntrinsicHeight() * scale), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - vectorDrawable.draw(canvas); - return bitmap; - } - - private static Bitmap getBitmap(Context context, int drawableId, float scale) { - Drawable drawable = ContextCompat.getDrawable(context, drawableId); - if (drawable instanceof BitmapDrawable) { - return BitmapFactory.decodeResource(context.getResources(), drawableId); - } else if (drawable instanceof VectorDrawable) { - return getBitmap((VectorDrawable) drawable, scale); - } else { - throw new IllegalArgumentException("unsupported drawable type"); - } - } - - /** - * Initializes an InputOverlayDrawableButton, given by resId, with all of the - * parameters set for it to be properly shown on the InputOverlay. - *

- * This works due to the way the X and Y coordinates are stored within - * the {@link SharedPreferences}. - *

- * In the input overlay configuration menu, - * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). - * the X and Y coordinates of the button at the END of its touch event - * (when you remove your finger/stylus from the touchscreen) are then stored - * within a SharedPreferences instance so that those values can be retrieved here. - *

- * This has a few benefits over the conventional way of storing the values - * (ie. within the yuzu ini file). - *

    - *
  • No native calls
  • - *
  • Keeps Android-only values inside the Android environment
  • - *
- *

- * Technically no modifications should need to be performed on the returned - * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait - * for Android to call the onDraw method. - * - * @param context The current {@link Context}. - * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State). - * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State). - * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. - * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set. - */ - private static InputOverlayDrawableButton initializeOverlayButton(Context context, - int defaultResId, int pressedResId, int buttonId, String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale; - - switch (buttonId) { - case ButtonType.BUTTON_HOME: - case ButtonType.BUTTON_CAPTURE: - case ButtonType.BUTTON_PLUS: - case ButtonType.BUTTON_MINUS: - scale = 0.35f; - break; - case ButtonType.TRIGGER_L: - case ButtonType.TRIGGER_R: - case ButtonType.TRIGGER_ZL: - case ButtonType.TRIGGER_ZR: - scale = 0.38f; - break; - default: - scale = 0.43f; - break; - } - - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableButton. - final Bitmap defaultStateBitmap = getBitmap(context, defaultResId, scale); - final Bitmap pressedStateBitmap = getBitmap(context, pressedResId, scale); - final InputOverlayDrawableButton overlayDrawable = - new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - String xKey; - String yKey; - - xKey = buttonId + orientation + "-X"; - yKey = buttonId + orientation + "-Y"; - - int drawableX = (int) sPrefs.getFloat(xKey, 0f); - int drawableY = (int) sPrefs.getFloat(yKey, 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableButton. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. - overlayDrawable.setBounds(drawableX - (width / 2), drawableY - (height / 2), drawableX + (width / 2), drawableY + (height / 2)); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2)); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableDpad} - * - * @param context The current {@link Context}. - * @param defaultResId The {@link Bitmap} resource ID of the default sate. - * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction. - * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions. - * @param buttonUp Identifier for the up button. - * @param buttonDown Identifier for the down button. - * @param buttonLeft Identifier for the left button. - * @param buttonRight Identifier for the right button. - * @return the initialized {@link InputOverlayDrawableDpad} - */ - private static InputOverlayDrawableDpad initializeOverlayDpad(Context context, - int defaultResId, - int pressedOneDirectionResId, - int pressedTwoDirectionsResId, - int buttonUp, - int buttonDown, - int buttonLeft, - int buttonRight, - String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on button ID and user preference - float scale = 0.40f; - - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableDpad. - final Bitmap defaultStateBitmap = getBitmap(context, defaultResId, scale); - final Bitmap pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, - scale); - final Bitmap pressedTwoDirectionsStateBitmap = getBitmap(context, pressedTwoDirectionsResId, - scale); - final InputOverlayDrawableDpad overlayDrawable = - new InputOverlayDrawableDpad(res, defaultStateBitmap, - pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap, - buttonUp, buttonDown, buttonLeft, buttonRight); - - // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f); - int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f); - - int width = overlayDrawable.getWidth(); - int height = overlayDrawable.getHeight(); - - // Now set the bounds for the InputOverlayDrawableDpad. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. - overlayDrawable.setBounds(drawableX - (width / 2), drawableY - (height / 2), drawableX + (width / 2), drawableY + (height / 2)); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2)); - - return overlayDrawable; - } - - /** - * Initializes an {@link InputOverlayDrawableJoystick} - * - * @param context The current {@link Context} - * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). - * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). - * @param pressedResInner Resource ID for the pressed inner image of the joystick. - * @param joystick Identifier for which joystick this is. - * @param button Identifier for which joystick button this is. - * @return the initialized {@link InputOverlayDrawableJoystick}. - */ - private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context, - int resOuter, int defaultResInner, int pressedResInner, int joystick, int button, String orientation) { - // Resources handle for fetching the initial Drawable resource. - final Resources res = context.getResources(); - - // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context); - - // Decide scale based on user preference - float scale = 0.40f; - scale *= (sPrefs.getInt("controlScale", 50) + 50); - scale /= 100; - - // Initialize the InputOverlayDrawableJoystick. - final Bitmap bitmapOuter = getBitmap(context, resOuter, scale); - final Bitmap bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f); - final Bitmap bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f); - - // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. - // These were set in the input overlay configuration menu. - int drawableX = (int) sPrefs.getFloat(button + orientation + "-X", 0f); - int drawableY = (int) sPrefs.getFloat(button + orientation + "-Y", 0f); - - float outerScale = 1.66f; - - // Now set the bounds for the InputOverlayDrawableJoystick. - // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. - int outerSize = bitmapOuter.getWidth(); - Rect outerRect = new Rect(drawableX - (outerSize / 2), drawableY - (outerSize / 2), drawableX + (outerSize / 2), drawableY + (outerSize / 2)); - Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale)); - - // Send the drawableId to the joystick so it can be referenced when saving control position. - final InputOverlayDrawableJoystick overlayDrawable - = new InputOverlayDrawableJoystick(res, bitmapOuter, - bitmapInnerDefault, bitmapInnerPressed, - outerRect, innerRect, joystick, button); - - // Need to set the image's position - overlayDrawable.setPosition(drawableX, drawableY); - - return overlayDrawable; - } - - @Override - public void draw(Canvas canvas) { - super.draw(canvas); - - for (InputOverlayDrawableButton button : overlayButtons) { - button.draw(canvas); - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) { - dpad.draw(canvas); - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - joystick.draw(canvas); - } - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - if (isInEditMode()) { - return onTouchWhileEditing(event); - } - boolean should_update_view = false; - for (InputOverlayDrawableButton button : overlayButtons) { - if (!button.updateStatus(event)) { - continue; - } - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, button.getId(), button.getStatus()); - should_update_view = true; - } - - for (InputOverlayDrawableDpad dpad : overlayDpads) { - if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) { - continue; - } - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getUpId(), dpad.getUpStatus()); - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getDownId(), dpad.getDownStatus()); - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getLeftId(), dpad.getLeftStatus()); - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getRightId(), dpad.getRightStatus()); - should_update_view = true; - } - - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - if (!joystick.updateStatus(event)) { - continue; - } - int axisID = joystick.getJoystickId(); - NativeLibrary.onGamePadJoystickEvent(NativeLibrary.Player1Device, axisID, joystick.getXAxis(), joystick.getYAxis()); - NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, joystick.getButtonId(), joystick.getButtonStatus()); - should_update_view = true; - } - - if (should_update_view) { - invalidate(); - } - - if (!mPreferences.getBoolean("isTouchEnabled", true)) { - return true; - } - - int pointerIndex = event.getActionIndex(); - int xPosition = (int) event.getX(pointerIndex); - int yPosition = (int) event.getY(pointerIndex); - int pointerId = event.getPointerId(pointerIndex); - int motion_event = event.getAction() & MotionEvent.ACTION_MASK; - boolean isActionDown = motion_event == MotionEvent.ACTION_DOWN || motion_event == MotionEvent.ACTION_POINTER_DOWN; - boolean isActionMove = motion_event == MotionEvent.ACTION_MOVE; - boolean isActionUp = motion_event == MotionEvent.ACTION_UP || motion_event == MotionEvent.ACTION_POINTER_UP; - - if (isActionDown && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchPressed(pointerId, xPosition, yPosition); - } - - if (isActionMove) { - for (int i = 0; i < event.getPointerCount(); i++) { - int fingerId = event.getPointerId(i); - if (isTouchInputConsumed(fingerId)) { - continue; - } - NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)); - } - } - - if (isActionUp && !isTouchInputConsumed(pointerId)) { - NativeLibrary.onTouchReleased(pointerId); - } - - return true; - } - - private boolean isTouchInputConsumed(int track_id) { - for (InputOverlayDrawableButton button : overlayButtons) { - if (button.getTrackId() == track_id) { - return true; - } - } - for (InputOverlayDrawableDpad dpad : overlayDpads) { - if (dpad.getTrackId() == track_id) { - return true; - } - } - for (InputOverlayDrawableJoystick joystick : overlayJoysticks) { - if (joystick.getTrackId() == track_id) { - return true; - } - } - return false; - } - - public boolean onTouchWhileEditing(MotionEvent event) { - // TODO: Reimplement this - return true; - } - - @Override - public void onSensorChanged(SensorEvent event) { - if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { - accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH; - accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH; - accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH; - } - - if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { - // Investigate why sensor value is off by 12x - gyro[0] = event.values[1] / 12.0f; - gyro[1] = -event.values[0] / 12.0f; - gyro[2] = event.values[2] / 12.0f; - } - - // Only update state on accelerometer data - if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) { - return; - } - - long delta_timestamp = (event.timestamp - motionTimestamp) / 1000; - motionTimestamp = event.timestamp; - NativeLibrary.onGamePadMotionEvent(NativeLibrary.Player1Device, delta_timestamp, gyro[0], gyro[1], gyro[2], accel[0], accel[1], accel[2]); - NativeLibrary.onGamePadMotionEvent(NativeLibrary.ConsoleDevice, delta_timestamp, gyro[0], gyro[1], gyro[2], accel[0], accel[1], accel[2]); - } - - @Override - public void onAccuracyChanged(Sensor sensor, int i) { - } - - private void addOverlayControls(String orientation) { - if (mPreferences.getBoolean("buttonToggle0", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_a, - R.drawable.facebutton_a_depressed, ButtonType.BUTTON_A, orientation)); - } - if (mPreferences.getBoolean("buttonToggle1", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_b, - R.drawable.facebutton_b_depressed, ButtonType.BUTTON_B, orientation)); - } - if (mPreferences.getBoolean("buttonToggle2", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_x, - R.drawable.facebutton_x_depressed, ButtonType.BUTTON_X, orientation)); - } - if (mPreferences.getBoolean("buttonToggle3", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_y, - R.drawable.facebutton_y_depressed, ButtonType.BUTTON_Y, orientation)); - } - if (mPreferences.getBoolean("buttonToggle4", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.l_shoulder, - R.drawable.l_shoulder_depressed, ButtonType.TRIGGER_L, orientation)); - } - if (mPreferences.getBoolean("buttonToggle5", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.r_shoulder, - R.drawable.r_shoulder_depressed, ButtonType.TRIGGER_R, orientation)); - } - if (mPreferences.getBoolean("buttonToggle6", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.zl_trigger, - R.drawable.zl_trigger_depressed, ButtonType.TRIGGER_ZL, orientation)); - } - if (mPreferences.getBoolean("buttonToggle7", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.zr_trigger, - R.drawable.zr_trigger_depressed, ButtonType.TRIGGER_ZR, orientation)); - } - if (mPreferences.getBoolean("buttonToggle8", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_plus, - R.drawable.facebutton_plus_depressed, ButtonType.BUTTON_PLUS, orientation)); - } - if (mPreferences.getBoolean("buttonToggle9", true)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_minus, - R.drawable.facebutton_minus_depressed, ButtonType.BUTTON_MINUS, orientation)); - } - if (mPreferences.getBoolean("buttonToggle10", true)) { - overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad_standard, - R.drawable.dpad_standard_cardinal_depressed, - R.drawable.dpad_standard_diagonal_depressed, - ButtonType.DPAD_UP, ButtonType.DPAD_DOWN, - ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation)); - } - if (mPreferences.getBoolean("buttonToggle11", true)) { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range, - R.drawable.joystick, R.drawable.joystick_depressed, - StickType.STICK_L, ButtonType.STICK_L, orientation)); - } - if (mPreferences.getBoolean("buttonToggle12", true)) { - overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range, - R.drawable.joystick, R.drawable.joystick_depressed, StickType.STICK_R, ButtonType.STICK_R, orientation)); - } - if (mPreferences.getBoolean("buttonToggle13", false)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_home, - R.drawable.facebutton_home_depressed, ButtonType.BUTTON_HOME, orientation)); - } - if (mPreferences.getBoolean("buttonToggle14", false)) { - overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_screenshot, - R.drawable.facebutton_screenshot_depressed, ButtonType.BUTTON_CAPTURE, orientation)); - } - } - - public void refreshControls() { - // Remove all the overlay buttons from the HashSet. - overlayButtons.clear(); - overlayDpads.clear(); - overlayJoysticks.clear(); - - String orientation = - getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ? - "-Portrait" : ""; - - // Add all the enabled overlay items back to the HashSet. - if (EmulationMenuSettings.getShowOverlay()) { - addOverlayControls(orientation); - } - - invalidate(); - } - - private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) { - final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - SharedPreferences.Editor sPrefsEditor = sPrefs.edit(); - sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x); - sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y); - sPrefsEditor.apply(); - } - - public void setIsInEditMode(boolean isInEditMode) { - mIsInEditMode = isInEditMode; - } - - private void defaultOverlay() { - if (!mPreferences.getBoolean("OverlayInit", false)) { - defaultOverlayLandscape(); - } - resetButtonPlacement(); - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - sPrefsEditor.putBoolean("OverlayInit", true); - sPrefsEditor.apply(); - } - - public void resetButtonPlacement() { - defaultOverlayLandscape(); - refreshControls(); - } - - private void defaultOverlayLandscape() { - SharedPreferences.Editor sPrefsEditor = mPreferences.edit(); - // Get screen size - Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay(); - DisplayMetrics outMetrics = new DisplayMetrics(); - display.getRealMetrics(outMetrics); - float maxX = outMetrics.heightPixels; - float maxY = outMetrics.widthPixels; - // Height and width changes depending on orientation. Use the larger value for height. - if (maxY > maxX) { - float tmp = maxX; - maxX = maxY; - maxY = tmp; - } - - Resources res = getResources(); - - // Each value is a percent from max X/Y stored as an int. Have to bring that value down - // to a decimal before multiplying by MAX X/Y. - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_A_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_A_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_B_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_B_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_X_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_X_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_Y_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_Y_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_ZL + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZL_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_ZL + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_ZR + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZR_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_ZR + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_DPAD_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_L_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_R_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_PLUS + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_PLUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_PLUS + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_MINUS + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_MINUS_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_MINUS + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_HOME_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_HOME_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.BUTTON_CAPTURE + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.BUTTON_CAPTURE + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_R + "-X", (((float) res.getInteger(R.integer.SWITCH_STICK_R_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_R + "-Y", (((float) res.getInteger(R.integer.SWITCH_STICK_R_Y) / 1000) * maxY)); - sPrefsEditor.putFloat(ButtonType.STICK_L + "-X", (((float) res.getInteger(R.integer.SWITCH_STICK_L_X) / 1000) * maxX)); - sPrefsEditor.putFloat(ButtonType.STICK_L + "-Y", (((float) res.getInteger(R.integer.SWITCH_STICK_L_Y) / 1000) * maxY)); - - // We want to commit right away, otherwise the overlay could load before this is saved. - sPrefsEditor.commit(); - } - - public boolean isInEditMode() { - return mIsInEditMode; - } -} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt new file mode 100644 index 0000000000..a964b62576 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt @@ -0,0 +1,886 @@ +package org.yuzu.yuzu_emu.overlay + +import android.app.Activity +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.VectorDrawable +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.view.MotionEvent +import android.view.SurfaceView +import android.view.View +import android.view.View.OnTouchListener +import androidx.core.content.ContextCompat +import androidx.preference.PreferenceManager +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.NativeLibrary.ButtonType +import org.yuzu.yuzu_emu.NativeLibrary.StickType +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.utils.EmulationMenuSettings + + +/** + * Draws the interactive input overlay on top of the + * [SurfaceView] that is rendering emulation. + */ +class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), + OnTouchListener, SensorEventListener { + private val overlayButtons: MutableSet = HashSet() + private val overlayDpads: MutableSet = HashSet() + private val overlayJoysticks: MutableSet = HashSet() + private var inEditMode = false + private val preferences: SharedPreferences = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + private val gyro = FloatArray(3) + private val accel = FloatArray(3) + private var motionTimestamp: Long = 0 + + init { + if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { + defaultOverlay() + } + + // Load the controls. + refreshControls() + + // Set the on motion sensor listener. + setMotionSensorListener(context) + + // Set the on touch listener. + setOnTouchListener(this) + + // Force draw + setWillNotDraw(false) + + // Request focus for the overlay so it has priority on presses. + requestFocus() + } + + private fun setMotionSensorListener(context: Context) { + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + if (gyroSensor != null) { + sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME) + } + if (accelSensor != null) { + sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME) + } + } + + override fun draw(canvas: Canvas) { + super.draw(canvas) + for (button in overlayButtons) { + button.draw(canvas) + } + for (dpad in overlayDpads) { + dpad.draw(canvas) + } + for (joystick in overlayJoysticks) { + joystick.draw(canvas) + } + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + if (inEditMode) { + return onTouchWhileEditing(event) + } + + var shouldUpdateView = false + + for (button in overlayButtons) { + if (!button.updateStatus(event)) { + continue + } + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + button.id, + button.status + ) + shouldUpdateView = true + } + + for (dpad in overlayDpads) { + if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlideEnable)) { + continue + } + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + dpad.upId, + dpad.upStatus + ) + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + dpad.downId, + dpad.downStatus + ) + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + dpad.leftId, + dpad.leftStatus + ) + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + dpad.rightId, + dpad.rightStatus + ) + shouldUpdateView = true + } + + for (joystick in overlayJoysticks) { + if (!joystick.updateStatus(event)) { + continue + } + val axisID = joystick.joystickId + NativeLibrary.onGamePadJoystickEvent( + NativeLibrary.Player1Device, + axisID, + joystick.xAxis, + joystick.realYAxis + ) + NativeLibrary.onGamePadButtonEvent( + NativeLibrary.Player1Device, + joystick.buttonId, + joystick.buttonStatus + ) + shouldUpdateView = true + } + + if (shouldUpdateView) + invalidate() + + if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) { + return true + } + + val pointerIndex = event.actionIndex + val xPosition = event.getX(pointerIndex).toInt() + val yPosition = event.getY(pointerIndex).toInt() + val pointerId = event.getPointerId(pointerIndex) + val motionEvent = event.action and MotionEvent.ACTION_MASK + val isActionDown = + motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN + val isActionMove = motionEvent == MotionEvent.ACTION_MOVE + val isActionUp = + motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP + + if (isActionDown && !isTouchInputConsumed(pointerId)) { + NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat()) + } + + if (isActionMove) { + for (i in 0 until event.pointerCount) { + val fingerId = event.getPointerId(i) + if (isTouchInputConsumed(fingerId)) { + continue + } + NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i)) + } + } + + if (isActionUp && !isTouchInputConsumed(pointerId)) { + NativeLibrary.onTouchReleased(pointerId) + } + + return true + } + + private fun isTouchInputConsumed(track_id: Int): Boolean { + for (button in overlayButtons) { + if (button.trackId == track_id) { + return true + } + } + for (dpad in overlayDpads) { + if (dpad.trackId == track_id) { + return true + } + } + for (joystick in overlayJoysticks) { + if (joystick.trackId == track_id) { + return true + } + } + return false + } + + private fun onTouchWhileEditing(event: MotionEvent?): Boolean { + // TODO: Reimplement this + return true + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) { + accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH + accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH + accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH + } + if (event.sensor.type == Sensor.TYPE_GYROSCOPE) { + // Investigate why sensor value is off by 12x + gyro[0] = event.values[1] / 12.0f + gyro[1] = -event.values[0] / 12.0f + gyro[2] = event.values[2] / 12.0f + } + + // Only update state on accelerometer data + if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) { + return + } + val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000 + motionTimestamp = event.timestamp + NativeLibrary.onGamePadMotionEvent( + NativeLibrary.Player1Device, + deltaTimestamp, + gyro[0], + gyro[1], + gyro[2], + accel[0], + accel[1], + accel[2] + ) + NativeLibrary.onGamePadMotionEvent( + NativeLibrary.ConsoleDevice, + deltaTimestamp, + gyro[0], + gyro[1], + gyro[2], + accel[0], + accel[1], + accel[2] + ) + } + + override fun onAccuracyChanged(sensor: Sensor, i: Int) {} + private fun addOverlayControls(orientation: String) { + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_a, + R.drawable.facebutton_a_depressed, + ButtonType.BUTTON_A, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_b, + R.drawable.facebutton_b_depressed, + ButtonType.BUTTON_B, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_x, + R.drawable.facebutton_x_depressed, + ButtonType.BUTTON_X, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_y, + R.drawable.facebutton_y_depressed, + ButtonType.BUTTON_Y, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.l_shoulder, + R.drawable.l_shoulder_depressed, + ButtonType.TRIGGER_L, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.r_shoulder, + R.drawable.r_shoulder_depressed, + ButtonType.TRIGGER_R, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.zl_trigger, + R.drawable.zl_trigger_depressed, + ButtonType.TRIGGER_ZL, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.zr_trigger, + R.drawable.zr_trigger_depressed, + ButtonType.TRIGGER_ZR, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_plus, + R.drawable.facebutton_plus_depressed, + ButtonType.BUTTON_PLUS, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_minus, + R.drawable.facebutton_minus_depressed, + ButtonType.BUTTON_MINUS, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) { + overlayDpads.add( + initializeOverlayDpad( + context, + R.drawable.dpad_standard, + R.drawable.dpad_standard_cardinal_depressed, + R.drawable.dpad_standard_diagonal_depressed, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.joystick_range, + R.drawable.joystick, + R.drawable.joystick_depressed, + StickType.STICK_L, + ButtonType.STICK_L, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) { + overlayJoysticks.add( + initializeOverlayJoystick( + context, + R.drawable.joystick_range, + R.drawable.joystick, + R.drawable.joystick_depressed, + StickType.STICK_R, + ButtonType.STICK_R, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_home, + R.drawable.facebutton_home_depressed, + ButtonType.BUTTON_HOME, + orientation + ) + ) + } + if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) { + overlayButtons.add( + initializeOverlayButton( + context, + R.drawable.facebutton_screenshot, + R.drawable.facebutton_screenshot_depressed, + ButtonType.BUTTON_CAPTURE, + orientation + ) + ) + } + } + + fun refreshControls() { + // Remove all the overlay buttons from the HashSet. + overlayButtons.clear() + overlayDpads.clear() + overlayJoysticks.clear() + val orientation = + if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" + + // Add all the enabled overlay items back to the HashSet. + if (EmulationMenuSettings.showOverlay) { + addOverlayControls(orientation) + } + invalidate() + } + + private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) { + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() + .putFloat("$sharedPrefsId$orientation-X", x.toFloat()) + .putFloat("$sharedPrefsId$orientation-Y", y.toFloat()) + .apply() + } + + fun setIsInEditMode(editMode: Boolean) { + inEditMode = editMode + } + + private fun defaultOverlay() { + if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { + defaultOverlayLandscape() + } + + resetButtonPlacement() + preferences.edit() + .putBoolean(Settings.PREF_OVERLAY_INIT, true) + .apply() + } + + fun resetButtonPlacement() { + defaultOverlayLandscape() + refreshControls() + } + + private fun defaultOverlayLandscape() { + // Get screen size + val display = (context as Activity).windowManager.defaultDisplay + val outMetrics = DisplayMetrics() + display.getRealMetrics(outMetrics) + var maxX = outMetrics.heightPixels.toFloat() + var maxY = outMetrics.widthPixels.toFloat() + // Height and width changes depending on orientation. Use the larger value for height. + if (maxY > maxX) { + val tmp = maxX + maxX = maxY + maxY = tmp + } + val res = resources + + // Each value is a percent from max X/Y stored as an int. Have to bring that value down + // to a decimal before multiplying by MAX X/Y. + preferences.edit() + .putFloat( + ButtonType.BUTTON_A.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_A.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_B.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_X.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_Y.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_ZL.toString() + "-X", + res.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_ZL.toString() + "-Y", + res.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_ZR.toString() + "-X", + res.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_ZR.toString() + "-Y", + res.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.DPAD_UP.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.DPAD_UP.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-X", + res.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_L.toString() + "-Y", + res.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-X", + res.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.TRIGGER_R.toString() + "-Y", + res.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_PLUS.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_PLUS.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_MINUS.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_MINUS.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_HOME.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_HOME.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.BUTTON_CAPTURE.toString() + "-X", + res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.BUTTON_CAPTURE.toString() + "-Y", + res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_R.toString() + "-X", + res.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_R.toString() + "-Y", + res.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 * maxY + ) + .putFloat( + ButtonType.STICK_L.toString() + "-X", + res.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 * maxX + ) + .putFloat( + ButtonType.STICK_L.toString() + "-Y", + res.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 * maxY + ) + .commit() + // We want to commit right away, otherwise the overlay could load before this is saved. + } + + override fun isInEditMode(): Boolean { + return inEditMode + } + + companion object { + /** + * Resizes a [Bitmap] by a given scale factor + * + * @param vectorDrawable The {@link Bitmap} to scale. + * @param scale The scale factor for the bitmap. + * @return The scaled [Bitmap] + */ + private fun getBitmap(vectorDrawable: VectorDrawable, scale: Float): Bitmap { + val bitmap = Bitmap.createBitmap( + (vectorDrawable.intrinsicWidth * scale).toInt(), + (vectorDrawable.intrinsicHeight * scale).toInt(), + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) + vectorDrawable.draw(canvas) + return bitmap + } + + private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap { + return when (val drawable = ContextCompat.getDrawable(context, drawableId)) { + is BitmapDrawable -> BitmapFactory.decodeResource(context.resources, drawableId) + is VectorDrawable -> getBitmap(drawable, scale) + else -> throw IllegalArgumentException("Unsupported drawable type") + } + } + + /** + * Initializes an InputOverlayDrawableButton, given by resId, with all of the + * parameters set for it to be properly shown on the InputOverlay. + * + * + * This works due to the way the X and Y coordinates are stored within + * the [SharedPreferences]. + * + * + * In the input overlay configuration menu, + * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay). + * the X and Y coordinates of the button at the END of its touch event + * (when you remove your finger/stylus from the touchscreen) are then stored + * within a SharedPreferences instance so that those values can be retrieved here. + * + * + * This has a few benefits over the conventional way of storing the values + * (ie. within the yuzu ini file). + * + * * No native calls + * * Keeps Android-only values inside the Android environment + * + * + * + * Technically no modifications should need to be performed on the returned + * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait + * for Android to call the onDraw method. + * + * @param context The current [Context]. + * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State). + * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State). + * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents. + * @return An [InputOverlayDrawableButton] with the correct drawing bounds set. + */ + private fun initializeOverlayButton( + context: Context, + defaultResId: Int, + pressedResId: Int, + buttonId: Int, + orientation: String + ): InputOverlayDrawableButton { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on button ID and user preference + var scale: Float = when (buttonId) { + ButtonType.BUTTON_HOME, + ButtonType.BUTTON_CAPTURE, + ButtonType.BUTTON_PLUS, + ButtonType.BUTTON_MINUS -> 0.35f + ButtonType.TRIGGER_L, + ButtonType.TRIGGER_R, + ButtonType.TRIGGER_ZL, + ButtonType.TRIGGER_ZR -> 0.38f + else -> 0.43f + } + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableButton. + val defaultStateBitmap = getBitmap(context, defaultResId, scale) + val pressedStateBitmap = getBitmap(context, pressedResId, scale) + val overlayDrawable = + InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId) + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val xKey = "$buttonId$orientation-X" + val yKey = "$buttonId$orientation-Y" + val drawableX = sPrefs.getFloat(xKey, 0f).toInt() + val drawableY = sPrefs.getFloat(yKey, 0f).toInt() + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableButton. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be. + overlayDrawable.setBounds( + drawableX - (width / 2), + drawableY - (height / 2), + drawableX + (width / 2), + drawableY + (height / 2) + ) + + // Need to set the image's position + overlayDrawable.setPosition( + drawableX - (width / 2), + drawableY - (height / 2) + ) + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableDpad] + * + * @param context The current [Context]. + * @param defaultResId The [Bitmap] resource ID of the default sate. + * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed sate in one direction. + * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed sate in two directions. + * @return the initialized [InputOverlayDrawableDpad] + */ + private fun initializeOverlayDpad( + context: Context, + defaultResId: Int, + pressedOneDirectionResId: Int, + pressedTwoDirectionsResId: Int, + orientation: String + ): InputOverlayDrawableDpad { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on button ID and user preference + var scale = 0.40f + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableDpad. + val defaultStateBitmap = + getBitmap(context, defaultResId, scale) + val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale) + val pressedTwoDirectionsStateBitmap = + getBitmap(context, pressedTwoDirectionsResId, scale) + + val overlayDrawable = InputOverlayDrawableDpad( + res, + defaultStateBitmap, + pressedOneDirectionStateBitmap, + pressedTwoDirectionsStateBitmap, + ButtonType.DPAD_UP, + ButtonType.DPAD_DOWN, + ButtonType.DPAD_LEFT, + ButtonType.DPAD_RIGHT + ) + + // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableX = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f).toInt() + val drawableY = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f).toInt() + val width = overlayDrawable.width + val height = overlayDrawable.height + + // Now set the bounds for the InputOverlayDrawableDpad. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be. + overlayDrawable.setBounds( + drawableX - (width / 2), + drawableY - (height / 2), + drawableX + (width / 2), + drawableY + (height / 2) + ) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2)) + return overlayDrawable + } + + /** + * Initializes an [InputOverlayDrawableJoystick] + * + * @param context The current [Context] + * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds). + * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around). + * @param pressedResInner Resource ID for the pressed inner image of the joystick. + * @param joystick Identifier for which joystick this is. + * @param button Identifier for which joystick button this is. + * @return the initialized [InputOverlayDrawableJoystick]. + */ + private fun initializeOverlayJoystick( + context: Context, + resOuter: Int, + defaultResInner: Int, + pressedResInner: Int, + joystick: Int, + button: Int, + orientation: String + ): InputOverlayDrawableJoystick { + // Resources handle for fetching the initial Drawable resource. + val res = context.resources + + // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick. + val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + // Decide scale based on user preference + var scale = 0.40f + scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat() + scale /= 100f + + // Initialize the InputOverlayDrawableJoystick. + val bitmapOuter = getBitmap(context, resOuter, scale) + val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f) + val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f) + + // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. + // These were set in the input overlay configuration menu. + val drawableX = sPrefs.getFloat("$button$orientation-X", 0f).toInt() + val drawableY = sPrefs.getFloat("$button$orientation-Y", 0f).toInt() + val outerScale = 1.66f + + // Now set the bounds for the InputOverlayDrawableJoystick. + // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be. + val outerSize = bitmapOuter.width + val outerRect = Rect( + drawableX - (outerSize / 2), + drawableY - (outerSize / 2), + drawableX + (outerSize / 2), + drawableY + (outerSize / 2) + ) + val innerRect = + Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt()) + + // Send the drawableId to the joystick so it can be referenced when saving control position. + val overlayDrawable = InputOverlayDrawableJoystick( + res, + bitmapOuter, + bitmapInnerDefault, + bitmapInnerPressed, + outerRect, + innerRect, + joystick, + button + ) + + // Need to set the image's position + overlayDrawable.setPosition(drawableX, drawableY) + return overlayDrawable + } + } +}