Merge pull request #10639 from 8bitDream/pictureinpicture

android: Support for Picture in Picture / Portrait
This commit is contained in:
bunnei 2023-06-15 16:40:13 -07:00 committed by GitHub
commit 3c217a5574
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 649 additions and 159 deletions

2
.gitignore vendored
View file

@ -26,6 +26,8 @@ CMakeSettings.json
# OSX global filetypes
# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc)
.DS_Store
.DS_Store?
._*
.AppleDouble
.LSOverride
.Spotlight-V100

View file

@ -53,7 +53,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
<activity
android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
android:theme="@style/Theme.Yuzu.Main"
android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
android:exported="true">
<intent-filter>

View file

@ -282,6 +282,11 @@ object NativeLibrary {
*/
external fun isRunning(): Boolean
/**
* Returns true if emulation is paused.
*/
external fun isPaused(): Boolean
/**
* Returns the performance stats for the current game
*/

View file

@ -4,14 +4,23 @@
package org.yuzu.yuzu_emu.activities
import android.app.Activity
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.util.Rational
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
@ -27,6 +36,8 @@ import androidx.navigation.fragment.NavHostFragment
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false
private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY"
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onDestroy() {
@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume()
nfcReader.startScanning()
startMotionSensorListener()
buildPictureInPictureParams()
}
override fun onPause() {
@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
stopMotionSensorListener()
}
override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
@ -230,6 +256,96 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
}
}
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
3 -> Rational(16, 10)
else -> null // Best fit
}
return this.apply { aspectRatio?.let { setAspectRatio(it) } }
}
private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): PictureInPictureParams.Builder {
val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf()
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
if (NativeLibrary.isPaused()) {
val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
val playPendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_play,
Intent(actionPlay),
pendingFlags
)
val playRemoteAction = RemoteAction(
playIcon,
getString(R.string.play),
getString(R.string.play),
playPendingIntent
)
pictureInPictureActions.add(playRemoteAction)
} else {
val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
val pausePendingIntent = PendingIntent.getBroadcast(
this@EmulationActivity,
R.drawable.ic_pip_pause,
Intent(actionPause),
pendingFlags
)
val pauseRemoteAction = RemoteAction(
pauseIcon,
getString(R.string.pause),
getString(R.string.pause),
pausePendingIntent
)
pictureInPictureActions.add(pauseRemoteAction)
}
return this.apply { setActions(pictureInPictureActions) }
}
fun buildPictureInPictureParams() {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
}
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
}
private var pictureInPictureReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
if (intent.action == actionPlay) {
if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation()
} else if (intent.action == actionPause) {
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
}
buildPictureInPictureParams()
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
IntentFilter().apply {
addAction(actionPause)
addAction(actionPlay)
}.also {
registerReceiver(pictureInPictureReceiver, it)
}
} else {
try {
unregisterReceiver(pictureInPictureReceiver)
} catch (ignored : Exception) {
}
}
}
private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

View file

@ -8,6 +8,7 @@ enum class BooleanSetting(
override val section: String,
override val defaultValue: Boolean
) : AbstractBooleanSetting {
PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
override var boolean: Boolean = defaultValue
@ -27,6 +28,7 @@ enum class BooleanSetting(
companion object {
private val NOT_RUNTIME_EDITABLE = listOf(
PICTURE_IN_PICTURE,
USE_CUSTOM_RTC
)

View file

@ -93,6 +93,11 @@ enum class IntSetting(
Settings.SECTION_RENDERER,
0
),
RENDERER_SCREEN_LAYOUT(
"screen_layout",
Settings.SECTION_RENDERER,
Settings.LayoutOption_MobileLandscape
),
RENDERER_ASPECT_RATIO(
"aspect_ratio",
Settings.SECTION_RENDERER,

View file

@ -133,7 +133,6 @@ class Settings {
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
@ -144,6 +143,10 @@ class Settings {
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5
init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf(

View file

@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.updatePadding
import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.NativeLibrary
@ -239,5 +240,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings)
}
fun launch(
context: Context,
launcher: ActivityResultLauncher<Intent>,
menuTag: String?,
gameId: String?
) {
val settings = Intent(context, SettingsActivity::class.java)
settings.putExtra(ARG_MENU_TAG, menuTag)
settings.putExtra(ARG_GAME_ID, gameId)
launcher.launch(settings)
}
}
}

View file

@ -166,6 +166,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.CPU_ACCURACY.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.PICTURE_IN_PICTURE,
R.string.picture_in_picture,
R.string.picture_in_picture_description,
BooleanSetting.PICTURE_IN_PICTURE.key,
BooleanSetting.PICTURE_IN_PICTURE.defaultValue
)
)
}
}
@ -283,6 +292,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.RENDERER_ANTI_ALIASING.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
R.string.renderer_screen_layout,
0,
R.array.rendererScreenLayoutNames,
R.array.rendererScreenLayoutValues,
IntSetting.RENDERER_SCREEN_LAYOUT.key,
IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO,

