mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-11-04 00:49:02 -06:00 
			
		
		
		
	android: Add Picture in Picture / Orientation
This commit is contained in:
		@@ -54,6 +54,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
 | 
			
		||||
            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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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,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() {
 | 
			
		||||
        val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
 | 
			
		||||
        val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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,14 @@ class Settings {
 | 
			
		||||
 | 
			
		||||
        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 {
 | 
			
		||||
            configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
 | 
			
		||||
                listOf(
 | 
			
		||||
 
 | 
			
		||||
@@ -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,12 @@ 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)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ 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
 | 
			
		||||
@@ -19,11 +20,14 @@ 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.isVisible
 | 
			
		||||
import androidx.core.view.updatePadding
 | 
			
		||||
import androidx.fragment.app.Fragment
 | 
			
		||||
import androidx.lifecycle.Lifecycle
 | 
			
		||||
@@ -61,11 +65,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
 | 
			
		||||
 | 
			
		||||
    val args by navArgs<EmulationFragmentArgs>()
 | 
			
		||||
 | 
			
		||||
    private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
 | 
			
		||||
 | 
			
		||||
    override fun onAttach(context: Context) {
 | 
			
		||||
        super.onAttach(context)
 | 
			
		||||
        if (context is EmulationActivity) {
 | 
			
		||||
            emulationActivity = 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 {
 | 
			
		||||
            throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
 | 
			
		||||
        }
 | 
			
		||||
@@ -129,7 +152,9 @@ 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 +187,7 @@ 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) }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -204,6 +229,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
 | 
			
		||||
        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() {
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
    fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
 | 
			
		||||
    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
 | 
			
		||||
                    binding.inGameMenu.layoutParams.height = it.bounds.bottom
 | 
			
		||||
                    binding.emulationContainer.layoutParams.height = it.bounds.top
 | 
			
		||||
                    // Prevent touch regions from being displayed in the hinge
 | 
			
		||||
                    binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
 | 
			
		||||
                    binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
 | 
			
		||||
                }
 | 
			
		||||
@@ -259,14 +333,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
 | 
			
		||||
            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.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
 | 
			
		||||
            binding.overlayContainer.updatePadding(0, 0, 0, 0)
 | 
			
		||||
            emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
 | 
			
		||||
            updateScreenLayout()
 | 
			
		||||
        }
 | 
			
		||||
        binding.surfaceInputOverlay.requestLayout()
 | 
			
		||||
        binding.inGameMenu.requestLayout()
 | 
			
		||||
        binding.emulationContainer.requestLayout()
 | 
			
		||||
        binding.overlayContainer.requestLayout()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.overlay
 | 
			
		||||
 | 
			
		||||
import android.annotation.SuppressLint
 | 
			
		||||
import android.app.Activity
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
@@ -15,12 +16,14 @@ import android.graphics.drawable.Drawable
 | 
			
		||||
import android.graphics.drawable.VectorDrawable
 | 
			
		||||
import android.os.Build
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.util.Rational
 | 
			
		||||
import android.view.HapticFeedbackConstants
 | 
			
		||||
import android.view.MotionEvent
 | 
			
		||||
import android.view.SurfaceView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.View.OnTouchListener
 | 
			
		||||
import android.view.WindowInsets
 | 
			
		||||
import android.view.WindowManager
 | 
			
		||||
import androidx.core.content.ContextCompat
 | 
			
		||||
import androidx.preference.PreferenceManager
 | 
			
		||||
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 kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
import kotlin.math.roundToInt
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draws the interactive input overlay on top of the
 | 
			
		||||
@@ -73,6 +77,25 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
 | 
			
		||||
        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) {
 | 
			
		||||
        super.draw(canvas)
 | 
			
		||||
        for (button in overlayButtons) {
 | 
			
		||||
@@ -754,9 +777,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
 | 
			
		||||
         */
 | 
			
		||||
        private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
 | 
			
		||||
            // Get screen size
 | 
			
		||||
            val windowMetrics =
 | 
			
		||||
                WindowMetricsCalculator.getOrCreate()
 | 
			
		||||
                    .computeCurrentWindowMetrics(context as Activity)
 | 
			
		||||
            val windowMetrics = WindowMetricsCalculator.getOrCreate()
 | 
			
		||||
                .computeCurrentWindowMetrics(context as Activity)
 | 
			
		||||
            var maxY = windowMetrics.bounds.height().toFloat()
 | 
			
		||||
            var maxX = windowMetrics.bounds.width().toFloat()
 | 
			
		||||
            var minY = 0
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_pause.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_pause.xml
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_play.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_play.xml
									
									
									
									
									
										Normal 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>
 | 
			
		||||
@@ -12,14 +12,21 @@
 | 
			
		||||
        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"
 | 
			
		||||
        <FrameLayout
 | 
			
		||||
            android:id="@+id/emulation_container"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:layout_gravity="center"
 | 
			
		||||
            android:focusable="false"
 | 
			
		||||
            android:focusableInTouchMode="false" />
 | 
			
		||||
            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"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:layout_gravity="center"
 | 
			
		||||
                android:focusable="false"
 | 
			
		||||
                android:focusableInTouchMode="false" />
 | 
			
		||||
 | 
			
		||||
        </FrameLayout>
 | 
			
		||||
 | 
			
		||||
        <FrameLayout
 | 
			
		||||
            android:id="@+id/overlay_container"
 | 
			
		||||
@@ -27,34 +34,36 @@
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:layout_gravity="bottom">
 | 
			
		||||
 | 
			
		||||
        <!-- This is the onscreen input overlay -->
 | 
			
		||||
        <org.yuzu.yuzu_emu.overlay.InputOverlay
 | 
			
		||||
            android:id="@+id/surface_input_overlay"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="match_parent"
 | 
			
		||||
            android:focusable="true"
 | 
			
		||||
            android:focusableInTouchMode="true" />
 | 
			
		||||
            <!-- This is the onscreen input overlay -->
 | 
			
		||||
            <org.yuzu.yuzu_emu.overlay.InputOverlay
 | 
			
		||||
                android:id="@+id/surface_input_overlay"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:layout_gravity="bottom"
 | 
			
		||||
                android:focusable="true"
 | 
			
		||||
                android:focusableInTouchMode="true" />
 | 
			
		||||
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@+id/show_fps_text"
 | 
			
		||||
            android:layout_width="wrap_content"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_gravity="left"
 | 
			
		||||
            android:clickable="false"
 | 
			
		||||
            android:focusable="false"
 | 
			
		||||
            android:shadowColor="@android:color/black"
 | 
			
		||||
            android:textColor="@android:color/white"
 | 
			
		||||
            android:textSize="12sp"
 | 
			
		||||
            tools:ignore="RtlHardcoded" />
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/show_fps_text"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="left"
 | 
			
		||||
                android:clickable="false"
 | 
			
		||||
                android:focusable="false"
 | 
			
		||||
                android:shadowColor="@android:color/black"
 | 
			
		||||
                android:textColor="@android:color/white"
 | 
			
		||||
                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" />
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -63,7 +72,7 @@
 | 
			
		||||
        android:id="@+id/in_game_menu"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="match_parent"
 | 
			
		||||
        android:layout_gravity="start|bottom"
 | 
			
		||||
        android:layout_gravity="start"
 | 
			
		||||
        app:headerLayout="@layout/header_in_game"
 | 
			
		||||
        app:menu="@menu/menu_in_game" />
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user