yuzu/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt

1171 lines
46 KiB
Kotlin

// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
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.Canvas
import android.graphics.Point
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable
import android.os.Build
import android.util.AttributeSet
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View
import android.view.View.OnTouchListener
import android.view.WindowInsets
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.window.layout.WindowMetricsCalculator
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
import kotlin.math.max
import kotlin.math.min
/**
* Draws the interactive input overlay on top of the
* [SurfaceView] that is rendering emulation.
*/
class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs),
OnTouchListener {
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
private var inEditMode = false
private var buttonBeingConfigured: InputOverlayDrawableButton? = null
private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
private lateinit var windowInsets: WindowInsets
var orientation = LANDSCAPE
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
windowInsets = rootWindowInsets
if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlay()
}
// Load the controls.
refreshControls()
// Set the on touch listener.
setOnTouchListener(this)
// Force draw
setWillNotDraw(false)
// Request focus for the overlay so it has priority on presses.
requestFocus()
}
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
val playerIndex =
if (NativeLibrary.isHandheldOnly()) NativeLibrary.ConsoleDevice else NativeLibrary.Player1Device
for (button in overlayButtons) {
if (!button.updateStatus(event)) {
continue
}
NativeLibrary.onGamePadButtonEvent(
playerIndex,
button.buttonId,
button.status
)
playHaptics(event)
shouldUpdateView = true
}
for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) {
continue
}
NativeLibrary.onGamePadButtonEvent(
playerIndex,
dpad.upId,
dpad.upStatus
)
NativeLibrary.onGamePadButtonEvent(
playerIndex,
dpad.downId,
dpad.downStatus
)
NativeLibrary.onGamePadButtonEvent(
playerIndex,
dpad.leftId,
dpad.leftStatus
)
NativeLibrary.onGamePadButtonEvent(
playerIndex,
dpad.rightId,
dpad.rightStatus
)
playHaptics(event)
shouldUpdateView = true
}
for (joystick in overlayJoysticks) {
if (!joystick.updateStatus(event)) {
continue
}
val axisID = joystick.joystickId
NativeLibrary.onGamePadJoystickEvent(
playerIndex,
axisID,
joystick.xAxis,
joystick.realYAxis
)
NativeLibrary.onGamePadButtonEvent(
playerIndex,
joystick.buttonId,
joystick.buttonStatus
)
playHaptics(event)
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 playHaptics(event: MotionEvent) {
if (EmulationMenuSettings.hapticFeedback) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP ->
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
}
}
}
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 {
val pointerIndex = event.actionIndex
val fingerPositionX = event.getX(pointerIndex).toInt()
val fingerPositionY = event.getY(pointerIndex).toInt()
for (button in overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
// If no button is being moved now, remember the currently touched button to move.
if (buttonBeingConfigured == null &&
button.bounds.contains(
fingerPositionX,
fingerPositionY
)
) {
buttonBeingConfigured = button
buttonBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
buttonBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured === button) {
// Persist button position by saving new place.
saveControlPosition(
buttonBeingConfigured!!.buttonId,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
orientation
)
buttonBeingConfigured = null
}
}
}
for (dpad in overlayDpads) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
// If no button is being moved now, remember the currently touched button to move.
if (buttonBeingConfigured == null &&
dpad.bounds.contains(fingerPositionX, fingerPositionY)
) {
dpadBeingConfigured = dpad
dpadBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
dpadBeingConfigured!!.onConfigureTouch(event)
invalidate()
return true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured === dpad) {
// Persist button position by saving new place.
saveControlPosition(
dpadBeingConfigured!!.upId,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
orientation
)
dpadBeingConfigured = null
}
}
}
for (joystick in overlayJoysticks) {
when (event.action) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
joystick.bounds.contains(
fingerPositionX,
fingerPositionY
)
) {
joystickBeingConfigured = joystick
joystickBeingConfigured!!.onConfigureTouch(event)
}
MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
joystickBeingConfigured!!.onConfigureTouch(event)
invalidate()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
saveControlPosition(
joystickBeingConfigured!!.buttonId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
orientation
)
joystickBeingConfigured = null
}
}
}
return true
}
private fun addOverlayControls(orientation: String) {
val windowSize = getSafeScreenSize(context)
if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
overlayButtons.add(
initializeOverlayButton(
context,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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,
windowSize,
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()
// 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) {
val windowSize = getSafeScreenSize(context)
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
.putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
.putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
.apply()
}
fun setIsInEditMode(editMode: Boolean) {
inEditMode = editMode
}
private fun defaultOverlay() {
if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlayByLayout(orientation)
}
resetButtonPlacement()
preferences.edit()
.putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
.apply()
}
fun resetButtonPlacement() {
defaultOverlayByLayout(orientation)
refreshControls()
}
private val landscapeResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X,
R.integer.SWITCH_BUTTON_A_Y,
R.integer.SWITCH_BUTTON_B_X,
R.integer.SWITCH_BUTTON_B_Y,
R.integer.SWITCH_BUTTON_X_X,
R.integer.SWITCH_BUTTON_X_Y,
R.integer.SWITCH_BUTTON_Y_X,
R.integer.SWITCH_BUTTON_Y_Y,
R.integer.SWITCH_TRIGGER_ZL_X,
R.integer.SWITCH_TRIGGER_ZL_Y,
R.integer.SWITCH_TRIGGER_ZR_X,
R.integer.SWITCH_TRIGGER_ZR_Y,
R.integer.SWITCH_BUTTON_DPAD_X,
R.integer.SWITCH_BUTTON_DPAD_Y,
R.integer.SWITCH_TRIGGER_L_X,
R.integer.SWITCH_TRIGGER_L_Y,
R.integer.SWITCH_TRIGGER_R_X,
R.integer.SWITCH_TRIGGER_R_Y,
R.integer.SWITCH_BUTTON_PLUS_X,
R.integer.SWITCH_BUTTON_PLUS_Y,
R.integer.SWITCH_BUTTON_MINUS_X,
R.integer.SWITCH_BUTTON_MINUS_Y,
R.integer.SWITCH_BUTTON_HOME_X,
R.integer.SWITCH_BUTTON_HOME_Y,
R.integer.SWITCH_BUTTON_CAPTURE_X,
R.integer.SWITCH_BUTTON_CAPTURE_Y,
R.integer.SWITCH_STICK_R_X,
R.integer.SWITCH_STICK_R_Y,
R.integer.SWITCH_STICK_L_X,
R.integer.SWITCH_STICK_L_Y
)
private val portraitResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X_PORTRAIT,
R.integer.SWITCH_BUTTON_A_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_B_X_PORTRAIT,
R.integer.SWITCH_BUTTON_B_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_X_X_PORTRAIT,
R.integer.SWITCH_BUTTON_X_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_Y_X_PORTRAIT,
R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT,
R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_L_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT,
R.integer.SWITCH_TRIGGER_R_X_PORTRAIT,
R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT,
R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT,
R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT,
R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT,
R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT,
R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT,
R.integer.SWITCH_STICK_R_X_PORTRAIT,
R.integer.SWITCH_STICK_R_Y_PORTRAIT,
R.integer.SWITCH_STICK_L_X_PORTRAIT,
R.integer.SWITCH_STICK_L_Y_PORTRAIT
)
private val foldableResources = arrayOf(
R.integer.SWITCH_BUTTON_A_X_FOLDABLE,
R.integer.SWITCH_BUTTON_A_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_B_X_FOLDABLE,
R.integer.SWITCH_BUTTON_B_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_X_X_FOLDABLE,
R.integer.SWITCH_BUTTON_X_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_Y_X_FOLDABLE,
R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE,
R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_L_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE,
R.integer.SWITCH_TRIGGER_R_X_FOLDABLE,
R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE,
R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE,
R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE,
R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE,
R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE,
R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE,
R.integer.SWITCH_STICK_R_X_FOLDABLE,
R.integer.SWITCH_STICK_R_Y_FOLDABLE,
R.integer.SWITCH_STICK_L_X_FOLDABLE,
R.integer.SWITCH_STICK_L_Y_FOLDABLE
)
private fun getResourceValue(descriptor: String, position: Int) : Float {
return when (descriptor) {
PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
}
}
private fun defaultOverlayByLayout(descriptor: String) {
// Each value represents the position of the button in relation to the screen size without insets.
preferences.edit()
.putFloat(
ButtonType.BUTTON_A.toString() + "$descriptor-X",
getResourceValue(descriptor, 0)
)
.putFloat(
ButtonType.BUTTON_A.toString() + "$descriptor-Y",
getResourceValue(descriptor, 1)
)
.putFloat(
ButtonType.BUTTON_B.toString() + "$descriptor-X",
getResourceValue(descriptor, 2)
)
.putFloat(
ButtonType.BUTTON_B.toString() + "$descriptor-Y",
getResourceValue(descriptor, 3)
)
.putFloat(
ButtonType.BUTTON_X.toString() + "$descriptor-X",
getResourceValue(descriptor, 4)
)
.putFloat(
ButtonType.BUTTON_X.toString() + "$descriptor-Y",
getResourceValue(descriptor, 5)
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "$descriptor-X",
getResourceValue(descriptor, 6)
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "$descriptor-Y",
getResourceValue(descriptor, 7)
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "$descriptor-X",
getResourceValue(descriptor, 8)
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "$descriptor-Y",
getResourceValue(descriptor, 9)
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "$descriptor-X",
getResourceValue(descriptor, 10)
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "$descriptor-Y",
getResourceValue(descriptor, 11)
)
.putFloat(
ButtonType.DPAD_UP.toString() + "$descriptor-X",
getResourceValue(descriptor, 12)
)
.putFloat(
ButtonType.DPAD_UP.toString() + "$descriptor-Y",
getResourceValue(descriptor, 13)
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "$descriptor-X",
getResourceValue(descriptor, 14)
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "$descriptor-Y",
getResourceValue(descriptor, 15)
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "$descriptor-X",
getResourceValue(descriptor, 16)
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "$descriptor-Y",
getResourceValue(descriptor, 17)
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "$descriptor-X",
getResourceValue(descriptor, 18)
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "$descriptor-Y",
getResourceValue(descriptor, 19)
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "$descriptor-X",
getResourceValue(descriptor, 20)
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "$descriptor-Y",
getResourceValue(descriptor, 21)
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "$descriptor-X",
getResourceValue(descriptor, 22)
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "$descriptor-Y",
getResourceValue(descriptor, 23)
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "$descriptor-X",
getResourceValue(descriptor, 24)
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "$descriptor-Y",
getResourceValue(descriptor, 25)
)
.putFloat(
ButtonType.STICK_R.toString() + "$descriptor-X",
getResourceValue(descriptor, 26)
)
.putFloat(
ButtonType.STICK_R.toString() + "$descriptor-Y",
getResourceValue(descriptor, 27)
)
.putFloat(
ButtonType.STICK_L.toString() + "$descriptor-X",
getResourceValue(descriptor, 28)
)
.putFloat(
ButtonType.STICK_L.toString() + "$descriptor-Y",
getResourceValue(descriptor, 29)
)
.apply()
}
override fun isInEditMode(): Boolean {
return inEditMode
}
companion object {
private val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
const val LANDSCAPE = ""
const val PORTRAIT = "_Portrait"
const val FOLDABLE = "_Foldable"
/**
* Resizes a [Bitmap] by a given scale factor
*
* @param context Context for getting the vector drawable
* @param drawableId The ID of the drawable to scale.
* @param scale The scale factor for the bitmap.
* @return The scaled [Bitmap]
*/
private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
val bitmap = Bitmap.createBitmap(
(vectorDrawable.intrinsicWidth * scale).toInt(),
(vectorDrawable.intrinsicHeight * scale).toInt(),
Bitmap.Config.ARGB_8888
)
val dm = context.resources.displayMetrics
val minScreenDimension = min(dm.widthPixels, dm.heightPixels)
val maxBitmapDimension = max(bitmap.width, bitmap.height)
val bitmapScale = scale * minScreenDimension / maxBitmapDimension
val scaledBitmap = Bitmap.createScaledBitmap(
bitmap,
(bitmap.width * bitmapScale).toInt(),
(bitmap.height * bitmapScale).toInt(),
true
)
val canvas = Canvas(scaledBitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
vectorDrawable.draw(canvas)
return scaledBitmap
}
/**
* Gets the safe screen size for drawing the overlay
*
* @param context Context for getting the window metrics
* @return A pair of points, the first being the top left corner of the safe area,
* the second being the bottom right corner of the safe area
*/
private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat()
var minY = 0
var minX = 0
// If we have API access, calculate the safe area to draw the overlay
var cutoutLeft = 0
var cutoutBottom = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null) {
if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
maxY = insets.boundingRectTop.bottom.toFloat()
if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
maxX = insets.boundingRectRight.left.toFloat()
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
cutoutLeft = insets.boundingRectRight.right - insets.boundingRectRight.left
cutoutBottom = insets.boundingRectTop.top - insets.boundingRectTop.bottom
}
}
// This makes sure that if we have an inset on one side of the screen, we mirror it on
// the other side. Since removing space from one of the max values messes with the scale,
// we also have to account for it using our min values.
if (maxX.toInt() != windowMetrics.bounds.width()) minX += cutoutLeft
if (maxY.toInt() != windowMetrics.bounds.height()) minY += cutoutBottom
if (minX > 0 && maxX.toInt() == windowMetrics.bounds.width()) {
maxX -= (minX * 2)
} else if (minX > 0) {
maxX -= minX
}
if (minY > 0 && maxY.toInt() == windowMetrics.bounds.height()) {
maxY -= (minY * 2)
} else if (minY > 0) {
maxY -= minY
}
return Pair(Point(minX, minY), Point(maxX.toInt(), maxY.toInt()))
}
/**
* 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 windowSize The size of the window to draw the overlay on.
* @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,
windowSize: Pair<Point, Point>,
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.07f
ButtonType.TRIGGER_L,
ButtonType.TRIGGER_R,
ButtonType.TRIGGER_ZL,
ButtonType.TRIGGER_ZR -> 0.26f
else -> 0.11f
}
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)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val xKey = "$buttonId-X$orientation"
val yKey = "$buttonId-Y$orientation"
val drawableXPercent = sPrefs.getFloat(xKey, 0f)
val drawableYPercent = sPrefs.getFloat(yKey, 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).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)
)
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
/**
* Initializes an [InputOverlayDrawableDpad]
*
* @param context The current [Context].
* @param windowSize The size of the window to draw the overlay on.
* @param defaultResId The [Bitmap] resource ID of the default state.
* @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed state in one direction.
* @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed state in two directions.
* @return the initialized [InputOverlayDrawableDpad]
*/
private fun initializeOverlayDpad(
context: Context,
windowSize: Pair<Point, Point>,
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.25f
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
)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).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))
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
/**
* Initializes an [InputOverlayDrawableJoystick]
*
* @param context The current [Context]
* @param windowSize The size of the window to draw the overlay on.
* @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,
windowSize: Pair<Point, Point>,
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.3f
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)
// Get the minimum and maximum coordinates of the screen where the button can be placed.
val min = windowSize.first
val max = windowSize.second
// The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
// These were set in the input overlay configuration menu.
val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
val drawableX = (drawableXPercent * max.x + min.x).toInt()
val drawableY = (drawableYPercent * max.y + min.y).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)
val savedOpacity = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100)
overlayDrawable.setOpacity(savedOpacity * 255 / 100)
return overlayDrawable
}
}
}