View file

@ -7,24 +7,26 @@ import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.content.res.Resources
import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Rational
import android.util.TypedValue
import android.view.*
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
@ -48,6 +50,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.utils.*
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@ -61,11 +64,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val args by navArgs<EmulationFragmentArgs>()
private var isInFoldableLayout = false
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is EmulationActivity) {
emulationActivity = context
NativeLibrary.setEmulationActivity(context)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(context)
.windowLayoutInfo(context)
.collect { updateFoldableLayout(context, it) }
}
}
onReturnFromSettings = context.activityResultRegistry.register(
"SettingsResult", ActivityResultContracts.StartActivityForResult()
) {
binding.surfaceEmulation.setAspectRatio(
when (IntSetting.RENDERER_ASPECT_RATIO.int) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
3 -> Rational(16, 10)
4 -> null // Stretch
else -> Rational(16, 9)
}
)
emulationActivity?.buildPictureInPictureParams()
updateScreenLayout()
}
} else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
}
@ -129,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
R.id.menu_settings -> {
SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
SettingsActivity.launch(
requireContext(),
onReturnFromSettings,
SettingsFile.FILE_NAME_CONFIG,
""
)
true
}
@ -162,7 +199,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(requireContext())
.windowLayoutInfo(requireActivity())
.collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) }
.collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (emulationActivity?.isInPictureInPictureMode == true) {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
}
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
}
} else {
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
}
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
} else {
binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
}
}
if (!binding.surfaceInputOverlay.isInEditMode) {
refreshInputOverlay()
}
}
}
@ -184,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
)
updateScreenLayout()
emulationState.run(emulationActivity!!.isActivityRecreated)
}
@ -243,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
@SuppressLint("SourceLockedOrientationActivity")
private fun updateScreenLayout() {
emulationActivity?.let {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
onConfigurationChanged(resources.configuration)
}
fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
if (it.isSeparating) {
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
binding.surfaceEmulation.layoutParams.height = it.bounds.top
// Restrict emulation and overlays to the top of the screen
binding.emulationContainer.layoutParams.height = it.bounds.top
binding.overlayContainer.layoutParams.height = it.bounds.top
// Restrict input and menu drawer to the bottom of the screen
binding.inputContainer.layoutParams.height = it.bounds.bottom
binding.inGameMenu.layoutParams.height = it.bounds.bottom
binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
isInFoldableLayout = true
binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
refreshInputOverlay()
}
}
it.isSeparating
} ?: false
if (!isFolding) {
binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.updatePadding(0, 0, 0, 0)
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
isInFoldableLayout = false
updateScreenLayout()
}
binding.surfaceInputOverlay.requestLayout()
binding.inGameMenu.requestLayout()
binding.emulationContainer.requestLayout()
binding.inputContainer.requestLayout()
binding.overlayContainer.requestLayout()
binding.inGameMenu.requestLayout()
}
override fun surfaceCreated(holder: SurfaceHolder) {
@ -397,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
popup.show()
}
@SuppressLint("SourceLockedOrientationActivity")
private fun startConfiguringControls() {
// Lock the current orientation to prevent editing inconsistencies
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
}
}
binding.doneControlConfig.visibility = View.VISIBLE
binding.surfaceInputOverlay.setIsInEditMode(true)
}
@ -405,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private fun stopConfiguringControls() {
binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
}
@SuppressLint("SetTextI18n")

