1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-04 12:00:25 +00:00

Address API 35 UI behavior changes

- Fix issue on foldable where the embedded window would obscure the main window when launching
- Fix edge-to-edge support for non-immersive apps / games
- Add edge-to-edge export option to allow non-immersive apps / games to extend edge to edge
This commit is contained in:
Fredia Huya-Kouadio
2025-06-19 16:01:06 -07:00
parent 5abed52fd9
commit 2f4c3d411c
9 changed files with 97 additions and 18 deletions

View File

@@ -598,6 +598,10 @@
<member name="permissions/write_user_dictionary" type="bool" setter="" getter=""> <member name="permissions/write_user_dictionary" type="bool" setter="" getter="">
Allows an application to write to the user dictionary. Allows an application to write to the user dictionary.
</member> </member>
<member name="screen/edge_to_edge" type="bool" setter="" getter="">
If [code]true[/code], this makes the navigation and status bars translucent and allows the application content to extend edge to edge.
[b]Note:[/b] You should ensure that none of the application content is occluded by system elements by using the [method DisplayServer.get_display_safe_area] and [method DisplayServer.get_display_cutouts] methods.
</member>
<member name="screen/immersive_mode" type="bool" setter="" getter=""> <member name="screen/immersive_mode" type="bool" setter="" getter="">
If [code]true[/code], hides the navigation and status bar. Set [method DisplayServer.window_set_mode] to change this at runtime. If [code]true[/code], hides the navigation and status bar. Set [method DisplayServer.window_set_mode] to change this at runtime.
</member> </member>

View File

