Просмотр исходного кода

android: Use modal navigation drawer as in game menu

Charles Lombardo 3 лет назад
Родитель
Сommit
273e81bb94

+ 13 - 114
src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt

@@ -8,15 +8,10 @@ import android.content.DialogInterface
 import android.content.Intent
 import android.graphics.Rect
 import android.os.Bundle
-import android.view.MotionEvent
 import android.view.View
 import android.view.WindowManager
-import androidx.activity.OnBackPressedCallback
-import androidx.annotation.IntDef
 import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
-import androidx.fragment.app.FragmentManager
 import androidx.preference.PreferenceManager
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.slider.Slider.OnChangeListener
@@ -25,8 +20,9 @@ import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.databinding.DialogSliderBinding
 import org.yuzu.yuzu_emu.features.settings.model.Settings
 import org.yuzu.yuzu_emu.fragments.EmulationFragment
-import org.yuzu.yuzu_emu.fragments.MenuFragment
+import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
 import org.yuzu.yuzu_emu.utils.ThemeHelper
 import kotlin.math.roundToInt
 
@@ -37,11 +33,11 @@ open class EmulationActivity : AppCompatActivity() {
     //private Intent foregroundService;
 
     var isActivityRecreated = false
-    private var selectedTitle: String? = null
-    private var path: String? = null
     private var menuVisible = false
     private var emulationFragment: EmulationFragment? = null
 
+    private lateinit var game: Game
+
     override fun onDestroy() {
         // TODO(bunnei): Disable notifications until we support app suspension.
         //stopService(foregroundService);
@@ -54,9 +50,7 @@ open class EmulationActivity : AppCompatActivity() {
         super.onCreate(savedInstanceState)
         if (savedInstanceState == null) {
             // Get params we were passed
-            val gameToEmulate = intent
-            path = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME)
-            selectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE)
+            game = intent.parcelable(EXTRA_SELECTED_GAME)!!
             isActivityRecreated = false
         } else {
             isActivityRecreated = true
@@ -73,34 +67,26 @@ open class EmulationActivity : AppCompatActivity() {
         emulationFragment =
             supportFragmentManager.findFragmentById(R.id.frame_emulation_fragment) as EmulationFragment?
         if (emulationFragment == null) {
-            emulationFragment = EmulationFragment.newInstance(path)
+            emulationFragment = EmulationFragment.newInstance(game)
             supportFragmentManager.beginTransaction()
                 .add(R.id.frame_emulation_fragment, emulationFragment!!)
                 .commit()
         }
-        title = selectedTitle
+        title = game.title
 
         // Start a foreground service to prevent the app from getting killed in the background
         // TODO(bunnei): Disable notifications until we support app suspension.
         //foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
         //startForegroundService(foregroundService);
-
-        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
-            override fun handleOnBackPressed() {
-                toggleMenu()
-            }
-        })
     }
 
     override fun onSaveInstanceState(outState: Bundle) {
-        outState.putString(EXTRA_SELECTED_GAME, path)
-        outState.putString(EXTRA_SELECTED_TITLE, selectedTitle)
+        outState.putParcelable(EXTRA_SELECTED_GAME, game)
         super.onSaveInstanceState(outState)
     }
 
     private fun restoreState(savedInstanceState: Bundle) {
-        path = savedInstanceState.getString(EXTRA_SELECTED_GAME)
-        selectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE)
+        game = savedInstanceState.parcelable(EXTRA_SELECTED_GAME)!!
 
         // If an alert prompt was in progress when state was restored, retry displaying it
         NativeLibrary.retryDisplayAlertPrompt()
