added swipe up to open game menu

This commit is contained in:
izzy2lost
2026-03-11 23:50:22 -04:00
parent 3bbca4cf59
commit b45a0c64f9
3 changed files with 191 additions and 0 deletions
@@ -11,11 +11,13 @@ import android.os.Process
import android.util.Log
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.view.ViewConfiguration
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.ImageView
@@ -30,6 +32,7 @@ import org.libsdl.app.SDLSurface
import java.io.File
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.max
class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
companion object {
@@ -58,6 +61,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
private var startupSnapshotSlot: Int? = null
private var startupSnapshotLoadScheduled = false
@Volatile private var processTerminationScheduled = false
private lateinit var swipeUpGestureRecognizer: SwipeUpGestureRecognizer
override fun createSDLSurface(context: Context): SDLSurface {
return super.createSDLSurface(context).apply {
@@ -76,6 +80,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
if (requestedSlot in 1..TOTAL_SNAPSHOT_SLOTS) {
startupSnapshotSlot = requestedSlot
}
initializeSwipeMenuGesture()
setupOnScreenController()
setupControllerDetection()
hideSystemUI()
@@ -121,6 +126,16 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
return super.dispatchKeyEvent(event)
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (shouldHandleActivitySwipeMenu() && swipeUpGestureRecognizer.onTouchEvent(event)) {
return true
}
if (!shouldHandleActivitySwipeMenu()) {
swipeUpGestureRecognizer.reset()
}
return super.dispatchTouchEvent(event)
}
private fun hideSystemUI() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11 (API 30) and above
@@ -144,6 +159,23 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
}
private fun initializeSwipeMenuGesture() {
val swipeTouchSlop = ViewConfiguration.get(this).scaledTouchSlop.toFloat()
swipeUpGestureRecognizer = SwipeUpGestureRecognizer(
minDistancePx = {
max(currentGestureHostHeight() * 0.14f, swipeTouchSlop * 8f)
},
touchSlopPx = { swipeTouchSlop },
canStartAt = { _, y ->
y >= currentGestureHostHeight() * 0.35f
},
onTriggered = {
onScreenController?.resetAllInputs()
showInGameMenu()
},
)
}
private fun setupOnScreenController() {
// Create on-screen controller
onScreenController = OnScreenController(this).apply {
@@ -180,6 +212,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
override fun onPause() {
swipeUpGestureRecognizer.reset()
onScreenController?.resetAllInputs()
super.onPause()
}
@@ -267,6 +300,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
if (shouldShow != isControllerVisible) {
isControllerVisible = shouldShow
swipeUpGestureRecognizer.reset()
onScreenController?.visibility = if (shouldShow) View.VISIBLE else View.GONE
}
}
@@ -292,6 +326,7 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
override fun onDestroy() {
Log.i(TAG, "onDestroy()")
swipeUpGestureRecognizer.reset()
inGameMenuDialog?.dismiss()
inGameMenuDialog = null
val shouldTerminateProcess = isFinishing && !isChangingConfigurations
@@ -665,6 +700,11 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
}
private fun showInGameMenu() {
swipeUpGestureRecognizer.reset()
if (inGameMenuDialog?.isShowing == true) {
return
}
val options = arrayOf(
getString(R.string.in_game_menu_resume),
if (isControllerVisible) {
@@ -700,6 +740,15 @@ class MainActivity : SDLActivity(), InputManager.InputDeviceListener {
dialog.show()
}
private fun shouldHandleActivitySwipeMenu(): Boolean {
return !isControllerVisible && inGameMenuDialog?.isShowing != true
}
private fun currentGestureHostHeight(): Float {
val hostHeight = mLayout?.height ?: window.decorView.height
return max(hostHeight, 1).toFloat()
}
private fun exitToGameLibrary() {
val intent = Intent(this, GameLibraryActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
@@ -8,6 +8,8 @@ import android.graphics.PointF
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import kotlin.math.max
import kotlin.math.pow
import kotlin.math.sqrt
@@ -25,6 +27,21 @@ class OnScreenController @JvmOverloads constructor(
private var menuButtonRadius = 0f
private var menuButtonPressed = false
private var menuButtonPointerId = -1
private val swipeTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
private val swipeUpGestureRecognizer = SwipeUpGestureRecognizer(
minDistancePx = {
max(height * 0.14f, swipeTouchSlop * 8f)
},
touchSlopPx = { swipeTouchSlop },
canStartAt = { x, y ->
y >= height * 0.35f && !isPointInInteractiveElement(x, y)
},
onTriggered = {
handleCancel()
onMenuButtonTapped?.invoke()
invalidate()
},
)
var onMenuButtonTapped: (() -> Unit)? = null
@@ -323,6 +340,11 @@ class OnScreenController @JvmOverloads constructor(
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (swipeUpGestureRecognizer.onTouchEvent(event)) {
invalidate()
return true
}
val pointerIndex = event.actionIndex
val pointerId = event.getPointerId(pointerIndex)
val x = event.getX(pointerIndex)
@@ -450,6 +472,8 @@ class OnScreenController @JvmOverloads constructor(
}
private fun handleCancel() {
swipeUpGestureRecognizer.reset()
if (menuButtonPointerId != -1) {
menuButtonPressed = false
menuButtonPointerId = -1
@@ -534,6 +558,18 @@ class OnScreenController @JvmOverloads constructor(
return sqrt(dx.pow(2) + dy.pow(2)) <= radius
}
private fun isPointInInteractiveElement(x: Float, y: Float): Boolean {
if (isPointInCircle(x, y, menuButtonCenter, menuButtonRadius)) {
return true
}
if (sticks.values.any { isPointInCircle(x, y, it.center, it.radius) }) {
return true
}
return buttons.values.any { isPointInCircle(x, y, it.center, it.radius) }
}
fun setVisibility(visible: Boolean) {
visibility = if (visible) View.VISIBLE else View.GONE
}
@@ -0,0 +1,106 @@
package com.izzy2lost.x1box
import android.view.MotionEvent
import kotlin.math.abs
import kotlin.math.max
class SwipeUpGestureRecognizer(
private val minDistancePx: () -> Float,
private val touchSlopPx: () -> Float,
private val canStartAt: (x: Float, y: Float) -> Boolean,
private val onTriggered: () -> Unit,
) {
private var activePointerId = INVALID_POINTER_ID
private var startX = 0f
private var startY = 0f
private var tracking = false
private var triggered = false
fun onTouchEvent(event: MotionEvent): Boolean {
if (triggered) {
when (event.actionMasked) {
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> reset()
MotionEvent.ACTION_POINTER_UP -> {
if (event.getPointerId(event.actionIndex) == activePointerId) {
reset()
}
}
}
return true
}
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
reset()
val x = event.getX(0)
val y = event.getY(0)
if (canStartAt(x, y)) {
activePointerId = event.getPointerId(0)
startX = x
startY = y
tracking = true
}
}
MotionEvent.ACTION_POINTER_DOWN -> {
reset()
}
MotionEvent.ACTION_MOVE -> {
if (!tracking) {
return false
}
val pointerIndex = event.findPointerIndex(activePointerId)
if (pointerIndex < 0) {
reset()
return false
}
val x = event.getX(pointerIndex)
val y = event.getY(pointerIndex)
val dx = x - startX
val dy = y - startY
val upwardDistance = -dy
val slop = touchSlopPx()
if (dy > slop || abs(dx) > max(slop * 5f, upwardDistance * 0.9f + slop)) {
reset()
return false
}
if (upwardDistance >= minDistancePx() &&
upwardDistance >= abs(dx) * 1.35f) {
triggered = true
tracking = false
onTriggered()
return true
}
}
MotionEvent.ACTION_POINTER_UP -> {
if (event.getPointerId(event.actionIndex) == activePointerId) {
reset()
}
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> reset()
}
return false
}
fun reset() {
activePointerId = INVALID_POINTER_ID
startX = 0f
startY = 0f
tracking = false
triggered = false
}
private companion object {
const val INVALID_POINTER_ID = -1
}
}