2
1
Fork 0
mirror of https://github.com/yuzu-emu/yuzu.git synced 2024-07-04 23:31:19 +01:00

android: Add Picture in Picture / Orientation

This commit is contained in:
Abandoned Cart 2023-06-10 22:42:54 -04:00
parent a10a091928
commit de9100ea81
15 changed files with 336 additions and 66 deletions

View file

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

View file

@ -4,14 +4,23 @@
package org.yuzu.yuzu_emu.activities package org.yuzu.yuzu_emu.activities
import android.app.Activity 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.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Icon
import android.hardware.Sensor import android.hardware.Sensor
import android.hardware.SensorEvent import android.hardware.SensorEvent
import android.hardware.SensorEventListener import android.hardware.SensorEventListener
import android.hardware.SensorManager import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Rational
import android.view.InputDevice import android.view.InputDevice
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent import android.view.MotionEvent
@ -27,6 +36,8 @@ import androidx.navigation.fragment.NavHostFragment
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding 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.features.settings.model.SettingsViewModel
import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.ControllerMappingHelper import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private var motionTimestamp: Long = 0 private var motionTimestamp: Long = 0
private var flipMotionOrientation: Boolean = false private var flipMotionOrientation: Boolean = false
private val actionPause = "ACTION_EMULATOR_PAUSE"
private val actionPlay = "ACTION_EMULATOR_PLAY"
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
override fun onDestroy() { override fun onDestroy() {
@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
super.onResume() super.onResume()
nfcReader.startScanning() nfcReader.startScanning()
startMotionSensorListener() startMotionSensorListener()
buildPictureInPictureParams()
} }
override fun onPause() { override fun onPause() {
@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
stopMotionSensorListener() 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) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
setIntent(intent) setIntent(intent)
@ -230,6 +256,79 @@ 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
}
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
val isEmulationPaused = emulationFragment?.isEmulationStatePaused() ?: false
if (isEmulationPaused) {
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) {
emulationFragment?.onPictureInPicturePlay()
} else if (intent.action == actionPause) {
emulationFragment?.onPictureInPicturePause()
}
buildPictureInPictureParams()
}
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) {
IntentFilter().apply {
addAction(actionPause)
addAction(actionPlay)
}.also {
registerReceiver(pictureInPictureReceiver, it)
}
emulationFragment?.onPictureInPictureEnter()
} else {
try {
unregisterReceiver(pictureInPictureReceiver)
} catch (ignored : Exception) { }
emulationFragment?.onPictureInPictureLeave()
}
}
private fun startMotionSensorListener() { private fun startMotionSensorListener() {
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

View file

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

View file

@ -93,6 +93,11 @@ enum class IntSetting(
Settings.SECTION_RENDERER, Settings.SECTION_RENDERER,
0 0
), ),
RENDERER_SCREEN_LAYOUT(
"screen_layout",
Settings.SECTION_RENDERER,
Settings.LayoutOption_MobileLandscape
),
RENDERER_ASPECT_RATIO( RENDERER_ASPECT_RATIO(
"aspect_ratio", "aspect_ratio",
Settings.SECTION_RENDERER, 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_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" 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_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
@ -144,6 +143,14 @@ class Settings {
private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
// 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
init { init {
configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
listOf( listOf(

View file

@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.NativeLibrary
@ -239,5 +240,12 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
settings.putExtra(ARG_GAME_ID, gameId) settings.putExtra(ARG_GAME_ID, gameId)
context.startActivity(settings) 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 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 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( add(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.RENDERER_ASPECT_RATIO, IntSetting.RENDERER_ASPECT_RATIO,

View file

@ -7,6 +7,7 @@ import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Resources import android.content.res.Resources
@ -19,11 +20,14 @@ import android.util.TypedValue
import android.view.* import android.view.*
import android.widget.TextView import android.widget.TextView
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -61,11 +65,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
val args by navArgs<EmulationFragmentArgs>() val args by navArgs<EmulationFragmentArgs>()
private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
if (context is EmulationActivity) { if (context is EmulationActivity) {
emulationActivity = context emulationActivity = context
NativeLibrary.setEmulationActivity(context) NativeLibrary.setEmulationActivity(context)
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 { } else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent") throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
} }
@ -129,7 +152,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_settings -> { R.id.menu_settings -> {
SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") SettingsActivity.launch(
requireContext(), onReturnFromSettings, SettingsFile.FILE_NAME_CONFIG, ""
)
true true
} }
@ -162,7 +187,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(requireContext()) WindowInfoTracker.getOrCreate(requireContext())
.windowLayoutInfo(requireActivity()) .windowLayoutInfo(requireActivity())
.collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
} }
} }
} }
@ -204,6 +229,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDetach() super.onDetach()
} }
fun isEmulationStatePaused() : Boolean {
return this::emulationState.isInitialized && emulationState.isPaused
}
fun onPictureInPictureEnter() {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
}
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
}
}
fun onPictureInPicturePause() {
if (!emulationState.isPaused) {
emulationState.pause()
}
}
fun onPictureInPicturePlay() {
if (emulationState.isPaused) {
emulationState.run(false)
}
}
fun onPictureInPictureLeave() {
if (EmulationMenuSettings.showOverlay) {
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
}
}
private fun refreshInputOverlay() { private fun refreshInputOverlay() {
binding.surfaceInputOverlay.refreshControls() binding.surfaceInputOverlay.refreshControls()
} }
@ -243,15 +299,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
@SuppressLint("SourceLockedOrientationActivity")
private fun updateScreenLayout() {
emulationActivity?.let {
when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
Settings.LayoutOption_MobileLandscape -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
}
Settings.LayoutOption_MobilePortrait -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
Settings.LayoutOption_Default -> {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
else -> { it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE }
}
}
}
private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
if (it.isSeparating) { if (it.isSeparating) {
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
binding.surfaceEmulation.layoutParams.height = it.bounds.top binding.emulationContainer.layoutParams.height = it.bounds.top
binding.inGameMenu.layoutParams.height = it.bounds.bottom // Prevent touch regions from being displayed in the hinge
binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
} }
@ -259,14 +333,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
it.isSeparating it.isSeparating
} ?: false } ?: false
if (!isFolding) { if (!isFolding) {
binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.overlayContainer.updatePadding(0, 0, 0, 0) binding.overlayContainer.updatePadding(0, 0, 0, 0)
emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE updateScreenLayout()
} }
binding.surfaceInputOverlay.requestLayout() binding.emulationContainer.requestLayout()
binding.inGameMenu.requestLayout()
binding.overlayContainer.requestLayout() binding.overlayContainer.requestLayout()
} }

