From d3c3b697555e25235f39c142ae1e188dd5a354d6 Mon Sep 17 00:00:00 2001 From: Charles Lombardo Date: Tue, 21 Mar 2023 00:34:39 -0400 Subject: [PATCH] android: Inset input overlay based on system cutouts --- src/android/app/build.gradle | 1 + src/android/app/src/main/AndroidManifest.xml | 2 +- .../yuzu_emu/fragments/EmulationFragment.kt | 2 +- .../org/yuzu/yuzu_emu/overlay/InputOverlay.kt | 144 +++++++++++------- .../app/src/main/res/values/themes.xml | 2 + 5 files changed, 93 insertions(+), 58 deletions(-) diff --git a/src/android/app/build.gradle b/src/android/app/build.gradle index 7539c27f61..c62177e6d5 100644 --- a/src/android/app/build.gradle +++ b/src/android/app/build.gradle @@ -138,6 +138,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" implementation "io.coil-kt:coil:2.2.2" implementation 'androidx.core:core-splashscreen:1.0.0' + implementation 'androidx.window:window:1.0.0' // Allows FRP-style asynchronous operations in Android. implementation 'io.reactivex:rxandroid:1.2.1' diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c37559b474..349dd50501 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -54,7 +54,7 @@ android:resizeableActivity="false" android:theme="@style/Theme.Yuzu.Main" android:launchMode="singleTop" - android:screenOrientation="landscape"/> + android:screenOrientation="userLandscape" /> diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index c02d35f5d7..0889b6f7f2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -153,7 +153,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram preferences.edit() .putInt(Settings.PREF_CONTROL_SCALE, 50) .apply() - binding.surfaceInputOverlay.resetButtonPlacement() + binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.resetButtonPlacement() } } private fun updateShowFpsOverlay() { 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 index 8df9b7bada..acd4a1fe21 100644 --- 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 @@ -18,14 +18,16 @@ import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager +import android.os.Build 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 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 @@ -34,7 +36,6 @@ 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. @@ -51,7 +52,22 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context private val accel = FloatArray(3) private var motionTimestamp: Long = 0 - init { + private lateinit var windowInsets: WindowInsets + + 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) + + sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME) + sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME) + } + + 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, false)) { defaultOverlay() } @@ -72,18 +88,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context 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) { @@ -483,141 +487,169 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context 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 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.Q) { + val insets = windowInsets.displayCutout!! + maxY = + if (insets.boundingRectTop.bottom != 0) insets.boundingRectTop.bottom.toFloat() else maxY + maxX = + if (insets.boundingRectRight.left != 0) insets.boundingRectRight.left.toFloat() else maxX + 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 } - 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 + resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_A.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_B.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_B.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_X.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_X.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_Y.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_Y.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.TRIGGER_ZL.toString() + "-X", - res.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.TRIGGER_ZL.toString() + "-Y", - res.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.TRIGGER_ZR.toString() + "-X", - res.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.TRIGGER_ZR.toString() + "-Y", - res.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.DPAD_UP.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.DPAD_UP.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.TRIGGER_L.toString() + "-X", - res.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.TRIGGER_L.toString() + "-Y", - res.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.TRIGGER_R.toString() + "-X", - res.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.TRIGGER_R.toString() + "-Y", - res.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_PLUS.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_PLUS.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_MINUS.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_MINUS.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_HOME.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_HOME.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.BUTTON_CAPTURE.toString() + "-X", - res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) + .toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.BUTTON_CAPTURE.toString() + "-Y", - res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) + .toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.STICK_R.toString() + "-X", - res.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.STICK_R.toString() + "-Y", - res.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 * maxY + minY ) .putFloat( ButtonType.STICK_L.toString() + "-X", - res.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 * maxX + resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 * maxX + minX ) .putFloat( ButtonType.STICK_L.toString() + "-Y", - res.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 * maxY + resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 * maxY + minY ) .commit() // We want to commit right away, otherwise the overlay could load before this is saved. diff --git a/src/android/app/src/main/res/values/themes.xml b/src/android/app/src/main/res/values/themes.xml index b2ed1d4b97..dfad059b69 100644 --- a/src/android/app/src/main/res/values/themes.xml +++ b/src/android/app/src/main/res/values/themes.xml @@ -40,6 +40,8 @@ @android:color/transparent @style/YuzuSlider + + default