Merge pull request #118417 from syntaxerror247/resize-and-move
Allow moving and resizing the embedded game window on Android
This commit is contained in:
+313
-37
@@ -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<View>()
|
||||
|
||||
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) {
|
||||
|
||||
+25
-3
@@ -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<View>(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 {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M480,880L310,710L367,653L440,726L440,520L235,520L308,592L250,650L80,480L249,311L306,368L234,440L440,440L440,234L367,307L310,250L480,80L650,250L593,307L520,234L520,440L725,440L652,368L710,310L880,480L710,650L653,593L726,520L520,520L520,725L592,652L650,710L480,880Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:pathData="M20,20 L20,4 M20,20 L4,20"
|
||||
android:strokeColor="#007AFF"
|
||||
android:strokeWidth="4"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"/>
|
||||
</vector>
|
||||
@@ -181,6 +181,15 @@
|
||||
android:src="@drawable/baseline_expand_less_24"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_drag_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_selected_button_bg"
|
||||
android:src="@drawable/drag_pan_24px"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_minimize_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/game_menu_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/godot_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/game_menu_fragment_container"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/lockAspectRatioCheckBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="60dp"
|
||||
android:background="#CC000000"
|
||||
android:padding="8dp"
|
||||
android:text="Lock Aspect Ratio"
|
||||
android:textColor="#FFFFFF"
|
||||
android:buttonTint="#FFFFFF"
|
||||
android:checked="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleTopLeft"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="180"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleTopRight"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="270"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleBottomLeft"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:rotation="90"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:id="@+id/handleBottomRight"
|
||||
android:layout_width="80px"
|
||||
android:layout_height="80px"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:background="@drawable/resize_handle"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -20,4 +20,5 @@
|
||||
<string name="show_game_resume_hint">Tap on \'Game\' to resume</string>
|
||||
<string name="restart_embed_game_hint">Restart game to embed</string>
|
||||
<string name="restart_non_embedded_game_hint">Restart Game to disable embedding</string>
|
||||
<string name="lock_aspect_ratio_btn_text">Lock Aspect ratio (%d x %d)</string>
|
||||
</resources>
|
||||
|
||||
@@ -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<String>()
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user