Skip to content

Commit 2f4c3d4

Browse files
committed
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
1 parent 5abed52 commit 2f4c3d4

File tree

9 files changed

+97
-18
lines changed

9 files changed

+97
-18
lines changed

platform/android/doc_classes/EditorExportPlatformAndroid.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,10 @@
598598
<member name="permissions/write_user_dictionary" type="bool" setter="" getter="">
599599
Allows an application to write to the user dictionary.
600600
</member>
601+
<member name="screen/edge_to_edge" type="bool" setter="" getter="">
602+
If [code]true[/code], this makes the navigation and status bars translucent and allows the application content to extend edge to edge.
603+
[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.
604+
</member>
601605
<member name="screen/immersive_mode" type="bool" setter="" getter="">
602606
If [code]true[/code], hides the navigation and status bar. Set [method DisplayServer.window_set_mode] to change this at runtime.
603607
</member>

platform/android/export/export_plugin.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,7 +1054,6 @@ void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset>
10541054

10551055
// Default/Reserved theme attributes.
10561056
Dictionary main_theme_attributes;
1057-
main_theme_attributes["android:windowDrawsSystemBarBackgrounds"] = "false";
10581057
main_theme_attributes["android:windowSwipeToDismiss"] = bool_to_string(p_preset->get("gesture/swipe_to_dismiss"));
10591058
main_theme_attributes["android:windowIsTranslucent"] = bool_to_string(should_be_transparent);
10601059
if (should_be_transparent) {
@@ -2163,6 +2162,7 @@ void EditorExportPlatformAndroid::get_export_options(List<ExportOption> *r_optio
21632162
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_normal"), true));
21642163
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_large"), true));
21652164
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/support_xlarge"), true));
2165+
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "screen/edge_to_edge"), false));
21662166

21672167
r_options->push_back(ExportOption(PropertyInfo(Variant::BOOL, "user_data_backup/allow"), false));
21682168

@@ -3078,6 +3078,11 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
30783078
command_line_strings.push_back("--fullscreen");
30793079
}
30803080