View file

@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
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, false)) {
if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlay()
}
@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val fingerPositionX = event.getX(pointerIndex).toInt()
val fingerPositionY = event.getY(pointerIndex).toInt()
// TODO: Provide support for portrait layout
//val orientation =
// if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
for (button in overlayButtons) {
// Determine the button state to apply based on the MotionEvent action flag.
when (event.action and MotionEvent.ACTION_MASK) {
@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
buttonBeingConfigured!!.buttonId,
buttonBeingConfigured!!.bounds.centerX(),
buttonBeingConfigured!!.bounds.centerY(),
""
orientation
)
buttonBeingConfigured = null
}
@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
dpadBeingConfigured!!.upId,
dpadBeingConfigured!!.bounds.centerX(),
dpadBeingConfigured!!.bounds.centerY(),
""
orientation
)
dpadBeingConfigured = null
}
@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
joystickBeingConfigured!!.buttonId,
joystickBeingConfigured!!.bounds.centerX(),
joystickBeingConfigured!!.bounds.centerY(),
""
orientation
)
joystickBeingConfigured = null
}
@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
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) {
@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val min = windowSize.first
val max = windowSize.second
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
.putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x)
.putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y)
.putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
.putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
.apply()
}
@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
}
private fun defaultOverlay() {
if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
defaultOverlayLandscape()
if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
defaultOverlayByLayout(orientation)
}
resetButtonPlacement()
preferences.edit()
.putBoolean(Settings.PREF_OVERLAY_INIT, true)
.putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
.apply()
}
fun resetButtonPlacement() {
defaultOverlayLandscape()
defaultOverlayByLayout(orientation)
refreshControls()
}
private fun defaultOverlayLandscape() {
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(orientation: String, position: Int) : Float {
return when (orientation) {
PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
}
}
private fun defaultOverlayByLayout(orientation: String) {
// Each value represents the position of the button in relation to the screen size without insets.
preferences.edit()
.putFloat(
ButtonType.BUTTON_A.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000
ButtonType.BUTTON_A.toString() + "-X$orientation",
getResourceValue(orientation, 0)
)
.putFloat(
ButtonType.BUTTON_A.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000
ButtonType.BUTTON_A.toString() + "-Y$orientation",
getResourceValue(orientation, 1)
)
.putFloat(
ButtonType.BUTTON_B.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000
ButtonType.BUTTON_B.toString() + "-X$orientation",
getResourceValue(orientation, 2)
)
.putFloat(
ButtonType.BUTTON_B.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000
ButtonType.BUTTON_B.toString() + "-Y$orientation",
getResourceValue(orientation, 3)
)
.putFloat(
ButtonType.BUTTON_X.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000
ButtonType.BUTTON_X.toString() + "-X$orientation",
getResourceValue(orientation, 4)
)
.putFloat(
ButtonType.BUTTON_X.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000
ButtonType.BUTTON_X.toString() + "-Y$orientation",
getResourceValue(orientation, 5)
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000
ButtonType.BUTTON_Y.toString() + "-X$orientation",
getResourceValue(orientation, 6)
)
.putFloat(
ButtonType.BUTTON_Y.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000
ButtonType.BUTTON_Y.toString() + "-Y$orientation",
getResourceValue(orientation, 7)
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000
ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
getResourceValue(orientation, 8)
)
.putFloat(
ButtonType.TRIGGER_ZL.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000
ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
getResourceValue(orientation, 9)
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000
ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
getResourceValue(orientation, 10)
)
.putFloat(
ButtonType.TRIGGER_ZR.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000
ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
getResourceValue(orientation, 11)
)
.putFloat(
ButtonType.DPAD_UP.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000
ButtonType.DPAD_UP.toString() + "-X$orientation",
getResourceValue(orientation, 12)
)
.putFloat(
ButtonType.DPAD_UP.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000
ButtonType.DPAD_UP.toString() + "-Y$orientation",
getResourceValue(orientation, 13)
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000
ButtonType.TRIGGER_L.toString() + "-X$orientation",
getResourceValue(orientation, 14)
)
.putFloat(
ButtonType.TRIGGER_L.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000
ButtonType.TRIGGER_L.toString() + "-Y$orientation",
getResourceValue(orientation, 15)
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "-X",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000
ButtonType.TRIGGER_R.toString() + "-X$orientation",
getResourceValue(orientation, 16)
)
.putFloat(
ButtonType.TRIGGER_R.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000
ButtonType.TRIGGER_R.toString() + "-Y$orientation",
getResourceValue(orientation, 17)
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000
ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
getResourceValue(orientation, 18)
)
.putFloat(
ButtonType.BUTTON_PLUS.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000
ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
getResourceValue(orientation, 19)
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000
ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
getResourceValue(orientation, 20)
)
.putFloat(
ButtonType.BUTTON_MINUS.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000
ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
getResourceValue(orientation, 21)
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000
ButtonType.BUTTON_HOME.toString() + "-X$orientation",
getResourceValue(orientation, 22)
)
.putFloat(
ButtonType.BUTTON_HOME.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000
ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
getResourceValue(orientation, 23)
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-X",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X)
.toFloat() / 1000
ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
getResourceValue(orientation, 24)
)
.putFloat(
ButtonType.BUTTON_CAPTURE.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y)
.toFloat() / 1000
ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
getResourceValue(orientation, 25)
)
.putFloat(
ButtonType.STICK_R.toString() + "-X",
resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000
ButtonType.STICK_R.toString() + "-X$orientation",
getResourceValue(orientation, 26)
)
.putFloat(
ButtonType.STICK_R.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000
ButtonType.STICK_R.toString() + "-Y$orientation",
getResourceValue(orientation, 27)
)
.putFloat(
ButtonType.STICK_L.toString() + "-X",
resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000
ButtonType.STICK_L.toString() + "-X$orientation",
getResourceValue(orientation, 28)
)
.putFloat(
ButtonType.STICK_L.toString() + "-Y",
resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000
ButtonType.STICK_L.toString() + "-Y$orientation",
getResourceValue(orientation, 29)
)
.apply()
}
@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
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
*
@ -754,8 +859,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/
private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size
val windowMetrics =
WindowMetricsCalculator.getOrCreate()
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat()
@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
if (insets != null) {
if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
insets.boundingRectTop.bottom.toFloat() else maxY
maxY = insets.boundingRectTop.bottom.toFloat()
if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
insets.boundingRectRight.left.toFloat() else maxX
maxX = insets.boundingRectRight.left.toFloat()
minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// 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 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()
@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// 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}$orientation-X", 0f)
val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f)
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
@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
// 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$orientation-X", 0f)
val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f)
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

