From eb3d2940e35ccd81de60369c237ef6fbceeaa5d6 Mon Sep 17 00:00:00 2001 From: Anish Kumar Date: Sat, 11 Apr 2026 22:04:47 +0530 Subject: [PATCH] Allow moving and resizing the embedded game window on Android --- .../editor/embed/EmbeddedGodotGame.kt | 350 ++++++++++++++++-- .../editor/embed/GameMenuFragment.kt | 28 +- .../src/main/res/drawable/drag_pan_24px.xml | 10 + .../src/main/res/drawable/resize_handle.xml | 13 + .../res/layout/game_menu_fragment_layout.xml | 9 + .../res/layout/godot_embedded_game_layout.xml | 72 ++++ .../editor/src/main/res/values/strings.xml | 1 + .../org/godotengine/godot/GodotActivity.kt | 25 +- 8 files changed, 463 insertions(+), 45 deletions(-) create mode 100644 platform/android/java/editor/src/main/res/drawable/drag_pan_24px.xml create mode 100644 platform/android/java/editor/src/main/res/drawable/resize_handle.xml create mode 100644 platform/android/java/editor/src/main/res/layout/godot_embedded_game_layout.xml diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt index 5456cb231eb..c4974e22599 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt @@ -31,13 +31,18 @@ package org.godotengine.editor.embed import android.content.pm.ActivityInfo +import android.graphics.Color +import android.graphics.Point import android.os.Bundle +import android.util.Rational import android.view.Gravity import android.view.MotionEvent +import android.view.View import android.view.WindowManager import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH +import android.widget.CheckBox import org.godotengine.editor.GodotGame import org.godotengine.editor.R import org.godotengine.godot.editor.utils.GameMenuUtils @@ -50,22 +55,67 @@ class EmbeddedGodotGame : GodotGame() { companion object { private val TAG = EmbeddedGodotGame::class.java.simpleName - private const val FULL_SCREEN_WIDTH = WindowManager.LayoutParams.MATCH_PARENT - private const val FULL_SCREEN_HEIGHT = WindowManager.LayoutParams.MATCH_PARENT + private const val PREFS_NAME = "embedded_game_window_prefs" + private const val KEY_X = "embedded_window_x" + private const val KEY_Y = "embedded_window_y" + private const val KEY_WIDTH = "embedded_window_width" + private const val KEY_HEIGHT = "embedded_window_height" + private const val KEY_FREE_RESIZE = "is_free_resize" + + private const val RESIZE_THRESHOLD = 80f + private const val MIN_WINDOW_SIZE = 400 + private const val MAX_SCREEN_PERCENT = 0.9f + + private const val RESIZE_UI_HIDE_DELAY_MS = 2000L } - private val defaultWidthInPx : Int by lazy { - resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width) - } - private val defaultHeightInPx : Int by lazy { - resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height) - } + private val defaultWidthInPx: Int by lazy { resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width) } + private val defaultHeightInPx: Int by lazy { resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height) } private var layoutWidthInPx = 0 private var layoutHeightInPx = 0 - - private var gameRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED private var isFullscreen = false + private var gameRequestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED + + private var resizingEnabled = false + private var isResizing = false + private var activeCorner = 0 + private var isFreeResize = false + + private var initialWinX = 0 + private var initialWinY = 0 + private var initialWidth = 0 + private var initialHeight = 0 + private var initialTouchX = 0f + private var initialTouchY = 0f + + private val lockAspectRatioCheckBox: CheckBox by lazy { findViewById(R.id.lockAspectRatioCheckBox) } + private val cornerHandles = mutableListOf() + + private val screenBounds: android.graphics.Rect by lazy { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { + windowManager.currentWindowMetrics.bounds + } else { + val size = Point() + windowManager.defaultDisplay.getRealSize(size) + android.graphics.Rect(0, 0, size.x, size.y) + } + } + + private val maxAllowedWidth: Int get() = (screenBounds.width() * MAX_SCREEN_PERCENT).toInt() + private val maxAllowedHeight: Int get() = (screenBounds.height() * MAX_SCREEN_PERCENT).toInt() + + private var lockedAspectRatio: Float = 1.6f + + private val disableResizeHandler = android.os.Handler(android.os.Looper.getMainLooper()) + + private val disableResizeRunnable = Runnable { + if (isResizing) return@Runnable // Keep it active user is resizing again + resizingEnabled = false + gameMenuFragment?.toggleDragButton(false) + lockAspectRatioCheckBox.visibility = View.GONE + cornerHandles.forEach { it.visibility = View.GONE } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -75,14 +125,54 @@ class EmbeddedGodotGame : GodotGame() { val layoutParams = window.attributes layoutParams.flags = layoutParams.flags or FLAG_NOT_TOUCH_MODAL or FLAG_WATCH_OUTSIDE_TOUCH layoutParams.flags = layoutParams.flags and FLAG_DIM_BEHIND.inv() - layoutParams.gravity = Gravity.END or Gravity.BOTTOM + layoutParams.gravity = Gravity.TOP or Gravity.START - layoutWidthInPx = defaultWidthInPx - layoutHeightInPx = defaultHeightInPx - - layoutParams.width = layoutWidthInPx - layoutParams.height = layoutHeightInPx + loadWindowBounds(layoutParams) window.attributes = layoutParams + + setupOverlayUI() + } + + override fun getGodotAppLayout() = R.layout.godot_embedded_game_layout + + private fun setupOverlayUI() { + lockAspectRatioCheckBox.isChecked = !isFreeResize + + lockAspectRatioCheckBox.setOnCheckedChangeListener { _, isChecked -> + isFreeResize = !isChecked + hideResizeUI() + if (isChecked) { + val lp = window.attributes + lockedAspectRatio = lp.width.toFloat() / lp.height.toFloat() + } + saveWindowBounds(updatePiPParams = false) + } + + cornerHandles.apply { + clear() + add(findViewById(R.id.handleTopLeft)) + add(findViewById(R.id.handleTopRight)) + add(findViewById(R.id.handleBottomLeft)) + add(findViewById(R.id.handleBottomRight)) + } + } + + private fun updateLabelText(w: Int, h: Int) { + lockAspectRatioCheckBox.text = getString(R.string.lock_aspect_ratio_btn_text, w, h) + } + + private fun showResizeUI() { + disableResizeHandler.removeCallbacks(disableResizeRunnable) + if (!resizingEnabled) { + resizingEnabled = true + lockAspectRatioCheckBox.visibility = View.VISIBLE + cornerHandles.forEach { it.visibility = View.VISIBLE } + } + } + + private fun hideResizeUI() { + disableResizeHandler.removeCallbacks(disableResizeRunnable) + disableResizeHandler.postDelayed(disableResizeRunnable, RESIZE_UI_HIDE_DELAY_MS) } override fun setRequestedOrientation(requestedOrientation: Int) { @@ -97,40 +187,213 @@ class EmbeddedGodotGame : GodotGame() { } override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (isFullscreen) return super.dispatchTouchEvent(event) + val layoutParams = window.attributes + when (event.actionMasked) { MotionEvent.ACTION_OUTSIDE -> { - if (!isFullscreen) { - if (gameMenuFragment?.isAlwaysOnTop() == true) { - enterPiPMode() - } else { - minimizeGameWindow() + if (gameMenuFragment?.isAlwaysOnTop() == true) { + updatePiPParams(aspectRatio = Rational(layoutWidthInPx, layoutHeightInPx)) + enterPiPMode() + } else { + minimizeGameWindow() + } + } + MotionEvent.ACTION_DOWN -> { + if (resizingEnabled) { + // Check if the click is inside the label's bounds + val location = IntArray(2) + lockAspectRatioCheckBox.getLocationOnScreen(location) + val checkBoxRect = android.graphics.Rect( + location[0], location[1], + location[0] + lockAspectRatioCheckBox.width, + location[1] + lockAspectRatioCheckBox.height + ) + + if (checkBoxRect.contains(event.rawX.toInt(), event.rawY.toInt())) { + // Let the CheckBox handle the click itself + return super.dispatchTouchEvent(event) + } + activeCorner = getTouchedCorner(event.x, event.y, layoutParams.width, layoutParams.height) + if (activeCorner != 0) { + isResizing = true + + initialTouchX = event.rawX + initialTouchY = event.rawY + initialWinX = layoutParams.x + initialWinY = layoutParams.y + initialWidth = layoutParams.width + initialHeight = layoutParams.height + return true } } } - MotionEvent.ACTION_MOVE -> { -// val layoutParams = window.attributes - // TODO: Add logic to move the embedded window. -// window.attributes = layoutParams + if (resizingEnabled && isResizing) { + val dx = (event.rawX - initialTouchX).toInt() + val dy = (event.rawY - initialTouchY).toInt() + applyResizeLogic(layoutParams, dx, dy) + updateLabelText(layoutParams.width, layoutParams.height) + window.attributes = layoutParams + return true + } + } + MotionEvent.ACTION_UP -> { + if (isResizing) { + isResizing = false + saveWindowBounds() + hideResizeUI() + return true + } } } return super.dispatchTouchEvent(event) } + private fun applyResizeLogic(layoutParams: WindowManager.LayoutParams, dx: Int, dy: Int) { + var newW = initialWidth + var newH = initialHeight + + when (activeCorner) { + Gravity.TOP or Gravity.START -> { + newW = (initialWidth - dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth) + newH = if (isFreeResize) { + (initialHeight - dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight) + } else { + (newW / lockedAspectRatio).toInt() + } + layoutParams.x = initialWinX + (initialWidth - newW) + layoutParams.y = initialWinY + (initialHeight - newH) + } + + Gravity.TOP or Gravity.END -> { + newW = (initialWidth + dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth) + newH = if (isFreeResize) { + (initialHeight - dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight) + } else { + (newW / lockedAspectRatio).toInt() + } + layoutParams.y = initialWinY + (initialHeight - newH) + } + + Gravity.BOTTOM or Gravity.START -> { + newW = (initialWidth - dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth) + newH = if (isFreeResize) { + (initialHeight + dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight) + } else { + (newW / lockedAspectRatio).toInt() + } + layoutParams.x = initialWinX + (initialWidth - newW) + } + + Gravity.BOTTOM or Gravity.END -> { + newW = (initialWidth + dx).coerceIn(MIN_WINDOW_SIZE, maxAllowedWidth) + newH = if (isFreeResize) { + (initialHeight + dy).coerceIn(MIN_WINDOW_SIZE, maxAllowedHeight) + } else { + (newW / lockedAspectRatio).toInt() + } + } + } + + // Final aspect-lock check + if (!isFreeResize && newH > maxAllowedHeight) { + newH = maxAllowedHeight + newW = (newH * lockedAspectRatio).toInt() + + // Re-adjust pivots if it hits the vertical cap + if (activeCorner == (Gravity.TOP or Gravity.START) || activeCorner == (Gravity.TOP or Gravity.END)) { + layoutParams.y = initialWinY + (initialHeight - newH) + } + if (activeCorner == (Gravity.TOP or Gravity.START) || activeCorner == (Gravity.BOTTOM or Gravity.START)) { + layoutParams.x = initialWinX + (initialWidth - newW) + } + } + + layoutParams.width = newW + layoutParams.height = newH + + val hittingLimit = newW >= maxAllowedWidth || newH >= maxAllowedHeight + lockAspectRatioCheckBox.setTextColor(if (hittingLimit) Color.RED else Color.WHITE) + } + + private fun getTouchedCorner(x: Float, y: Float, w: Int, h: Int): Int { + return when { + x < RESIZE_THRESHOLD && y < RESIZE_THRESHOLD -> Gravity.TOP or Gravity.START + x > w - RESIZE_THRESHOLD && y < RESIZE_THRESHOLD -> Gravity.TOP or Gravity.END + x < RESIZE_THRESHOLD && y > h - RESIZE_THRESHOLD -> Gravity.BOTTOM or Gravity.START + x > w - RESIZE_THRESHOLD && y > h - RESIZE_THRESHOLD -> Gravity.BOTTOM or Gravity.END + else -> 0 + } + } + + private fun saveWindowBounds(updatePiPParams: Boolean = true) { + if (isFullscreen) return + + val layoutParams = window.attributes + layoutWidthInPx = layoutParams.width + layoutHeightInPx = layoutParams.height + getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().apply { + putInt(KEY_X, layoutParams.x) + putInt(KEY_Y, layoutParams.y) + putInt(KEY_WIDTH, layoutParams.width) + putInt(KEY_HEIGHT, layoutParams.height) + putBoolean(KEY_FREE_RESIZE, isFreeResize) + apply() + } + if (updatePiPParams) { + updatePiPParams(aspectRatio = Rational(layoutParams.width, layoutParams.height)) + } + } + + private fun loadWindowBounds(layoutParams: WindowManager.LayoutParams) { + val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE) + isFreeResize = prefs.getBoolean(KEY_FREE_RESIZE, false) + layoutWidthInPx = prefs.getInt(KEY_WIDTH, defaultWidthInPx) + layoutHeightInPx = prefs.getInt(KEY_HEIGHT, defaultHeightInPx) + layoutParams.x = prefs.getInt(KEY_X, screenBounds.width() - layoutWidthInPx) + layoutParams.y = prefs.getInt(KEY_Y, screenBounds.height() - layoutHeightInPx) + layoutParams.width = layoutWidthInPx + layoutParams.height = layoutHeightInPx + lockedAspectRatio = layoutParams.width.toFloat() / layoutParams.height.toFloat() + updateLabelText(layoutWidthInPx, layoutHeightInPx) + } + + override fun dragGameWindow(view: View, event: MotionEvent): Boolean { + if (isFullscreen) return false + val lp = window.attributes + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + showResizeUI() + gameMenuFragment?.toggleDragButton(true) + initialTouchX = event.rawX + initialTouchY = event.rawY + initialWinX = lp.x + initialWinY = lp.y + return true + } + MotionEvent.ACTION_MOVE -> { + lp.x = (initialWinX + (event.rawX - initialTouchX)).toInt() + lp.y = (initialWinY + (event.rawY - initialTouchY)).toInt() + window.attributes = lp + return true + } + MotionEvent.ACTION_UP -> { + saveWindowBounds(updatePiPParams = false) // Only window position is changed, no need to update aspect ratio. + hideResizeUI() + return true + } + } + return false + } + override fun getEditorWindowInfo() = EMBEDDED_RUN_GAME_INFO override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.ENABLED override fun isGameEmbedded() = true - private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) { - val layoutParams = window.attributes - layoutParams.width = widthInPx - layoutParams.height = heightInPx - window.attributes = layoutParams - } - - override fun isMinimizedButtonEnabled() = true + override fun isMinimizedButtonEnabled() = isFullscreen override fun isCloseButtonEnabled() = true @@ -140,24 +403,37 @@ class EmbeddedGodotGame : GodotGame() { override fun isMenuBarCollapsable() = false + override fun isDragButtonEnabled() = !isFullscreen + override fun isAlwaysOnTopSupported() = isPiPModeSupported() override fun onFullScreenUpdated(enabled: Boolean) { godot?.enableImmersiveMode(enabled) isFullscreen = enabled + + val layoutParams = window.attributes if (enabled) { - layoutWidthInPx = FULL_SCREEN_WIDTH - layoutHeightInPx = FULL_SCREEN_HEIGHT + layoutWidthInPx = WindowManager.LayoutParams.MATCH_PARENT + layoutHeightInPx = WindowManager.LayoutParams.MATCH_PARENT requestedOrientation = gameRequestedOrientation + layoutParams.x = 0 + layoutParams.y = 0 } else { - layoutWidthInPx = defaultWidthInPx - layoutHeightInPx = defaultHeightInPx + loadWindowBounds(layoutParams) // Cache the last used orientation in fullscreen to reapply when re-entering fullscreen. gameRequestedOrientation = requestedOrientation requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } updateWindowDimensions(layoutWidthInPx, layoutHeightInPx) + gameMenuFragment?.refreshButtonsVisibility() + } + + private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) { + val layoutParams = window.attributes + layoutParams.width = widthInPx + layoutParams.height = heightInPx + window.attributes = layoutParams } override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) { diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt index 8161fc1c25b..46b3b8f9762 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt @@ -31,11 +31,11 @@ package org.godotengine.editor.embed import android.content.Context -import android.os.Build import android.os.Bundle import android.preference.PreferenceManager import android.view.LayoutInflater import android.view.MenuItem +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.Button @@ -112,13 +112,14 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { fun minimizeGameWindow() {} fun closeGameWindow() {} + fun dragGameWindow(view: View, event: MotionEvent): Boolean { return false} fun isMinimizedButtonEnabled() = false fun isFullScreenButtonEnabled() = false fun isCloseButtonEnabled() = false fun isPiPButtonEnabled() = false fun isMenuBarCollapsable() = false - + fun isDragButtonEnabled() = false fun isAlwaysOnTopSupported() = false fun onFullScreenUpdated(enabled: Boolean) {} @@ -128,6 +129,9 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { private val collapseMenuButton: View? by lazy { view?.findViewById(R.id.game_menu_collapse_button) } + private val dragButton: View? by lazy { + view?.findViewById(R.id.game_menu_drag_button) + } private val suspendButton: View? by lazy { view?.findViewById(R.id.game_menu_suspend_button) } @@ -260,6 +264,7 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { val isCloseButtonEnabled = menuListener?.isCloseButtonEnabled() == true val isPiPButtonEnabled = menuListener?.isPiPButtonEnabled() == true val isMenuBarCollapsable = menuListener?.isMenuBarCollapsable() == true + val isDragButtonEnabled = menuListener?.isDragButtonEnabled() == true // Show the divider if any of the window controls is visible view.findViewById(R.id.game_menu_window_controls_divider)?.isVisible = @@ -267,7 +272,8 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { isFullScreenButtonEnabled || isCloseButtonEnabled || isPiPButtonEnabled || - isMenuBarCollapsable + isMenuBarCollapsable || + isDragButtonEnabled collapseMenuButton?.apply { isVisible = isMenuBarCollapsable @@ -275,6 +281,13 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { collapseGameMenu() } } + dragButton?.apply { + isVisible = isDragButtonEnabled + setOnTouchListener { v, event -> + menuListener?.dragGameWindow(v, event) == true + } + + } fullscreenButton?.apply{ isVisible = isFullScreenButtonEnabled setOnClickListener { @@ -442,6 +455,10 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { internal fun isAlwaysOnTop() = isGameEmbedded && alwaysOnTopChecked + internal fun toggleDragButton(pressed: Boolean) { + dragButton?.isPressed = pressed + } + private fun collapseGameMenu() { view?.isVisible = false PreferenceManager.getDefaultSharedPreferences(context).edit { @@ -458,6 +475,11 @@ class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener { menuListener?.onGameMenuCollapsed(false) } + internal fun refreshButtonsVisibility() { + minimizeButton?.isVisible = menuListener?.isMinimizedButtonEnabled() == true + dragButton?.isVisible = menuListener?.isDragButtonEnabled() == true + } + private fun updateAlwaysOnTop(enabled: Boolean) { alwaysOnTopChecked = enabled PreferenceManager.getDefaultSharedPreferences(context).edit { diff --git a/platform/android/java/editor/src/main/res/drawable/drag_pan_24px.xml b/platform/android/java/editor/src/main/res/drawable/drag_pan_24px.xml new file mode 100644 index 00000000000..52ba6706a2e --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/drag_pan_24px.xml @@ -0,0 +1,10 @@ + + + diff --git a/platform/android/java/editor/src/main/res/drawable/resize_handle.xml b/platform/android/java/editor/src/main/res/drawable/resize_handle.xml new file mode 100644 index 00000000000..555564facc3 --- /dev/null +++ b/platform/android/java/editor/src/main/res/drawable/resize_handle.xml @@ -0,0 +1,13 @@ + + + + diff --git a/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml b/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml index 5103fd46be5..f37251505e1 100644 --- a/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml +++ b/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml @@ -181,6 +181,15 @@ android:src="@drawable/baseline_expand_less_24" /> + + + + + + + + + + + + + + + + + + + diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index bbd2f1425bd..a383b2d6d0b 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ Tap on \'Game\' to resume Restart game to embed Restart Game to disable embedding + Lock Aspect ratio (%d x %d) diff --git a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt index 6eec5f78877..14a27f13556 100644 --- a/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/main/java/org/godotengine/godot/GodotActivity.kt @@ -71,7 +71,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPicturePr // This window must not match those in BaseGodotEditor.RUN_GAME_INFO etc @JvmStatic - private final val DEFAULT_WINDOW_ID = 664; + private val DEFAULT_WINDOW_ID = 664 } /** @@ -81,6 +81,12 @@ abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPicturePr private val autoEnterPiP = AtomicBoolean(false) private val gameViewSourceRectHint = Rect() private val commandLineParams = ArrayList() + + // The bounds of what the aspect ratio can be are between 2.39:1 and 1:2.39 (inclusive). + // If aspect ratio does not fall between these values, app will crash. + private val minPiPRatio = Rational(100, 239) + private val maxPiPRatio = Rational(239, 100) + /** * Interaction with the [Godot] object is delegated to the [GodotFragment] class. */ @@ -321,21 +327,30 @@ abstract class GodotActivity : FragmentActivity(), GodotHost, PictureInPicturePr */ protected open fun isPiPEnabled() = false - internal fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) { + fun updatePiPParams(enableAutoEnter: Boolean = autoEnterPiP.get(), aspectRatio: Rational? = pipAspectRatio.get()) { + val fixedAspectRatio = aspectRatio?.let { + if (it < minPiPRatio || it > maxPiPRatio) { + Log.w(TAG, "The bounds of the aspect ratio must be between 2.39:1 and 1:2.39 (inclusive). Coercing to valid range.") + it.coerceIn(minPiPRatio, maxPiPRatio) + } else { + it + } + } + if (isPiPModeSupported()) { autoEnterPiP.set(enableAutoEnter) - pipAspectRatio.set(aspectRatio) + pipAspectRatio.set(fixedAspectRatio) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val builder = PictureInPictureParams.Builder() .setSourceRectHint(gameViewSourceRectHint) - .setAspectRatio(aspectRatio) + .setAspectRatio(fixedAspectRatio) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { builder.setSeamlessResizeEnabled(false) .setAutoEnterEnabled(enableAutoEnter) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - builder.setExpandedAspectRatio(aspectRatio) + builder.setExpandedAspectRatio(fixedAspectRatio) } setPictureInPictureParams(builder.build()) }