Răsfoiți Sursa

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
Fredia Huya-Kouadio 3 luni în urmă
părinte
comite
2f4c3d411c

+ 4 - 0
platform/android/doc_classes/EditorExportPlatformAndroid.xml

@@ -598,6 +598,10 @@
 		<member name="permissions/write_user_dictionary" type="bool" setter="" getter="">
 			Allows an application to write to the user dictionary.
 		</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="">
 			If [code]true[/code], hides the navigation and status bar. Set [method DisplayServer.window_set_mode] to change this at runtime.
 		</member>

+ 6 - 1
platform/android/export/export_plugin.cpp

@@ -1054,7 +1054,6 @@ void EditorExportPlatformAndroid::_fix_themes_xml(const Ref<EditorExportPreset>
 
 	// Default/Reserved 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:windowIsTranslucent"] = bool_to_string(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_large"), 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));
 
@@ -3078,6 +3078,11 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
 		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");
 	if (debug_opengl) {
 		command_line_strings.push_back("--debug_opengl");

+ 0 - 1
platform/android/java/app/res/values/themes.xml

@@ -3,7 +3,6 @@
 	<!-- 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. -->
 	<style name="GodotAppMainTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
-		<item name="android:windowDrawsSystemBarBackgrounds">false</item>
 		<item name="android:windowSwipeToDismiss">false</item>
 		<item name="android:windowIsTranslucent">false</item>
 	</style>

+ 23 - 0
platform/android/java/app/src/com/godot/game/GodotApp.java

@@ -30,11 +30,13 @@
 
 package com.godot.game;
 
+import org.godotengine.godot.Godot;
 import org.godotengine.godot.GodotActivity;
 
 import android.os.Bundle;
 import android.util.Log;
 
+import androidx.activity.EdgeToEdge;
 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
 	public void onCreate(Bundle savedInstanceState) {
 		SplashScreen.installSplashScreen(this);
+		EdgeToEdge.enable(this);
 		super.onCreate(savedInstanceState);
 	}
+
+	@Override
+	public void onResume() {
+		super.onResume();
+		updateImmersiveAndEdgeToEdgeModes.run();
+	}
+
+	@Override
+	public void onGodotMainLoopStarted() {
+		super.onGodotMainLoopStarted();
+		runOnUiThread(updateImmersiveAndEdgeToEdgeModes);
+	}
 }

+ 1 - 2
platform/android/java/editor/src/main/AndroidManifest.xml

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

+ 18 - 5
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt

@@ -45,9 +45,9 @@ import android.os.Process
 import android.preference.PreferenceManager
 import android.util.Log
 import android.view.View
-import android.view.WindowManager
 import android.widget.TextView
 import android.widget.Toast
+import androidx.activity.enableEdgeToEdge
 import androidx.annotation.CallSuper
 import androidx.core.content.edit
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -215,9 +215,9 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 	override fun onCreate(savedInstanceState: Bundle?) {
 		installSplashScreen()
 
-		// Prevent the editor window from showing in the display cutout
-		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) {
-			window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+		val editorWindowInfo = getEditorWindowInfo()
+		if (editorWindowInfo == EDITOR_MAIN_INFO || editorWindowInfo == RUN_GAME_INFO) {
+			enableEdgeToEdge()
 		}
 
 		// 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() {
 		super.onGodotMainLoopStarted()
 		runOnUiThread {
 			// Hide the loading indicator
 			editorLoadingIndicator?.visibility = View.GONE
+			updateImmersiveAndEdgeToEdgeModes()
 		}
 	}
 
 	override fun onResume() {
 		super.onResume()
+		updateImmersiveAndEdgeToEdgeModes()
+
 		if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
 			godot?.isEditorHint() == true &&
 			(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.
 		// This doesn't apply to the play / game window since for that window fullscreen is
 		// 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 &&
 			!args.contains(FULLSCREEN_ARG) &&
 			!args.contains(FULLSCREEN_ARG_SHORT)

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

@@ -87,8 +87,8 @@ class EmbeddedGodotGame : GodotGame() {
 
 	override fun setRequestedOrientation(requestedOrientation: Int) {
 		// Allow orientation change only if fullscreen mode is active
-		// or if the requestedOrientation is landscape (i.e switching to default).
-		if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE) {
+		// or if the requestedOrientation is unspecified (i.e switching to default).
+		if (isFullscreen || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
 			super.setRequestedOrientation(requestedOrientation)
 		} else {
 			// 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.
 			gameRequestedOrientation = requestedOrientation
-			requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+			requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
 		}
 		updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
 	}

+ 0 - 3
platform/android/java/editor/src/main/res/values/themes.xml

@@ -1,9 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 	<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 name="GodotGameTheme" parent="GodotEditorTheme">

+ 42 - 3
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -172,6 +172,7 @@ class Godot private constructor(val context: Context) {
 	private var commandLine : MutableList<String> = ArrayList<String>()
 	private var xrMode = XRMode.REGULAR
 	private val useImmersive = AtomicBoolean(false)
+	private val isEdgeToEdge = AtomicBoolean(false)
 	private var useDebugOpengl = false
 	private var darkMode = false
 
@@ -235,6 +236,8 @@ class Godot private constructor(val context: Context) {
 					xrMode = XRMode.OPENXR
 				} else if (commandLine[i] == "--debug_opengl") {
 					useDebugOpengl = true
+				} else if (commandLine[i] == "--edge_to_edge") {
+					isEdgeToEdge.set(true)
 				} else if (commandLine[i] == "--fullscreen") {
 					useImmersive.set(true)
 					newArgs.add(commandLine[i])
@@ -332,10 +335,45 @@ class Godot private constructor(val context: Context) {
 		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.
 	 * Must be called from the UI thread.
 	 */
+	@JvmOverloads
 	fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
 		val activity = getActivity() ?: return
 		val window = activity.window ?: return
@@ -344,7 +382,7 @@ class Godot private constructor(val context: Context) {
 			return
 		}
 
-		WindowCompat.setDecorFitsSystemWindows(window, !enabled)
+		WindowCompat.setDecorFitsSystemWindows(window, !(isEdgeToEdge.get() || useImmersive.get()))
 		val controller = WindowInsetsControllerCompat(window, window.decorView)
 		if (enabled) {
 			controller.hide(WindowInsetsCompat.Type.systemBars())
@@ -380,6 +418,9 @@ class Godot private constructor(val context: Context) {
 	@Keep
 	fun isInImmersiveMode() = useImmersive.get()
 
+	@Keep
+	fun isInEdgeToEdgeMode() = isEdgeToEdge.get()
+
 	/**
 	 * 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()
 		registerSensorsIfNeeded()
-		enableImmersiveMode(useImmersive.get(), true)
 		for (plugin in pluginRegistry.allPlugins) {
 			plugin.onMainResume()
 		}
@@ -704,7 +744,6 @@ class Godot private constructor(val context: Context) {
 
 		runOnHostThread {
 			registerSensorsIfNeeded()
-			enableImmersiveMode(useImmersive.get(), true)
 		}
 
 		for (plugin in pluginRegistry.allPlugins) {