View file

@ -11,14 +11,6 @@ object EmulationMenuSettings {
private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
// These must match what is defined in src/core/settings.h
const val LayoutOption_Default = 0
const val LayoutOption_SingleScreen = 1
const val LayoutOption_LargeScreen = 2
const val LayoutOption_SideScreen = 3
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5
var joystickRelCenter: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
set(value) {
@ -41,16 +33,6 @@ object EmulationMenuSettings {
.apply()
}
var landscapeScreenLayout: Int
get() = preferences.getInt(
Settings.PREF_MENU_SETTINGS_LANDSCAPE,
LayoutOption_MobileLandscape
)
set(value) {
preferences.edit()
.putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
.apply()
}
var showFps: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
set(value) {

View file

@ -202,6 +202,11 @@ public:
return m_is_running;
}
bool IsPaused() const {
std::scoped_lock lock(m_mutex);
return m_is_running && m_is_paused;
}
const Core::PerfStatsResults& PerfStats() const {
std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
return m_perf_stats;
@ -287,11 +292,13 @@ public:
void PauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Pause();
m_is_paused = true;
}
void UnPauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Run();
m_is_paused = false;
}
void HaltEmulation() {
@ -473,6 +480,7 @@ private:
std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
bool m_is_running{};
bool m_is_paused{};
SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv
return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
[[maybe_unused]] jclass clazz) {
return EmulationSession::GetInstance().IsHandheldOnly();

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z" />
</vector>

View file

@ -12,6 +12,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/emulation_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- This is what everything is rendered to during emulation -->
<org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
android:id="@+id/surface_emulation"
@ -21,8 +26,10 @@
android:focusable="false"
android:focusableInTouchMode="false" />
</FrameLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:id="@+id/input_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom">
@ -32,9 +39,26 @@
android:id="@+id/surface_input_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:focusable="true"
android:focusableInTouchMode="true" />
<Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/done_control_config"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/emulation_done"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/show_fps_text"
android:layout_width="wrap_content"
@ -47,14 +71,6 @@
android:textSize="12sp"
tools:ignore="RtlHardcoded" />
<Button
style="@style/Widget.Material3.Button.ElevatedButton"
android:id="@+id/done_control_config"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/emulation_done"
android:visibility="gone" />
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -119,6 +119,18 @@
<item>3</item>
</integer-array>
<string-array name="rendererScreenLayoutNames">
<item>@string/screen_layout_landscape</item>
<item>@string/screen_layout_portrait</item>
<item>@string/screen_layout_auto</item>
</string-array>
<integer-array name="rendererScreenLayoutValues">
<item>5</item>
<item>4</item>
<item>0</item>
</integer-array>
<string-array name="rendererAspectRatioNames">
<item>@string/ratio_default</item>
<item>@string/ratio_force_four_three</item>

View file

@ -34,4 +34,68 @@
<integer name="SWITCH_BUTTON_DPAD_X">260</integer>
<integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
<!-- Default SWITCH portrait layout -->
<integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
<integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer>
<integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer>
<integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer>
<integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer>
<integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer>
<integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer>
<integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer>
<integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer>
<integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer>
<integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer>
<integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer>
<integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer>
<integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer>
<integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer>
<integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer>
<integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer>
<integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer>
<integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer>
<integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer>
<integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer>
<integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer>
<integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer>
<integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer>
<integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
<integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
<!-- Default SWITCH foldable layout -->
<integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
<integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer>
<integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer>
<integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer>
<integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer>
<integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer>
<integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer>
<integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer>
<integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer>
<integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer>
<integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer>
<integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer>
<integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer>
<integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer>
<integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer>
<integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer>
<integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer>
<integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer>
<integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer>
<integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer>
<integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer>
<integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer>
<integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer>
<integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer>
<integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
<integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
<integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
</resources>

View file

@ -162,6 +162,7 @@
<string name="renderer_accuracy">Accuracy level</string>
<string name="renderer_resolution">Resolution (Handheld/Docked)</string>
<string name="renderer_vsync">VSync mode</string>
<string name="renderer_screen_layout">Orientation</string>
<string name="renderer_aspect_ratio">Aspect ratio</string>
<string name="renderer_scaling_filter">Window adapting filter</string>
<string name="renderer_anti_aliasing">Anti-aliasing method</string>
@ -326,6 +327,11 @@
<string name="anti_aliasing_fxaa">FXAA</string>
<string name="anti_aliasing_smaa">SMAA</string>
<!-- Screen Layouts -->
<string name="screen_layout_landscape">Landscape</string>
<string name="screen_layout_portrait">Portrait</string>
<string name="screen_layout_auto">Auto</string>
<!-- Aspect Ratios -->
<string name="ratio_default">Default (16:9)</string>
<string name="ratio_force_four_three">Force 4:3</string>
@ -364,6 +370,12 @@
<string name="use_black_backgrounds">Black backgrounds</string>
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
<!-- Picture-In-Picture -->
<string name="picture_in_picture">Picture in Picture</string>
<string name="picture_in_picture_description">Minimize window when placed in the background</string>
<string name="pause">Pause</string>
<string name="play">Play</string>
<!-- Licenses screen strings -->
<string name="licenses">Licenses</string>
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>