Android: Add drawerlayout with the option to exit emulation (#298)

This commit is contained in:
NullVector 2026-09-26 20:26:24 +09:30 committed by GitHub
parent 1a6c52216d
commit 00b15a301e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 297 additions and 146 deletions

View File

@ -58,6 +58,7 @@ android {
dependencies {
implementation("androidx.activity:activity-ktx:1.9.3")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.drawerlayout:drawerlayout:1.2.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("androidx.window:window:1.3.0")

View File

@ -35,6 +35,7 @@ object NativeLibrary {
external fun getSettingString(setting: String): String
external fun getSettingBool(setting: String): Boolean
external fun setSettingString(setting: String, value: String)
external fun setSettingBool(setting: String, value: Boolean)
external fun getSettingInt(setting: String): Int
external fun setSettingInt(setting: String, value: Int)
external fun saveSettings()

View File

@ -2,7 +2,16 @@ package org.nxemu
object NXUISetting {
const val GameDirectories = "nxui:GameDirectories"
const val ShowInputOverlay = "nxui:ShowInputOverlay"
const val JoystickRelCenter = "nxui:JoystickRelCenter"
const val DpadSlide = "nxui:DpadSlide"
const val HapticFeedback = "nxui:HapticFeedback"
const val Touchscreen = "nxui:Touchscreen"
const val OverlayScale = "nxui:OverlayScale"
const val OverlayOpacity = "nxui:OverlayOpacity"
const val OverlayControlData = "nxui:OverlayControlData"
const val ThemeMode = "nxui:ThemeMode"
const val LockDrawer = "nxui:LockDrawer"
}
object NXCoreSetting {

View File

@ -1,11 +0,0 @@
package org.nxemu.features.settings.model
enum class BooleanSetting {
JOYSTICK_REL_CENTER,
DPAD_SLIDE,
HAPTIC_FEEDBACK,
SHOW_INPUT_OVERLAY,
TOUCHSCREEN;
fun getBoolean(needsGlobal: Boolean = false): Boolean = true
}

View File

@ -1,8 +0,0 @@
package org.nxemu.features.settings.model
enum class IntSetting(private val default: Int) {
OVERLAY_SCALE(50),
OVERLAY_OPACITY(100);
fun getInt(needsGlobal: Boolean = false): Int = default
}

View File

@ -24,13 +24,13 @@ import androidx.core.content.ContextCompat
import androidx.window.layout.WindowMetricsCalculator
import kotlin.math.max
import kotlin.math.min
import org.nxemu.NXUISetting
import org.nxemu.NativeLibrary
import org.nxemu.features.input.NativeInput
import org.nxemu.R
import org.nxemu.features.input.model.NativeAnalog
import org.nxemu.features.input.model.NativeButton
import org.nxemu.features.input.model.NpadStyleIndex
import org.nxemu.features.settings.model.BooleanSetting
import org.nxemu.features.settings.model.IntSetting
import org.nxemu.overlay.model.OverlayControl
import org.nxemu.overlay.model.OverlayControlData
import org.nxemu.overlay.model.OverlayLayout
@ -119,7 +119,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
}
for (dpad in overlayDpads) {
if (!dpad.updateStatus(event, BooleanSetting.DPAD_SLIDE.getBoolean())) {
if (!dpad.updateStatus(event, NativeLibrary.getSettingBool(NXUISetting.DpadSlide))) {
continue
}
NativeInput.onOverlayButtonEvent(
@ -169,7 +169,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
invalidate()
}
if (!BooleanSetting.TOUCHSCREEN.getBoolean()) {
if (!NativeLibrary.getSettingBool(NXUISetting.Touchscreen)) {
return true
}
@ -206,7 +206,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
}
private fun playHaptics(event: MotionEvent) {
if (BooleanSetting.HAPTIC_FEEDBACK.getBoolean()) {
if (NativeLibrary.getSettingBool(NXUISetting.HapticFeedback)) {
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_POINTER_DOWN ->
@ -601,7 +601,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
overlayJoysticks.clear()
// Add all the enabled overlay items back to the HashSet.
if (BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()) {
if (NativeLibrary.getSettingBool(NXUISetting.ShowInputOverlay)) {
addOverlayControls(layout)
}
invalidate()
@ -857,7 +857,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
else -> 0.11f
}
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale *= (NativeLibrary.getSettingInt(NXUISetting.OverlayScale) + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableButton.
@ -896,7 +896,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
drawableX - (width / 2),
drawableY - (height / 2)
)
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
overlayDrawable.setOpacity(NativeLibrary.getSettingInt(NXUISetting.OverlayOpacity) * 255 / 100)
return overlayDrawable
}
@ -924,7 +924,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Decide scale based on button ID and user preference
var scale = 0.25f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale *= (NativeLibrary.getSettingInt(NXUISetting.OverlayScale) + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableDpad.
@ -963,7 +963,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Need to set the image's position
overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
overlayDrawable.setOpacity(NativeLibrary.getSettingInt(NXUISetting.OverlayOpacity) * 255 / 100)
return overlayDrawable
}
@ -997,7 +997,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Decide scale based on user preference
var scale = 0.3f
scale *= (IntSetting.OVERLAY_SCALE.getInt() + 50).toFloat()
scale *= (NativeLibrary.getSettingInt(NXUISetting.OverlayScale) + 50).toFloat()
scale /= 100f
// Initialize the InputOverlayDrawableJoystick.
@ -1042,7 +1042,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) :
// Need to set the image's position
overlayDrawable.setPosition(drawableX, drawableY)
overlayDrawable.setOpacity(IntSetting.OVERLAY_OPACITY.getInt() * 255 / 100)
overlayDrawable.setOpacity(NativeLibrary.getSettingInt(NXUISetting.OverlayOpacity) * 255 / 100)
return overlayDrawable
}
}

View File

@ -13,10 +13,11 @@ import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import org.nxemu.NXUISetting
import org.nxemu.NativeLibrary
import org.nxemu.features.input.NativeInput.ButtonState
import org.nxemu.features.input.model.NativeAnalog
import org.nxemu.features.input.model.NativeButton
import org.nxemu.features.settings.model.BooleanSetting
/**
* Custom [BitmapDrawable] that is capable
@ -126,7 +127,7 @@ class InputOverlayDrawableJoystick(
pressedState = true
outerBitmap.alpha = 0
boundsBoxBitmap.alpha = opacity
if (BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()) {
if (NativeLibrary.getSettingBool(NXUISetting.JoystickRelCenter)) {
virtBounds.offset(
xPosition - virtBounds.centerX(),
yPosition - virtBounds.centerY()

View File

@ -11,8 +11,11 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Base64
import android.util.Log
import android.view.Gravity
import android.view.MotionEvent
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.View
@ -20,14 +23,18 @@ import android.view.WindowManager
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.drawerlayout.widget.DrawerLayout
import org.nxemu.NXCoreSetting
import org.nxemu.NXUISetting
import org.nxemu.NativeLibrary
import org.nxemu.R
import org.nxemu.overlay.InputOverlay
import org.nxemu.overlay.model.OverlayLayout
import org.nxemu.utils.NativeConfig
import org.json.JSONObject
import java.nio.ByteBuffer
import java.util.Locale
@ -50,6 +57,8 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
private lateinit var showFpsText: TextView
private lateinit var showDeviceText: TextView
private lateinit var surfaceInputOverlay: InputOverlay
private lateinit var drawerLayout: DrawerLayout
private lateinit var lockDrawerItem: TextView
private lateinit var overlayAppVersion: String
private lateinit var overlayPhoneModel: String
private lateinit var overlaySoc: String
@ -79,12 +88,28 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
surfaceInputOverlay = findViewById(R.id.surface_input_overlay)
surfaceInputOverlay.setZOrderMediaOverlay(true)
surfaceInputOverlay.holder.setFormat(PixelFormat.TRANSLUCENT)
drawerLayout = findViewById(R.id.drawer_layout)
lockDrawerItem = findViewById(R.id.menu_lock_drawer)
setupInGameDrawer()
updateInputOverlayLayout()
cacheDeviceOverlayInfo()
NativeLibrary.addSettingChangedListener(settingChangedListener)
hideLoadingIfFirstFrame()
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (drawerLayout.isDrawerOpen(Gravity.START)) {
drawerLayout.closeDrawer(Gravity.START)
} else {
drawerLayout.openDrawer(Gravity.START)
}
}
},
)
val path = intent.getStringExtra(EXTRA_GAME_PATH)
if (path.isNullOrEmpty()) {
Log.e(TAG, "Missing EXTRA_GAME_PATH")
@ -103,6 +128,78 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
}
}
private fun setupInGameDrawer() {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
drawerLayout.addDrawerListener(object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {
releaseOverlayTouches()
if (slideOffset > 0f) {
surfaceInputOverlay.visibility = View.INVISIBLE
}
}
override fun onDrawerOpened(drawerView: View) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED)
findViewById<View>(R.id.in_game_menu).requestFocus()
}
override fun onDrawerClosed(drawerView: View) {
drawerLayout.setDrawerLockMode(savedDrawerLockMode())
if (loadingIndicator.visibility != View.VISIBLE) {
surfaceInputOverlay.visibility = View.VISIBLE
}
}
})
lockDrawerItem.setOnClickListener { toggleDrawerLock() }
findViewById<View>(R.id.menu_exit).setOnClickListener {
drawerLayout.closeDrawers()
finish()
}
updateLockDrawerLabel()
}
private fun releaseOverlayTouches() {
val event = MotionEvent.obtain(
SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(),
MotionEvent.ACTION_UP,
0f,
0f,
0,
)
surfaceInputOverlay.dispatchTouchEvent(event)
event.recycle()
}
private fun savedDrawerLockMode(): Int {
val mode = NativeLibrary.getSettingInt(NXUISetting.LockDrawer)
return if (mode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
} else {
DrawerLayout.LOCK_MODE_UNLOCKED
}
}
private fun toggleDrawerLock() {
val mode = if (savedDrawerLockMode() == DrawerLayout.LOCK_MODE_UNLOCKED) {
DrawerLayout.LOCK_MODE_LOCKED_CLOSED
} else {
DrawerLayout.LOCK_MODE_UNLOCKED
}
NativeLibrary.setSettingInt(NXUISetting.LockDrawer, mode)
updateLockDrawerLabel()
NativeConfig.saveGlobalConfig()
}
private fun updateLockDrawerLabel() {
lockDrawerItem.text = getString(
if (savedDrawerLockMode() == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
R.string.unlock_drawer
} else {
R.string.lock_drawer
},
)
}
private fun hideSystemBars() {
WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.systemBars())
@ -115,7 +212,10 @@ class EmulationActivity : ComponentActivity(), SurfaceHolder.Callback {
stopAnimatedDrawables()
loadingIndicator.visibility = View.GONE
startPerfOverlay()
surfaceInputOverlay.visibility = View.VISIBLE
if (!drawerLayout.isDrawerOpen(Gravity.START)) {
surfaceInputOverlay.visibility = View.VISIBLE
}
drawerLayout.setDrawerLockMode(savedDrawerLockMode())
updateInputOverlayLayout()
surfaceInputOverlay.refreshControls()
}

View File

@ -26,6 +26,16 @@ class NxEmuBridge(private val activity: MainActivity) {
NativeLibrary.saveSettings()
}
@JavascriptInterface
fun getSettingBool(setting: String): Boolean {
return NativeLibrary.getSettingBool(setting)
}
@JavascriptInterface
fun setSettingBool(setting: String, value: Boolean) {
NativeLibrary.setSettingBool(setting, value)
}
@JavascriptInterface
fun getSettingInt(setting: String): Int {
return NativeLibrary.getSettingInt(setting)

View File

@ -1,140 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<SurfaceView
android:id="@+id/emulation_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true" />
<org.nxemu.overlay.InputOverlay
android:id="@+id/surface_input_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="invisible" />
android:keepScreenOn="true">
<FrameLayout
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@android:color/black">
<ImageView
android:id="@+id/loading_corner_logo"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="top|start"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:padding="8dp"
android:scaleType="fitStart"
android:visibility="gone" />
<SurfaceView
android:id="@+id/emulation_surface"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<org.nxemu.overlay.InputOverlay
android:id="@+id/surface_input_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/emulation_loading_card"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp"
android:paddingVertical="20dp">
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:visibility="invisible" />
<FrameLayout
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/loading_image"
android:layout_width="64dp"
android:layout_height="64dp"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher" />
android:id="@+id/loading_corner_logo"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_gravity="top|start"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:padding="8dp"
android:scaleType="fitStart"
android:visibility="gone" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical">
android:layout_gravity="center"
android:background="@drawable/emulation_loading_card"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="24dp"
android:paddingVertical="20dp">
<TextView
android:id="@+id/loading_title"
<ImageView
android:id="@+id/loading_image"
android:layout_width="64dp"
android:layout_height="64dp"
android:contentDescription="@string/app_name"
android:scaleType="fitCenter"
android:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:maxWidth="280dp"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
android:layout_marginStart="16dp"
android:orientation="vertical">
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/loading"
android:textColor="#B3FFFFFF"
android:textSize="14sp" />
<TextView
android:id="@+id/loading_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:maxWidth="280dp"
android:singleLine="true"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
<ProgressBar
android:id="@+id/loading_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:indeterminate="true" />
<TextView
android:id="@+id/loading_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/loading"
android:textColor="#B3FFFFFF"
android:textSize="14sp" />
<ProgressBar
android:id="@+id/loading_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:indeterminate="true" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/loading_corner_banner"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_gravity="bottom|end"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:maxWidth="186dp"
android:padding="6dp"
android:scaleType="fitEnd"
android:visibility="gone" />
<ImageView
android:id="@+id/loading_corner_banner"
android:layout_width="wrap_content"
android:layout_height="64dp"
android:layout_gravity="bottom|end"
android:adjustViewBounds="true"
android:contentDescription="@null"
android:maxWidth="186dp"
android:padding="6dp"
android:scaleType="fitEnd"
android:visibility="gone" />
</FrameLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="20dp"
android:paddingTop="4dp">
<TextView
android:id="@+id/show_fps_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:clickable="false"
android:focusable="false"
android:shadowColor="@android:color/black"
android:shadowRadius="3"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone" />
<TextView
android:id="@+id/show_device_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:clickable="false"
android:focusable="false"
android:gravity="end"
android:shadowColor="@android:color/black"
android:shadowRadius="3"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/overlay_container"
android:layout_width="match_parent"
<LinearLayout
android:id="@+id/in_game_menu"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="20dp"
android:paddingTop="4dp">
android:layout_gravity="start"
android:background="#F0121212"
android:clickable="true"
android:elevation="16dp"
android:focusable="true"
android:orientation="vertical"
android:paddingTop="24dp">
<TextView
android:id="@+id/menu_lock_drawer"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/lock_drawer"
android:textColor="@android:color/white"
android:textSize="16sp" />
<TextView
android:id="@+id/show_fps_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:clickable="false"
android:focusable="false"
android:shadowColor="@android:color/black"
android:shadowRadius="3"
android:id="@+id/menu_exit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:paddingHorizontal="20dp"
android:text="@string/emulation_exit"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone" />
<TextView
android:id="@+id/show_device_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:clickable="false"
android:focusable="false"
android:gravity="end"
android:shadowColor="@android:color/black"
android:shadowRadius="3"
android:textColor="@android:color/white"
android:textSize="12sp"
android:visibility="gone" />
</FrameLayout>
</FrameLayout>
android:textSize="16sp" />
</LinearLayout>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -2,4 +2,7 @@
<resources>
<string name="app_name">NxEmu</string>
<string name="loading">Loading...</string>
<string name="emulation_exit">Exit emulation</string>
<string name="lock_drawer">Lock drawer</string>
<string name="unlock_drawer">Unlock drawer</string>
</resources>