@@ -1054,7 +1054,6 @@ void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset>
// Default/Reserved theme attributes. // Default/Reserved theme attributes.
Dictionary main_theme_attributes; Dictionary main_theme_attributes;
main_theme_attributes["android:windowDrawsSystemBarBackgrounds"] = "false";
main_theme_attributes["android:windowSwipeToDismiss"] = bool_to_string(p_preset->get("gesture/swipe_to_dismiss")); main_theme_attributes["android:windowSwipeToDismiss"] = bool_to_string(p_preset->get("gesture/swipe_to_dismiss"));
main_theme_attributes["android:windowIsTranslucent"] = bool_to_string(should_be_transparent); main_theme_attributes["android:windowIsTranslucent"] = bool_to_string(should_be_transparent);
if (should_be_transparent) { if (should_be_transparent) {
@@ -2163,6 +2162,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/edge_to_edge"), false));
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false)); r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));
@@ -3078,6 +3078,11 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
command_line_strings.push_back("--fullscreen"); command_line_strings.push_back("--fullscreen");
} }
bool edge_to_edge = p_preset->get("screen/edge_to_edge");
if (edge_to_edge) {
command_line_strings.push_back("--edge_to_edge");
}
bool debug_opengl = p_preset->get("graphics/opengl_debug"); bool debug_opengl = p_preset->get("graphics/opengl_debug");
if (debug_opengl) { if (debug_opengl) {
command_line_strings.push_back("--debug_opengl"); command_line_strings.push_back("--debug_opengl");

View File

@@ -3,7 +3,6 @@
<!-- GodotAppMainTheme is auto-generated during export. Manual changes will be overwritten. <!-- GodotAppMainTheme is auto-generated during export. Manual changes will be overwritten.
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. --> To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar"> <style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowSwipeToDismiss">false</item> <item name="android:windowSwipeToDismiss">false</item>
<item name="android:windowIsTranslucent">false</item> <item name="android:windowIsTranslucent">false</item>
</style> </style>

View File

@@ -30,11 +30,13 @@
package com.godot.game; package com.godot.game;
import org.godotengine.godot.Godot;
import org.godotengine.godot.GodotActivity; import org.godotengine.godot.GodotActivity;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import androidx.activity.EdgeToEdge;
import androidx.core.splashscreen.SplashScreen; import androidx.core.splashscreen.SplashScreen;
/** /**
@@ -54,9 +56,30 @@ public class GodotApp extends GodotActivity {
} }
} }
private final Runnable updateImmersiveAndEdgeToEdgeModes = () -> {
Godot godot = getGodot();
if (godot != null) {
godot.enableImmersiveMode(godot.isInImmersiveMode(), true);
godot.enableEdgeToEdge(godot.isInEdgeToEdgeMode(), true);
}
};
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
SplashScreen.installSplashScreen(this); SplashScreen.installSplashScreen(this);
EdgeToEdge.enable(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
@Override
public void onResume() {
super.onResume();
updateImmersiveAndEdgeToEdgeModes.run();
}
@Override
public void onGodotMainLoopStarted() {
super.onGodotMainLoopStarted();
runOnUiThread(updateImmersiveAndEdgeToEdgeModes);
}
} }

View File

@@ -88,8 +88,7 @@
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:process=":EmbeddedGodotGame" android:process=":EmbeddedGodotGame"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true" />
android:screenOrientation="userLandscape" />
<activity <activity
android:name=".GodotXRGame" android:name=".GodotXRGame"
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"

View File

@@ -45,9 +45,9 @@ import android.os.Process
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -215,9 +215,9 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
// Prevent the editor window from showing in the display cutout val editorWindowInfo = getEditorWindowInfo()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) { if (editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) {
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER enableEdgeToEdge()
} }
// We exclude certain permissions from the set we request at startup, as they'll be // We exclude certain permissions from the set we request at startup, as they'll be
@@ -273,16 +273,29 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
} }
} }
private fun updateImmersiveAndEdgeToEdgeModes() {
val editorWindowInfo = getEditorWindowInfo()
if (editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) {
godot?.apply {
enableImmersiveMode(isInImmersiveMode(), true)
enableEdgeToEdge(isInEdgeToEdgeMode(), true)
}
}
}
override fun onGodotMainLoopStarted() { override fun onGodotMainLoopStarted() {
super.onGodotMainLoopStarted() super.onGodotMainLoopStarted()
runOnUiThread { runOnUiThread {
// Hide the loading indicator // Hide the loading indicator
editorLoadingIndicator?.visibility = View.GONE editorLoadingIndicator?.visibility = View.GONE
updateImmersiveAndEdgeToEdgeModes()
} }
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateImmersiveAndEdgeToEdgeModes()
if (getEditorWindowInfo() == EDITOR_MAIN_INFO && if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
godot?.isEditorHint() == true && godot?.isEditorHint() == true &&
(editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) || (editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) ||
@@ -365,7 +378,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
// fullscreen mode, we want to remain in fullscreen mode. // fullscreen mode, we want to remain in fullscreen mode.
// This doesn't apply to the play / game window since for that window fullscreen is // This doesn't apply to the play / game window since for that window fullscreen is
// controlled by the game logic. // controlled by the game logic.
val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO && val updatedArgs = if ((editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) &&
godot?.isInImmersiveMode() == true && godot?.isInImmersiveMode() == true &&
!args.contains(FULLSCREEN_ARG) && !args.contains(FULLSCREEN_ARG) &&
!args.contains(FULLSCREEN_ARG_SHORT) !args.contains(FULLSCREEN_ARG_SHORT)

View File

@@ -87,8 +87,8 @@ class EmbeddedGodotGame : GodotGame() {
override fun setRequestedOrientation(requestedOrientation: Int) { override fun setRequestedOrientation(requestedOrientation: Int) {
// Allow orientation change only if fullscreen mode is active // Allow orientation change only if fullscreen mode is active
// or if the requestedOrientation is landscape (i.e switching to default). // or if the requestedOrientation is unspecified (i.e switching to default).
if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE) { if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
super.setRequestedOrientation(requestedOrientation) super.setRequestedOrientation(requestedOrientation)
} else { } else {
// Cache the requestedOrientation to apply when switching to fullscreen. // Cache the requestedOrientation to apply when switching to fullscreen.
@@ -155,7 +155,7 @@ class EmbeddedGodotGame : GodotGame() {
// Cache the last used orientation in fullscreen to reapply when re-entering fullscreen. // Cache the last used orientation in fullscreen to reapply when re-entering fullscreen.
gameRequestedOrientation = requestedOrientation gameRequestedOrientation = requestedOrientation
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} }
updateWindowDimensions(layoutWidthInPx, layoutHeightInPx) updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
} }

View File

@@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="GodotEditorTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"> <style name="GodotEditorTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
</style> </style>
<style name="GodotGameTheme" parent="GodotEditorTheme"> <style name="GodotGameTheme" parent="GodotEditorTheme">

View File

@@ -172,6 +172,7 @@ class Godot private constructor(val context: Context) {
private var commandLine : MutableList<String> = ArrayList<String>() private var commandLine : MutableList<String> = ArrayList<String>()
private var xrMode = XRMode.REGULAR private var xrMode = XRMode.REGULAR
private val useImmersive = AtomicBoolean(false) private val useImmersive = AtomicBoolean(false)
private val isEdgeToEdge = AtomicBoolean(false)
private var useDebugOpengl = false private var useDebugOpengl = false
private var darkMode = false private var darkMode = false
@@ -235,6 +236,8 @@ class Godot private constructor(val context: Context) {
xrMode = XRMode.OPENXR xrMode = XRMode.OPENXR
} else if (commandLine[i] == "--debug_opengl") { } else if (commandLine[i] == "--debug_opengl") {
useDebugOpengl = true useDebugOpengl = true
} else if (commandLine[i] == "--edge_to_edge") {
isEdgeToEdge.set(true)
} else if (commandLine[i] == "--fullscreen") { } else if (commandLine[i] == "--fullscreen") {
useImmersive.set(true) useImmersive.set(true)
newArgs.add(commandLine[i]) newArgs.add(commandLine[i])
@@ -332,10 +335,45 @@ class Godot private constructor(val context: Context) {
return isNativeInitialized() return isNativeInitialized()
} }
/**
* Enable edge-to-edge.
*
* Must be called from the UI thread.
*/
@JvmOverloads
fun enableEdgeToEdge(enabled: Boolean, override: Boolean = false) {
val window = getActivity()?.window ?: return
if (!isEdgeToEdge.compareAndSet(!enabled, enabled) && !override) {
return
}
val rootView = window.decorView
WindowCompat.setDecorFitsSystemWindows(window, !(isEdgeToEdge.get() || useImmersive.get()))
if (enabled) {
ViewCompat.setOnApplyWindowInsetsListener(rootView, null)
rootView.setPadding(0, 0, 0, 0)
} else {
val insetType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
if (rootView.rootWindowInsets != null) {
val windowInsets = WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets)
val insets = windowInsets.getInsets(insetType)
rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom)
}
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v: View, insets: WindowInsetsCompat ->
val windowInsets = insets.getInsets(insetType)
v.setPadding(windowInsets.left, windowInsets.top, windowInsets.right, windowInsets.bottom)
WindowInsetsCompat.CONSUMED
}
}
}
/** /**
* Toggle immersive mode. * Toggle immersive mode.
* Must be called from the UI thread. * Must be called from the UI thread.
*/ */
@JvmOverloads
fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) { fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
val activity = getActivity() ?: return val activity = getActivity() ?: return
val window = activity.window ?: return val window = activity.window ?: return
@@ -344,7 +382,7 @@ class Godot private constructor(val context: Context) {
return return
} }
WindowCompat.setDecorFitsSystemWindows(window, !enabled) WindowCompat.setDecorFitsSystemWindows(window, !(isEdgeToEdge.get() || useImmersive.get()))
val controller = WindowInsetsControllerCompat(window, window.decorView) val controller = WindowInsetsControllerCompat(window, window.decorView)
if (enabled) { if (enabled) {
controller.hide(WindowInsetsCompat.Type.systemBars()) controller.hide(WindowInsetsCompat.Type.systemBars())
@@ -380,6 +418,9 @@ class Godot private constructor(val context: Context) {
@Keep @Keep
fun isInImmersiveMode() = useImmersive.get() fun isInImmersiveMode() = useImmersive.get()
@Keep
fun isInEdgeToEdgeMode() = isEdgeToEdge.get()
/** /**
* Used to complete initialization of the view used by the engine for rendering. * Used to complete initialization of the view used by the engine for rendering.
* *
@@ -551,7 +592,6 @@ class Godot private constructor(val context: Context) {
renderView?.onActivityResumed() renderView?.onActivityResumed()
registerSensorsIfNeeded() registerSensorsIfNeeded()
enableImmersiveMode(useImmersive.get(), true)
for (plugin in pluginRegistry.allPlugins) { for (plugin in pluginRegistry.allPlugins) {
plugin.onMainResume() plugin.onMainResume()
} }
@@ -704,7 +744,6 @@ class Godot private constructor(val context: Context) {
runOnHostThread { runOnHostThread {
registerSensorsIfNeeded() registerSensorsIfNeeded()
enableImmersiveMode(useImmersive.get(), true)
} }
for (plugin in pluginRegistry.allPlugins) { for (plugin in pluginRegistry.allPlugins) {