android: Convert InputOverlay to Kotlin

merge-requests/60/head
Charles Lombardo 2023-03-08 17:12:47 +07:00 committed by bunnei
parent 096cdc57bb
commit a1c57de466
2 changed files with 886 additions and 656 deletions

@ -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<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
private final Set<InputOverlayDrawableJoystick> 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.
* <p>
* This works due to the way the X and Y coordinates are stored within
* the {@link SharedPreferences}.
* <p>
* 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.
* <p>
* This has a few benefits over the conventional way of storing the values
* (ie. within the yuzu ini file).
* <ul>
* <li>No native calls</li>
* <li>Keeps Android-only values inside the Android environment</li>
* </ul>
* <p>
* 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;
}
}

@ -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<InputOverlayDrawableButton> = HashSet()
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = 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
}
}
}