View file

@ -3,6 +3,7 @@
package org.yuzu.yuzu_emu.overlay package org.yuzu.yuzu_emu.overlay
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
@ -15,12 +16,14 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.VectorDrawable import android.graphics.drawable.VectorDrawable
import android.os.Build import android.os.Build
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Rational
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.SurfaceView import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.window.layout.WindowMetricsCalculator import androidx.window.layout.WindowMetricsCalculator
@ -33,6 +36,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
/** /**
* Draws the interactive input overlay on top of the * Draws the interactive input overlay on top of the
@ -73,6 +77,25 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
requestFocus() requestFocus()
} }
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
if (height > width) {
val aspectRatio = with (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) {
val metrics = maximumWindowMetrics.bounds
Rational(metrics.height(), metrics.width()).toFloat()
}
val newWidth: Int = width
val newHeight: Int = (width / aspectRatio).roundToInt()
setMeasuredDimension(newWidth, newHeight)
invalidate()
} else {
setMeasuredDimension(width, height)
}
}
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
super.draw(canvas) super.draw(canvas)
for (button in overlayButtons) { for (button in overlayButtons) {
@ -754,9 +777,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
*/ */
private fun getSafeScreenSize(context: Context): Pair<Point, Point> { private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
// Get screen size // Get screen size
val windowMetrics = val windowMetrics = WindowMetricsCalculator.getOrCreate()
WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(context as Activity)
.computeCurrentWindowMetrics(context as Activity)
var maxY = windowMetrics.bounds.height().toFloat() var maxY = windowMetrics.bounds.height().toFloat()
var maxX = windowMetrics.bounds.width().toFloat() var maxX = windowMetrics.bounds.width().toFloat()
var minY = 0 var minY = 0

View file

@ -11,14 +11,6 @@ object EmulationMenuSettings {
private val preferences = private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 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 var joystickRelCenter: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
set(value) { set(value) {
@ -41,16 +33,6 @@ object EmulationMenuSettings {
.apply() .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 var showFps: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
set(value) { set(value) {

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,14 +12,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<!-- This is what everything is rendered to during emulation --> <FrameLayout
<org.yuzu.yuzu_emu.views.FixedRatioSurfaceView android:id="@+id/emulation_container"
android:id="@+id/surface_emulation"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:layout_gravity="center"
android:focusable="false" <!-- This is what everything is rendered to during emulation -->
android:focusableInTouchMode="false" /> <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
android:id="@+id/surface_emulation"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:focusable="false"
android:focusableInTouchMode="false" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/overlay_container" android:id="@+id/overlay_container"
@ -27,34 +34,36 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="bottom"> android:layout_gravity="bottom">
<!-- This is the onscreen input overlay --> <!-- This is the onscreen input overlay -->
<org.yuzu.yuzu_emu.overlay.InputOverlay <org.yuzu.yuzu_emu.overlay.InputOverlay
android:id="@+id/surface_input_overlay" android:id="@+id/surface_input_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:focusable="true" android:layout_gravity="bottom"
android:focusableInTouchMode="true" /> android:focusable="true"
android:focusableInTouchMode="true" />
<TextView <TextView
android:id="@+id/show_fps_text" android:id="@+id/show_fps_text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="left" android:layout_gravity="left"
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:shadowColor="@android:color/black" android:shadowColor="@android:color/black"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:textSize="12sp" android:textSize="12sp"
tools:ignore="RtlHardcoded" /> 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" />
<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>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -63,7 +72,7 @@
android:id="@+id/in_game_menu" android:id="@+id/in_game_menu"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start|bottom" android:layout_gravity="start"
app:headerLayout="@layout/header_in_game" app:headerLayout="@layout/header_in_game"
app:menu="@menu/menu_in_game" /> app:menu="@menu/menu_in_game" />

View file

@ -119,6 +119,18 @@
<item>3</item> <item>3</item>
</integer-array> </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"> <string-array name="rendererAspectRatioNames">
<item>@string/ratio_default</item> <item>@string/ratio_default</item>
<item>@string/ratio_force_four_three</item> <item>@string/ratio_force_four_three</item>

View file

@ -162,6 +162,7 @@
<string name="renderer_accuracy">Accuracy level</string> <string name="renderer_accuracy">Accuracy level</string>
<string name="renderer_resolution">Resolution (Handheld/Docked)</string> <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
<string name="renderer_vsync">VSync mode</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_aspect_ratio">Aspect ratio</string>
<string name="renderer_scaling_filter">Window adapting filter</string> <string name="renderer_scaling_filter">Window adapting filter</string>
<string name="renderer_anti_aliasing">Anti-aliasing method</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_fxaa">FXAA</string>
<string name="anti_aliasing_smaa">SMAA</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 --> <!-- Aspect Ratios -->
<string name="ratio_default">Default (16:9)</string> <string name="ratio_default">Default (16:9)</string>
<string name="ratio_force_four_three">Force 4:3</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">Black backgrounds</string>
<string name="use_black_backgrounds_description">When using the dark theme, apply 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 --> <!-- Licenses screen strings -->
<string name="licenses">Licenses</string> <string name="licenses">Licenses</string>
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>