@@ -110,6 +96,8 @@ open class EmulationActivity : AppCompatActivity() {
         window.attributes.layoutInDisplayCutoutMode =
             WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
 
+        window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
+
         // It would be nice to use IMMERSIVE_STICKY, but that doesn't show the toolbar.
         window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
@@ -119,15 +107,6 @@ open class EmulationActivity : AppCompatActivity() {
                 View.SYSTEM_UI_FLAG_IMMERSIVE
     }
 
-    fun handleMenuAction(action: Int) {
-        when (action) {
-            MENU_ACTION_EXIT -> {
-                emulationFragment!!.stopEmulation()
-                finish()
-            }
-        }
-    }
-
     private fun editControlsPlacement() {
         if (emulationFragment!!.isConfiguringControls) {
             emulationFragment!!.stopConfiguringControls()
@@ -176,94 +155,14 @@ open class EmulationActivity : AppCompatActivity() {
             .show()
     }
 
-    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
-        if (event.actionMasked == MotionEvent.ACTION_DOWN) {
-            var anyMenuClosed = false
-            var submenu = supportFragmentManager.findFragmentById(R.id.frame_submenu)
-            if (submenu != null && areCoordinatesOutside(submenu.view, event.x, event.y)) {
-                closeSubmenu()
-                submenu = null
-                anyMenuClosed = true
-            }
-            if (submenu == null) {
-                val menu = supportFragmentManager.findFragmentById(R.id.frame_menu)
-                if (menu != null && areCoordinatesOutside(menu.view, event.x, event.y)) {
-                    closeMenu()
-                    anyMenuClosed = true
-                }
-            }
-            if (anyMenuClosed) {
-                return true
-            }
-        }
-        return super.dispatchTouchEvent(event)
-    }
-
-    @Retention(AnnotationRetention.SOURCE)
-    @IntDef(
-        MENU_ACTION_EDIT_CONTROLS_PLACEMENT,
-        MENU_ACTION_TOGGLE_CONTROLS,
-        MENU_ACTION_ADJUST_SCALE,
-        MENU_ACTION_EXIT,
-        MENU_ACTION_SHOW_FPS,
-        MENU_ACTION_RESET_OVERLAY,
-        MENU_ACTION_SHOW_OVERLAY,
-        MENU_ACTION_OPEN_SETTINGS
-    )
-    annotation class MenuAction
-
-    private fun closeSubmenu(): Boolean {
-        return supportFragmentManager.popBackStackImmediate(
-            BACKSTACK_NAME_SUBMENU,
-            FragmentManager.POP_BACK_STACK_INCLUSIVE
-        )
-    }
-
-    private fun closeMenu(): Boolean {
-        menuVisible = false
-        return supportFragmentManager.popBackStackImmediate(
-            BACKSTACK_NAME_MENU,
-            FragmentManager.POP_BACK_STACK_INCLUSIVE
-        )
-    }
-
-    private fun toggleMenu() {
-        if (!closeMenu()) {
-            val fragment: Fragment = MenuFragment.newInstance()
-            supportFragmentManager.beginTransaction()
-                .setCustomAnimations(
-                    R.animator.menu_slide_in_from_start,
-                    R.animator.menu_slide_out_to_start,
-                    R.animator.menu_slide_in_from_start,
-                    R.animator.menu_slide_out_to_start
-                )
-                .add(R.id.frame_menu, fragment)
-                .addToBackStack(BACKSTACK_NAME_MENU)
-                .commit()
-            menuVisible = true
-        }
-    }
-
     companion object {
-        private const val BACKSTACK_NAME_MENU = "menu"
-        private const val BACKSTACK_NAME_SUBMENU = "submenu"
         const val EXTRA_SELECTED_GAME = "SelectedGame"
-        const val EXTRA_SELECTED_TITLE = "SelectedTitle"
-        const val MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0
-        const val MENU_ACTION_TOGGLE_CONTROLS = 1
-        const val MENU_ACTION_ADJUST_SCALE = 2
-        const val MENU_ACTION_EXIT = 3
-        const val MENU_ACTION_SHOW_FPS = 4
-        const val MENU_ACTION_RESET_OVERLAY = 6
-        const val MENU_ACTION_SHOW_OVERLAY = 7
-        const val MENU_ACTION_OPEN_SETTINGS = 8
         private const val EMULATION_RUNNING_NOTIFICATION = 0x1000
 
         @JvmStatic
-        fun launch(activity: FragmentActivity, path: String?, title: String?) {
+        fun launch(activity: FragmentActivity, game: Game) {
             val launcher = Intent(activity, EmulationActivity::class.java)
-            launcher.putExtra(EXTRA_SELECTED_GAME, path)
-            launcher.putExtra(EXTRA_SELECTED_TITLE, title)
+            launcher.putExtra(EXTRA_SELECTED_GAME, game)
             activity.startActivity(launcher)
         }
 

+ 2 - 3
src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt

@@ -13,7 +13,6 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.ImageView
 import androidx.appcompat.app.AppCompatActivity
-import androidx.fragment.app.FragmentActivity
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.RecyclerView
 import coil.load
@@ -23,8 +22,8 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch
 import org.yuzu.yuzu_emu.databinding.CardGameBinding
+import org.yuzu.yuzu_emu.activities.EmulationActivity
 import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.model.GameDatabase
 import org.yuzu.yuzu_emu.utils.Log
@@ -181,7 +180,7 @@ class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapte
      */
     override fun onClick(view: View) {
         val holder = view.tag as GameViewHolder
-        launch((view.context as FragmentActivity), holder.game.path, holder.game.title)
+        EmulationActivity.launch((view.context as AppCompatActivity), holder.game)
     }
 
     private fun isValidGame(path: String): Boolean {

+ 114 - 7
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

@@ -10,7 +10,14 @@ import android.graphics.Color
 import android.os.Bundle
 import android.os.Handler
 import android.view.*
+import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+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.fragment.app.Fragment
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import androidx.preference.PreferenceManager
@@ -20,10 +27,15 @@ import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.activities.EmulationActivity
 import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
 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.model.Game
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization.DirectoryInitializationState
 import org.yuzu.yuzu_emu.utils.DirectoryStateReceiver
+import org.yuzu.yuzu_emu.utils.InsetsHelper
 import org.yuzu.yuzu_emu.utils.Log
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
 
 class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
     private lateinit var preferences: SharedPreferences
@@ -35,6 +47,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
     private var _binding: FragmentEmulationBinding? = null
     private val binding get() = _binding!!
 
+    private lateinit var game: Game
+
     override fun onAttach(context: Context) {
         super.onAttach(context)
         if (context is EmulationActivity) {
@@ -54,8 +68,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
         // So this fragment doesn't restart on configuration changes; i.e. rotation.
         retainInstance = true
         preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
-        val gamePath = requireArguments().getString(KEY_GAMEPATH)
-        emulationState = EmulationState(gamePath)
+        game = requireArguments().parcelable(EmulationActivity.EXTRA_SELECTED_GAME)!!
+        emulationState = EmulationState(game.path)
     }
 
     /**
@@ -78,6 +92,57 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
         // Setup overlay.
         resetInputOverlay()
         updateShowFpsOverlay()
+
+        binding.inGameMenu.getHeaderView(0).findViewById<TextView>(R.id.text_game_title).text =
+            game.title
+        binding.inGameMenu.setNavigationItemSelectedListener {
+            when (it.itemId) {
+                R.id.menu_pause_emulation -> {
+                    if (emulationState.isPaused) {
+                        emulationState.run(false)
+                        it.title = resources.getString(R.string.emulation_pause)
+                        it.icon = ResourcesCompat.getDrawable(
+                            resources,
+                            R.drawable.ic_pause,
+                            requireContext().theme
+                        )
+                    } else {
+                        emulationState.pause()
+                        it.title = resources.getString(R.string.emulation_unpause)
+                        it.icon = ResourcesCompat.getDrawable(
+                            resources,
+                            R.drawable.ic_play,
+                            requireContext().theme
+                        )
+                    }
+                    true
+                }
+                R.id.menu_settings -> {
+                    SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "")
+                    true
+                }
+                R.id.menu_overlay_controls -> {
+                    showOverlayOptions()
+                    true
+                }
+                R.id.menu_exit -> {
+                    requireActivity().finish()
+                    emulationState.stop()
+                    true
+                }
+                else -> true
+            }
+        }
+
+        setInsets()
+
+        requireActivity().onBackPressedDispatcher.addCallback(
+            requireActivity(),
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    if (binding.drawerLayout.isOpen) binding.drawerLayout.close() else binding.drawerLayout.open()
+                }
+            })
     }
 
     override fun onResume() {
@@ -202,8 +267,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
         NativeLibrary.DoFrame()
     }
 
-    fun stopEmulation() {
-        emulationState.stop()
+    private fun showOverlayOptions() {
+        val anchor = binding.inGameMenu.findViewById<View>(R.id.menu_overlay_controls)
+        val popup = PopupMenu(requireContext(), anchor)
+
+        popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
+
+        popup.setOnMenuItemClickListener {
+            when (it.itemId) {
+                R.id.menu_edit_overlay -> {
+                    binding.drawerLayout.close()
+                    binding.surfaceInputOverlay.requestFocus()
+                    startConfiguringControls()
+                    true
+                }
+                R.id.menu_reset_overlay -> {
+                    binding.drawerLayout.close()
+                    resetInputOverlay()
+                    true
+                }
+                else -> true
+            }
+        }
+
+        popup.show()
     }
 
     fun startConfiguringControls() {
@@ -219,6 +306,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
     val isConfiguringControls: Boolean
         get() = binding.surfaceInputOverlay.isInEditMode
 
+    private fun setInsets() {
+        ViewCompat.setOnApplyWindowInsetsListener(binding.inGameMenu) { v: View, windowInsets: WindowInsetsCompat ->
+            val cutInsets: Insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+            var left = 0
+            var right = 0
+            if (ViewCompat.getLayoutDirection(v) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+                left = cutInsets.left
+            } else {
+                right = cutInsets.right
+            }
+
+            // Don't use padding if the navigation bar isn't in the way
+            if (InsetsHelper.getBottomPaddingRequired(requireActivity()) > 0) {
+                v.setPadding(left, cutInsets.top, right, 0)
+            } else {
+                v.setPadding(left, cutInsets.top, right, 0)
+            }
+            windowInsets
+        }
+    }
+
     private class EmulationState(private val mGamePath: String?) {
         private var state: State
         private var surface: Surface? = null
@@ -340,12 +448,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
     }
 
     companion object {
-        private const val KEY_GAMEPATH = "gamepath"
         private val perfStatsUpdateHandler = Handler()
 
-        fun newInstance(gamePath: String?): EmulationFragment {
+        fun newInstance(game: Game): EmulationFragment {
             val args = Bundle()
-            args.putString(KEY_GAMEPATH, gamePath)
+            args.putParcelable(EmulationActivity.EXTRA_SELECTED_GAME, game)
             val fragment = EmulationFragment()
             fragment.arguments = args
             return fragment

+ 0 - 129
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MenuFragment.java

@@ -1,129 +0,0 @@
-package org.yuzu.yuzu_emu.fragments;
-
-import android.content.pm.PackageManager;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.util.SparseIntArray;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.LinearLayout;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.fragment.app.Fragment;
-
-import com.google.android.material.color.MaterialColors;
-import com.google.android.material.elevation.ElevationOverlayProvider;
-
-import org.yuzu.yuzu_emu.R;
-import org.yuzu.yuzu_emu.activities.EmulationActivity;
-
-
-public final class MenuFragment extends Fragment implements View.OnClickListener
-{
-    private static final String KEY_TITLE = "title";
-    private static final String KEY_WII = "wii";
-    private static SparseIntArray buttonsActionsMap = new SparseIntArray();
-
-    private int mCutInset = 0;
-
-    static
-    {
-        buttonsActionsMap.append(R.id.menu_exit, EmulationActivity.MENU_ACTION_EXIT);
-    }
-
-    public static MenuFragment newInstance()
-    {
-        MenuFragment fragment = new MenuFragment();
-
-        Bundle arguments = new Bundle();
-        fragment.setArguments(arguments);
-
-        return fragment;
-    }
-
-    // This is primarily intended to account for any navigation bar at the bottom of the screen
-    private int getBottomPaddingRequired()
-    {
-        Rect visibleFrame = new Rect();
-        requireActivity().getWindow().getDecorView().getWindowVisibleDisplayFrame(visibleFrame);
-        return visibleFrame.bottom - visibleFrame.top - getResources().getDisplayMetrics().heightPixels;
-    }
-
-    @NonNull
-    @Override
-    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
-                             Bundle savedInstanceState)
-    {
-        View rootView = inflater.inflate(R.layout.fragment_ingame_menu, container, false);
-
-        LinearLayout options = rootView.findViewById(R.id.layout_options);
-
-//        mPauseEmulation = options.findViewById(R.id.menu_pause_emulation);
-//        mUnpauseEmulation = options.findViewById(R.id.menu_unpause_emulation);
-//
-//        updatePauseUnpauseVisibility();
-//
-//        if (!requireActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN))
-//        {
-//            options.findViewById(R.id.menu_overlay_controls).setVisibility(View.GONE);
-//        }
-//
-//        if (!getArguments().getBoolean(KEY_WII, true))
-//        {
-//            options.findViewById(R.id.menu_refresh_wiimotes).setVisibility(View.GONE);
-//        }
-
-        int bottomPaddingRequired = getBottomPaddingRequired();
-
-        // Provide a safe zone between the navigation bar and Exit Emulation to avoid accidental touches
-        float density = getResources().getDisplayMetrics().density;
-        if (bottomPaddingRequired >= 32 * density)
-        {
-            bottomPaddingRequired += 32 * density;
-        }
-
-        if (bottomPaddingRequired > rootView.getPaddingBottom())
-        {
-            rootView.setPadding(rootView.getPaddingLeft(), rootView.getPaddingTop(),
-                    rootView.getPaddingRight(), bottomPaddingRequired);
-        }
-
-        for (int childIndex = 0; childIndex < options.getChildCount(); childIndex++)
-        {
-            Button button = (Button) options.getChildAt(childIndex);
-
-            button.setOnClickListener(this);
-        }
-
-        rootView.findViewById(R.id.menu_exit).setOnClickListener(this);
-
-//        mTitleText = rootView.findViewById(R.id.text_game_title);
-//        String title = getArguments().getString(KEY_TITLE, null);
-//        if (title != null)
-//        {
-//            mTitleText.setText(title);
-//        }
-
-        if (getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR)
-        {
-//            rootView.post(() -> NativeLibrary.SetObscuredPixelsLeft(rootView.getWidth()));
-        }
-
-        return rootView;
-    }
-
-    @Override
-    public void onClick(View button)
-    {
-        int action = buttonsActionsMap.get(button.getId());
-        EmulationActivity activity = (EmulationActivity) requireActivity();
-        activity.handleMenuAction(action);
-    }
-}

+ 8 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InsetsHelper.kt

@@ -1,7 +1,9 @@
 package org.yuzu.yuzu_emu.utils
 
 import android.annotation.SuppressLint
+import android.app.Activity
 import android.content.Context
+import android.graphics.Rect
 import android.view.ViewGroup.MarginLayoutParams
 import androidx.core.graphics.Insets
 import com.google.android.material.appbar.AppBarLayout
@@ -27,4 +29,10 @@ object InsetsHelper {
             resources.getInteger(resourceId)
         } else 0
     }
+
+    fun getBottomPaddingRequired(activity: Activity): Int {
+        val visibleFrame = Rect()
+        activity.window.decorView.getWindowVisibleDisplayFrame(visibleFrame)
+        return visibleFrame.bottom - visibleFrame.top - activity.resources.displayMetrics.heightPixels
+    }
 }

+ 37 - 0
src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/SerializableHelper.kt

@@ -0,0 +1,37 @@
+package org.yuzu.yuzu_emu.utils
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import java.io.Serializable
+
+object SerializableHelper {
+    inline fun <reified T : Serializable> Bundle.serializable(key: String): T? {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+            getSerializable(key, T::class.java)
+        else
+            getSerializable(key) as? T
+    }
+
+    inline fun <reified T : Serializable> Intent.serializable(key: String): T? {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+            getSerializableExtra(key, T::class.java)
+        else
+            getSerializableExtra(key) as? T
+    }
+
+    inline fun <reified T : Parcelable> Bundle.parcelable(key: String): T? {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+            getParcelable(key, T::class.java)
+        else
+            getParcelable(key) as? T
+    }
+
+    inline fun <reified T : Parcelable> Intent.parcelable(key: String): T? {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
+            getParcelableExtra(key, T::class.java)
+        else
+            getParcelableExtra(key) as? T
+    }
+}

+ 9 - 0
src/android/app/src/main/res/drawable/ic_controller.xml

@@ -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="?attr/colorControlNormal"
+        android:pathData="M21,6L3,6c-1.1,0 -2,0.9 -2,2v8c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,8c0,-1.1 -0.9,-2 -2,-2zM11,13L8,13v3L6,16v-3L3,13v-2h3L6,8h2v3h3v2zM15.5,15c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM19.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S18.67,9 19.5,9s1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5z" />
+</vector>

+ 10 - 0
src/android/app/src/main/res/drawable/ic_exit.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:autoMirrored="true"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="?attr/colorControlNormal"
+        android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z" />
+</vector>

+ 9 - 0
src/android/app/src/main/res/drawable/ic_pause.xml

@@ -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="?attr/colorControlNormal"
+        android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
+</vector>

+ 9 - 0
src/android/app/src/main/res/drawable/ic_play.xml

@@ -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="?attr/colorControlNormal"
+        android:pathData="M8,5v14l11,-7z" />
+</vector>

+ 7 - 26
src/android/app/src/main/res/layout/activity_emulation.xml

@@ -1,32 +1,13 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-             android:layout_width="match_parent"
-             android:layout_height="match_parent"
-             android:keepScreenOn="true"
-             xmlns:tools="http://schemas.android.com/tools"
-             android:id="@+id/frame_content">
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/frame_content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:keepScreenOn="true">
 
     <FrameLayout
         android:id="@+id/frame_emulation_fragment"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:baselineAligned="false">
-
-        <FrameLayout
-            android:id="@+id/frame_menu"
-            android:layout_width="@dimen/menu_width"
-            android:layout_height="match_parent"
-            tools:layout="@layout/fragment_ingame_menu"/>
-
-        <FrameLayout
-            android:id="@+id/frame_submenu"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"/>
-
-    </LinearLayout>
+        android:layout_height="match_parent" />
 
 </FrameLayout>

+ 54 - 38
src/android/app/src/main/res/layout/fragment_emulation.xml

@@ -1,47 +1,63 @@
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/drawer_layout"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:keepScreenOn="true"
-    tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment">
+    tools:context="org.yuzu.yuzu_emu.fragments.EmulationFragment"
+    tools:openDrawer="start">
 
-    <!-- This is what everything is rendered to during emulation -->
-    <Button
-        android:id="@+id/done_control_config"
-        style="@style/Widget.Material3.Button.Icon"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center"
-        android:padding="@dimen/spacing_small"
-        android:text="@string/emulation_done"
-        android:visibility="gone" />
-
-    <!-- This is the onscreen input overlay -->
-    <SurfaceView
-        android:id="@+id/surface_emulation"
+    <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:focusable="false"
-        android:focusableInTouchMode="false" />
+        android:layout_height="match_parent">
 
-    <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 -->
+        <SurfaceView
+            android:id="@+id/surface_emulation"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:focusable="false"
+            android:focusableInTouchMode="false" />
+
+        <TextView
+            android:id="@+id/show_fps_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="18dp"
+            android:layout_marginTop="2dp"
+            android:clickable="false"
+            android:linksClickable="false"
+            android:longClickable="false"
+            android:shadowColor="@android:color/black"
+            android:textColor="@android:color/white"
+            android:textSize="12sp" />
 
-    <TextView
-        android:id="@+id/show_fps_text"
+        <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 what everything is rendered to during emulation -->
+        <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" />
+
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+    <com.google.android.material.navigation.NavigationView
+        android:id="@+id/in_game_menu"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_marginStart="18dp"
-        android:layout_marginTop="2dp"
-        android:clickable="false"
-        android:linksClickable="false"
-        android:longClickable="false"
-        android:shadowColor="@android:color/black"
-        android:textColor="@android:color/white"
-        android:textSize="12sp" />
-
-</FrameLayout>
+        android:layout_height="match_parent"
+        android:layout_gravity="start"
+        app:headerLayout="@layout/header_in_game"
+        app:menu="@menu/menu_in_game" />
+
+</androidx.drawerlayout.widget.DrawerLayout>

+ 0 - 56
src/android/app/src/main/res/layout/fragment_ingame_menu.xml

@@ -1,56 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:background="?attr/colorSurface"
-    android:elevation="3dp"
-    tools:layout_width="250dp">
-
-    <TextView
-        android:id="@+id/text_game_title"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_marginHorizontal="32dp"
-        android:layout_marginVertical="24dp"
-        android:ellipsize="end"
-        android:letterSpacing="0"
-        android:maxLines="@integer/game_title_lines"
-        android:textSize="20sp"
-        android:textColor="?attr/colorOnSurface"
-        tools:text="The Legend of Zelda: Breath of the Wild" />
-
-    <com.google.android.material.divider.MaterialDivider
-        android:id="@+id/divider"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <ScrollView
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1"
-        android:scrollbarSize="4dp"
-        android:fadeScrollbars="false">
-
-        <LinearLayout
-            android:id="@+id/layout_options"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:orientation="vertical" />
-
-    </ScrollView>
-
-    <com.google.android.material.divider.MaterialDivider
-        android:id="@+id/divider_2"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <Button
-        android:id="@+id/menu_exit"
-        style="@style/InGameMenuOption"
-        android:layout_marginTop="@dimen/spacing_large"
-        android:text="@string/emulation_exit" />
-
-</LinearLayout>

+ 23 - 0
src/android/app/src/main/res/layout/header_in_game.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:fitsSystemWindows="true">
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/text_game_title"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_margin="24dp"
+        android:textAppearance="?attr/textAppearanceHeadlineMedium"
+        android:textColor="?attr/colorOnSurface"
+        android:textAlignment="viewStart"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Super Mario Odyssey" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 32 - 0
src/android/app/src/main/res/menu/menu_in_game.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:title="">
+
+        <menu>
+
+            <item
+                android:id="@+id/menu_pause_emulation"
+                android:icon="@drawable/ic_pause"
+                android:title="@string/emulation_pause" />
+
+            <item
+                android:id="@+id/menu_settings"
+                android:icon="@drawable/ic_settings"
+                android:title="@string/preferences_settings" />
+
+            <item
+                android:id="@+id/menu_overlay_controls"
+                android:icon="@drawable/ic_controller"
+                android:title="@string/emulation_input_overlay" />
+
+        </menu>
+
+    </item>
+
+    <item
+        android:id="@+id/menu_exit"
+        android:icon="@drawable/ic_exit"
+        android:title="@string/emulation_exit" />
+
+</menu>

+ 12 - 0
src/android/app/src/main/res/menu/menu_overlay_options.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/menu_edit_overlay"
+        android:title="@string/emulation_touch_overlay_edit" />
+
+    <item
+        android:id="@+id/menu_reset_overlay"
+        android:title="@string/emulation_touch_overlay_reset" />
+
+</menu>

+ 4 - 0
src/android/app/src/main/res/values/strings.xml

@@ -87,6 +87,10 @@
     <string name="emulation_toggle_controls">Toggle Controls</string>
     <string name="emulation_control_scale">Adjust Scale</string>
     <string name="emulation_touch_overlay_reset">Reset Overlay</string>
+    <string name="emulation_touch_overlay_edit">Edit Overlay</string>
+    <string name="emulation_pause">Pause Emulation</string>
+    <string name="emulation_unpause">Unpause Emulation</string>
+    <string name="emulation_input_overlay">Input Overlay</string>
 
     <string name="load_settings">Loading Settings…</string>