diff --git a/platform/android/doc_classes/EditorExportPlatformAndroid.xml b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
index 66ec7c042ec..62ee39fbb98 100644
--- a/platform/android/doc_classes/EditorExportPlatformAndroid.xml
+++ b/platform/android/doc_classes/EditorExportPlatformAndroid.xml
@@ -52,6 +52,12 @@
Path to a ZIP file holding the source for the export template used in a Gradle build. If left empty, the default template is used.
+
+ A dictionary of custom theme attributes to include in the exported Android project. Each entry defines a theme attribute name and its value, and will be added to the [b]GodotAppMainTheme[/b].
+ For example, the key [code]android:windowSwipeToDismiss[/code] with the value [code]false[/code] is resolved to [code]<item name="android:windowSwipeToDismiss">false</item>[/code].
+ [b]Note:[/b] To add a custom attribute to the [b]GodotAppSplashTheme[/b], prefix the attribute name with [code][splash][/code].
+ [b]Note:[/b] Reserved attributes configured via other export options or project settings cannot be overridden by [code]custom_theme_attributes[/code] and are skipped during export.
+
Application export format (*.apk or *.aab).
diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp
index 7aef7e1269c..e0ac999bc18 100644
--- a/platform/android/export/export_plugin.cpp
+++ b/platform/android/export/export_plugin.cpp
@@ -1039,52 +1039,100 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref &p_preset) {
const String themes_xml_path = ExportTemplateManager::get_android_build_directory(p_preset).path_join("res/values/themes.xml");
- bool enable_swipe_to_dismiss = p_preset->get("gesture/swipe_to_dismiss");
if (!FileAccess::exists(themes_xml_path)) {
print_error("res/values/themes.xml does not exist.");
return;
}
- String xml_content;
- Ref file = FileAccess::open(themes_xml_path, FileAccess::READ);
- PackedStringArray lines = file->get_as_text().split("\n");
- file->close();
+ // 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"));
- // Check if the themes.xml already contains - element.
- // If found, update its value based on `enable_swipe_to_dismiss`.
- bool found = false;
- bool modified = false;
- for (int i = 0; i < lines.size(); i++) {
- String line = lines[i];
- if (line.contains("
- ")) {
- lines.set(i, vformat("
- %s
", bool_to_string(enable_swipe_to_dismiss)));
- found = true;
- modified = true;
- break;
- }
- }
+ Dictionary splash_theme_attributes;
+ splash_theme_attributes["android:windowSplashScreenBackground"] = "@mipmap/icon_background";
+ splash_theme_attributes["windowSplashScreenAnimatedIcon"] = "@mipmap/icon_foreground";
+ splash_theme_attributes["postSplashScreenTheme"] = "@style/GodotAppMainTheme";
- // If - is not found and `enable_swipe_to_dismiss` is false:
- // Add a new
- element before the closing tag.
- if (!found && !enable_swipe_to_dismiss) {
- for (int i = 0; i < lines.size(); i++) {
- if (lines[i].contains("")) {
- lines.insert(i, "
- false
");
- modified = true;
- break;
+ Dictionary custom_theme_attributes = p_preset->get("gradle_build/custom_theme_attributes");
+
+ // Does not override default/reserved theme attributes; skips any duplicates from custom_theme_attributes.
+ for (const Variant &k : custom_theme_attributes.keys()) {
+ String key = k;
+ String value = custom_theme_attributes[k];
+ if (key.begins_with("[splash]")) {
+ String splash_key = key.trim_prefix("[splash]");
+ if (splash_theme_attributes.has(splash_key)) {
+ WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", splash_key));
+ } else {
+ splash_theme_attributes[splash_key] = value;
+ }
+ } else {
+ if (main_theme_attributes.has(key)) {
+ WARN_PRINT(vformat("Skipped custom_theme_attribute '%s'; this is a reserved attribute configured via other export options or project settings.", key));
+ } else {
+ main_theme_attributes[key] = value;
}
}
}
- // Reconstruct the XML content from the modified lines.
- if (modified) {
- xml_content = String("\n").join(lines);
- store_string_at_path(themes_xml_path, xml_content);
- print_verbose("Successfully modified " + themes_xml_path + ": " + "\n" + xml_content);
- } else {
- print_verbose("No changes needed for " + themes_xml_path);
+ Ref file = FileAccess::open(themes_xml_path, FileAccess::READ);
+ PackedStringArray lines = file->get_as_text().split("\n");
+ file->close();
+
+ PackedStringArray new_lines;
+ bool inside_main_theme = false;
+ bool inside_splash_theme = false;
+
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines[i];
+
+ if (line.contains(" in the end.
+ inside_main_theme = false;
+ continue;
+ }
+
+ // Inject GodotAppSplashTheme attributes.
+ if (inside_splash_theme && line.contains("")) {
+ for (const Variant &attribute : splash_theme_attributes.keys()) {
+ String value = splash_theme_attributes[attribute];
+ String item_line = vformat(" - %s
", attribute, value);
+ new_lines.append(item_line);
+ }
+ new_lines.append(line); // Add in the end.
+ inside_splash_theme = false;
+ continue;
+ }
+
+ // Add all other lines unchanged.
+ if (!inside_main_theme && !inside_splash_theme) {
+ new_lines.append(line);
+ }
}
+
+ // Reconstruct the XML content from the modified lines.
+ String xml_content = String("\n").join(new_lines);
+ store_string_at_path(themes_xml_path, xml_content);
+ print_verbose("Successfully modified " + themes_xml_path + ": " + "\n" + xml_content);
}
void EditorExportPlatformAndroid::_fix_manifest(const Ref &p_preset, Vector &p_manifest, bool p_give_internet) {
@@ -1994,6 +2042,11 @@ String EditorExportPlatformAndroid::get_export_option_warning(const EditorExport
}
}
}
+ } else if (p_name == "gradle_build/custom_theme_attributes") {
+ bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
+ if (bool(p_preset->get("gradle_build/custom_theme_attributes")) && !gradle_build_enabled) {
+ return TTR("\"Use Gradle Build\" is required to add custom theme attributes.");
+ }
} else if (p_name == "package/show_in_android_tv") {
bool gradle_build_enabled = p_preset->get("gradle_build/use_gradle_build");
if (bool(p_preset->get("package/show_in_android_tv")) && !gradle_build_enabled) {
@@ -2027,6 +2080,8 @@ void EditorExportPlatformAndroid::get_export_options(List *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/min_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_MIN_SDK_VERSION)), "", false, true));
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "gradle_build/target_sdk", PROPERTY_HINT_PLACEHOLDER_TEXT, vformat("%d (default)", DEFAULT_TARGET_SDK_VERSION)), "", false, true));
+ r_options->push_back(ExportOption(PropertyInfo(Variant::DICTIONARY, "gradle_build/custom_theme_attributes", PROPERTY_HINT_DICTIONARY_TYPE, "String;String"), Dictionary()));
+
#ifndef DISABLE_DEPRECATED
Vector plugins_configs = get_plugins();
for (int i = 0; i < plugins_configs.size(); i++) {
@@ -2108,6 +2163,7 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor
bool advanced_options_enabled = p_preset->are_advanced_options_enabled();
if (p_option == "graphics/opengl_debug" ||
+ p_option == "gradle_build/custom_theme_attributes" ||
p_option == "command_line/extra_args" ||
p_option == "permissions/custom_permissions" ||
p_option == "keystore/debug" ||
diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml
index c97054233cb..53e3516154d 100644
--- a/platform/android/java/app/res/values/themes.xml
+++ b/platform/android/java/app/res/values/themes.xml
@@ -1,22 +1,17 @@
-
+
+