3081+
bool edge_to_edge = p_preset->get("screen/edge_to_edge");
3082+
if (edge_to_edge) {
3083+
command_line_strings.push_back("--edge_to_edge");
3084+
}
3085+
30813086
bool debug_opengl = p_preset->get("graphics/opengl_debug");
30823087
if (debug_opengl) {
30833088
command_line_strings.push_back("--debug_opengl");

platform/android/java/app/res/values/themes.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<!-- GodotAppMainTheme is auto-generated during export. Manual changes will be overwritten.
44
To add custom attributes, use the "gradle_build/custom_theme_attributes" Android export option. -->
55
<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
6-
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
76
<item name="android:windowSwipeToDismiss">false</item>
87
<item name="android:windowIsTranslucent">false</item>
98
</style>

platform/android/java/app/src/com/godot/game/GodotApp.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030

3131
package com.godot.game;
3232

33+
import org.godotengine.godot.Godot;
3334
import org.godotengine.godot.GodotActivity;
3435

3536
import android.os.Bundle;
3637
import android.util.Log;
3738

39+
import androidx.activity.EdgeToEdge;
3840
import androidx.core.splashscreen.SplashScreen;
3941

4042
/**
@@ -54,9 +56,30 @@ public class GodotApp extends GodotActivity {
5456
}
5557
}
5658

59+
private final Runnable updateImmersiveAndEdgeToEdgeModes = () -> {
60+
Godot godot = getGodot();
61+
if (godot != null) {
62+
godot.enableImmersiveMode(godot.isInImmersiveMode(), true);
63+
godot.enableEdgeToEdge(godot.isInEdgeToEdgeMode(), true);
64+
}
65+
};
66+
5767
@Override
5868
public void onCreate(Bundle savedInstanceState) {
5969
SplashScreen.installSplashScreen(this);
70+
EdgeToEdge.enable(this);
6071
super.onCreate(savedInstanceState);
6172
}
73+
74+
@Override
75+
public void onResume() {
76+
super.onResume();
77+
updateImmersiveAndEdgeToEdgeModes.run();
78+
}
79+
80+
@Override
81+
public void onGodotMainLoopStarted() {
82+
super.onGodotMainLoopStarted();
83+
runOnUiThread(updateImmersiveAndEdgeToEdgeModes);
84+
}
6285
}

platform/android/java/editor/src/main/AndroidManifest.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@
8888
android:excludeFromRecents="true"
8989
android:launchMode="singleTask"
9090
android:process=":EmbeddedGodotGame"
91-
android:supportsPictureInPicture="true"
92-
android:screenOrientation="userLandscape" />
91+
android:supportsPictureInPicture="true" />
9392
<activity
9493
android:name=".GodotXRGame"
9594
android:configChanges="layoutDirection|locale|orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"

platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ import android.os.Process
4545
import android.preference.PreferenceManager
4646
import android.util.Log
4747
import android.view.View
48-
import android.view.WindowManager
4948
import android.widget.TextView
5049
import android.widget.Toast
50+
import androidx.activity.enableEdgeToEdge
5151
import androidx.annotation.CallSuper
5252
import androidx.core.content.edit
5353
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -215,9 +215,9 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
215215
override fun onCreate(savedInstanceState: Bundle?) {
216216
installSplashScreen()
217217

218-
// Prevent the editor window from showing in the display cutout
219-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) {
220-
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
218+
val editorWindowInfo = getEditorWindowInfo()
219+
if (editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) {
220+
enableEdgeToEdge()
221221
}
222222

223223
// 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
273273
}
274274
}
275275

276+
private fun updateImmersiveAndEdgeToEdgeModes() {
277+
val editorWindowInfo = getEditorWindowInfo()
278+
if (editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) {
279+
godot?.apply {
280+
enableImmersiveMode(isInImmersiveMode(), true)
281+
enableEdgeToEdge(isInEdgeToEdgeMode(), true)
282+
}
283+
}
284+
}
285+
276286
override fun onGodotMainLoopStarted() {
277287
super.onGodotMainLoopStarted()
278288
runOnUiThread {
279289
// Hide the loading indicator
280290
editorLoadingIndicator?.visibility = View.GONE
291+
updateImmersiveAndEdgeToEdgeModes()
281292
}
282293
}
283294

284295
override fun onResume() {
285296
super.onResume()
297+
updateImmersiveAndEdgeToEdgeModes()
298+
286299
if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
287300
godot?.isEditorHint() == true &&
288301
(editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) ||
@@ -365,7 +378,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
365378
// fullscreen mode, we want to remain in fullscreen mode.
366379
// This doesn't apply to the play / game window since for that window fullscreen is
367380
// controlled by the game logic.
368-
val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO &&
381+
val updatedArgs = if ((editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) &&
369382
godot?.isInImmersiveMode() == true &&
370383
!args.contains(FULLSCREEN_ARG) &&
371384
!args.contains(FULLSCREEN_ARG_SHORT)

platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ class EmbeddedGodotGame : GodotGame() {
8787

8888
override fun setRequestedOrientation(requestedOrientation: Int) {
8989
// Allow orientation change only if fullscreen mode is active
90-
// or if the requestedOrientation is landscape (i.e switching to default).
91-
if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE) {
90+
// or if the requestedOrientation is unspecified (i.e switching to default).
91+
if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
9292
super.setRequestedOrientation(requestedOrientation)
9393
} else {
9494
// Cache the requestedOrientation to apply when switching to fullscreen.
@@ -155,7 +155,7 @@ class EmbeddedGodotGame : GodotGame() {
155155

156156
// Cache the last used orientation in fullscreen to reapply when re-entering fullscreen.
157157
gameRequestedOrientation = requestedOrientation
158-
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
158+
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
159159
}
160160
updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
161161
}

platform/android/java/editor/src/main/res/values/themes.xml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<style name="GodotEditorTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen">
4-
<item name="android:statusBarColor">@android:color/transparent</item>
5-
<item name="android:navigationBarColor">@android:color/transparent</item>
6-
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
74
</style>
85

96
<style name="GodotGameTheme" parent="GodotEditorTheme">

platform/android/java/lib/src/org/godotengine/godot/Godot.kt

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class Godot private constructor(val context: Context) {
172172
private var commandLine : MutableList<String> = ArrayList<String>()
173173
private var xrMode = XRMode.REGULAR
174174
private val useImmersive = AtomicBoolean(false)
175+
private val isEdgeToEdge = AtomicBoolean(false)
175176
private var useDebugOpengl = false
176177
private var darkMode = false
177178

@@ -235,6 +236,8 @@ class Godot private constructor(val context: Context) {
235236
xrMode = XRMode.OPENXR
236237
} else if (commandLine[i] == "--debug_opengl") {
237238
useDebugOpengl = true
239+
} else if (commandLine[i] == "--edge_to_edge") {
240+
isEdgeToEdge.set(true)
238241
} else if (commandLine[i] == "--fullscreen") {
239242
useImmersive.set(true)
240243
newArgs.add(commandLine[i])
@@ -332,10 +335,45 @@ class Godot private constructor(val context: Context) {
332335
return isNativeInitialized()
333336
}
334337

338+
/**
339+
* Enable edge-to-edge.
340+
*
341+
* Must be called from the UI thread.
342+
*/
343+
@JvmOverloads
344+
fun enableEdgeToEdge(enabled: Boolean, override: Boolean = false) {
345+
val window = getActivity()?.window ?: return
346+
347+
if (!isEdgeToEdge.compareAndSet(!enabled, enabled) && !override) {
348+
return
349+
}
350+
351+
val rootView = window.decorView
352+
WindowCompat.setDecorFitsSystemWindows(window, !(isEdgeToEdge.get() || useImmersive.get()))
353+
if (enabled) {
354+
ViewCompat.setOnApplyWindowInsetsListener(rootView, null)
355+
rootView.setPadding(0, 0, 0, 0)
356+
} else {
357+
val insetType = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
358+
if (rootView.rootWindowInsets != null) {
359+
val windowInsets = WindowInsetsCompat.toWindowInsetsCompat(rootView.rootWindowInsets)
360+
val insets = windowInsets.getInsets(insetType)
361+
rootView.setPadding(insets.left, insets.top, insets.right, insets.bottom)
362+
}
363+
364+
ViewCompat.setOnApplyWindowInsetsListener(rootView) { v: View, insets: WindowInsetsCompat ->
365+
val windowInsets = insets.getInsets(insetType)
366+
v.setPadding(windowInsets.left, windowInsets.top, windowInsets.right, windowInsets.bottom)
367+
WindowInsetsCompat.CONSUMED
368+
}
369+
}
370+
}
371+
335372
/**
336373
* Toggle immersive mode.
337374
* Must be called from the UI thread.
338375
*/
376+
@JvmOverloads
339377
fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
340378
val activity = getActivity() ?: return
341379
val window = activity.window ?: return
@@ -344,7 +382,7 @@ class Godot private constructor(val context: Context) {
344382
return
345383
}
346384

347-
WindowCompat.setDecorFitsSystemWindows(window, !enabled)
385+
WindowCompat.setDecorFitsSystemWindows(window, !(isEdgeToEdge.get() || useImmersive.get()))
348386
val controller = WindowInsetsControllerCompat(window, window.decorView)
349387
if (enabled) {
350388
controller.hide(WindowInsetsCompat.Type.systemBars())
@@ -380,6 +418,9 @@ class Godot private constructor(val context: Context) {
380418
@Keep
381419
fun isInImmersiveMode() = useImmersive.get()
382420

421+
@Keep
422+
fun isInEdgeToEdgeMode() = isEdgeToEdge.get()
423+
383424
/**
384425
* Used to complete initialization of the view used by the engine for rendering.
385426
*
@@ -551,7 +592,6 @@ class Godot private constructor(val context: Context) {
551592

552593
renderView?.onActivityResumed()
553594
registerSensorsIfNeeded()
554-
enableImmersiveMode(useImmersive.get(), true)
555595
for (plugin in pluginRegistry.allPlugins) {
556596
plugin.onMainResume()
557597
}
@@ -704,7 +744,6 @@ class Godot private constructor(val context: Context) {
704744

705745
runOnHostThread {
706746
registerSensorsIfNeeded()
707-
enableImmersiveMode(useImmersive.get(), true)
708747
}
709748

710749
for (plugin in pluginRegistry.allPlugins) {

0 commit comments

Comments
 (0)