Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ flutter_native_splash:
# The branding image dimensions must be 800x320 px.
#branding: assets/dart.png
#branding_dark: assets/dart_dark.png

# app theme styles:
# Set to true if your app has its own light/dark setting. Adds the NativeSplashLight and
# NativeSplashDark styles. See "Following your app's light/dark setting" below.
#app_theme_styles: true
```

## 2. Run the package
Expand Down Expand Up @@ -261,6 +266,51 @@ Be aware of the following considerations regarding these elements:

**_PLEASE NOTE:_** The splash screen does not appear when you launch the app from a notification. Apparently this is the intended behavior on Android 12: [core-splashscreen Icon not shown when cold launched from notification](https://issuetracker.google.com/issues/199776339?pli=1).

## Following your app's light/dark setting

The Android 12+ splash screen follows the phone's dark mode. If your app lets people choose light or dark in the app, the splash screen can follow that choice instead:

1. Add `app_theme_styles: true` to the `android_12` section and run the package. This adds two styles, `NativeSplashLight` and `NativeSplashDark`, which keep their colors and images whatever the phone's theme.

2. Whenever the setting changes, pass one of them to [`SplashScreen.setSplashScreenTheme`](https://developer.android.com/reference/android/window/SplashScreen#setSplashScreenTheme(int)), or `Resources.ID_NULL` to follow the phone again. Android remembers the choice, and uses it the next time the app starts. This package has no native code, so add a method channel to your `MainActivity.kt`:

```kotlin
import android.content.res.Resources
import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "splash_theme")
.setMethodCallHandler { call, result ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setSplashScreenTheme(
when (call.arguments) {
"light" -> R.style.NativeSplashLight
"dark" -> R.style.NativeSplashDark
else -> Resources.ID_NULL // follow the phone
}
)
}
result.success(null)
}
}
}
```

and call it from Dart when the user picks a theme:

```dart
const splashTheme = MethodChannel('splash_theme');

await splashTheme.invokeMethod('set', 'dark'); // 'light', 'dark' or 'system'
```

`UiModeManager.setApplicationNightMode` is not enough on its own: some launchers, such as Samsung's One UI launcher, draw the splash screen in the phone's theme anyway when the app is opened from its icon.

# Flavor Support

If you have a project setup that contains multiple flavors or environments, and you created more than one flavor this would be a feature for you.
Expand Down Expand Up @@ -464,7 +514,9 @@ This is caused by an [iOS splash caching bug](https://stackoverflow.com/question

## Can I base light/dark mode on app settings?

No. This package creates a splash screen that is displayed before Flutter is loaded. Because of this, when the splash screen loads, internal app settings are not available to the splash screen. Unfortunately, this means that it is impossible to control light/dark settings of the splash from app settings.
On Android 12 and later, yes: see [Following your app's light/dark setting](#following-your-apps-lightdark-setting).

Elsewhere, no. This package creates a splash screen that is displayed before Flutter is loaded. Because of this, when the splash screen loads, internal app settings are not available to the splash screen. Unfortunately, this means that it is impossible to control light/dark settings of the splash from app settings.

# Notes

Expand Down
4 changes: 4 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ flutter_native_splash:
#color_dark: "#042a49"
#icon_background_color_dark: "#111111"

# Set app_theme_styles to true if your app has its own light/dark setting. See "Following your
# app's light/dark setting" in the README.
#app_theme_styles: true

# The android, ios and web parameters can be used to disable generating a splash screen on a given
# platform.
#android: false
Expand Down
183 changes: 136 additions & 47 deletions lib/android.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ void _createAndroidSplash({
required String? screenOrientation,
String? android12BrandingImagePath,
String? android12DarkBrandingImagePath,
bool android12AppThemeStyles = false,
int? androidMinSdk,
}) {
androidMinSdk ??= 16;
Expand Down Expand Up @@ -235,6 +236,59 @@ void _createAndroidSplash({
);
}

// A drawable with a night variant follows the phone's theme, so when the dark
// image differs, the fixed light and dark styles get copies without one.
final copyImage =
android12AppThemeStyles && android12DarkImagePath != android12ImagePath;
final copyBranding = android12AppThemeStyles &&
android12DarkBrandingImagePath != android12BrandingImagePath;
_applyImageAndroid(
imagePath: copyImage ? android12ImagePath : null,
fileName: 'android12splash_light.png',
);
_applyImageAndroid(
imagePath: copyImage ? android12DarkImagePath : null,
fileName: 'android12splash_dark.png',
);
_applyImageAndroid(
imagePath: copyBranding ? android12BrandingImagePath : null,
android12: true,
fileName: 'android12branding_light.png',
);
_applyImageAndroid(
imagePath: copyBranding ? android12DarkBrandingImagePath : null,
android12: true,
fileName: 'android12branding_dark.png',
);

if (androidV31StylesFile != null) {
String? drawable(String? path, String name, bool copy, String variant) =>
path == null ? null : (copy ? '${name}_$variant' : name);
_applyAppThemeStyles(
file: androidV31StylesFile,
light: android12AppThemeStyles
? (
backgroundColor: android12BackgroundColor,
image: drawable(
android12ImagePath, 'android12splash', copyImage, 'light'),
iconBackgroundColor: android12IconBackgroundColor,
brandingImage: drawable(android12BrandingImagePath,
'android12branding', copyBranding, 'light'),
)
: null,
dark: android12AppThemeStyles
? (
backgroundColor: android12DarkBackgroundColor,
image: drawable(
android12DarkImagePath, 'android12splash', copyImage, 'dark'),
iconBackgroundColor: darkAndroid12IconBackgroundColor,
brandingImage: drawable(android12DarkBrandingImagePath,
'android12branding', copyBranding, 'dark'),
)
: null,
);
}

if (androidStylesFile != null) {
_applyStylesXml(
fullScreen: fullscreen,
Expand Down Expand Up @@ -491,58 +545,93 @@ Future<void> _updateStylesFile({
);

// In Android 12, the color must be set directly in the styles.xml
if (android12BackgroundColor == null) {
_removeElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenBackground',
);
} else {
_replaceElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenBackground',
value: '#$android12BackgroundColor',
);
}
_setAndroid12SplashItems(launchTheme, (
backgroundColor: android12BackgroundColor,
image: android12ImagePath == null ? null : 'android12splash',
iconBackgroundColor: android12IconBackgroundColor,
brandingImage:
android12BrandingImagePath == null ? null : 'android12branding',
));

if (android12BrandingImagePath == null) {
_removeElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenBrandingImage',
);
} else {
_replaceElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenBrandingImage',
value: '@drawable/android12branding',
);
}
stylesFile.writeAsStringSync(
'${stylesDocument.toXmlString(pretty: true, indent: ' ')}\n',
);
}

if (android12ImagePath == null) {
_removeElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenAnimatedIcon',
);
} else {
_replaceElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenAnimatedIcon',
value: '@drawable/android12splash',
);
}
/// The Android 12+ splash of a style: colors as hex without `#`, images as
/// drawable names. A null value removes its item from the style.
typedef _Android12Splash = ({
String? backgroundColor,
String? image,
String? iconBackgroundColor,
String? brandingImage,
});

void _setAndroid12SplashItems(XmlElement style, _Android12Splash splash) {
final items = {
'android:windowSplashScreenBackground':
splash.backgroundColor == null ? null : '#${splash.backgroundColor}',
'android:windowSplashScreenBrandingImage': splash.brandingImage == null
? null
: '@drawable/${splash.brandingImage}',
'android:windowSplashScreenAnimatedIcon':
splash.image == null ? null : '@drawable/${splash.image}',
'android:windowSplashScreenIconBackgroundColor':
splash.iconBackgroundColor == null
? null
: '#${splash.iconBackgroundColor}',
};
items.forEach((name, value) {
if (value == null) {
_removeElement(launchTheme: style, name: name);
} else {
_replaceElement(launchTheme: style, name: name, value: value);
}
});
}

if (android12IconBackgroundColor == null) {
_removeElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenIconBackgroundColor',
);
} else {
_replaceElement(
launchTheme: launchTheme,
name: 'android:windowSplashScreenIconBackgroundColor',
value: '#$android12IconBackgroundColor',
);
/// Adds the NativeSplashLight and NativeSplashDark styles to [file], or
/// removes them when [light] and [dark] are null. An app passes them to
/// SplashScreen.setSplashScreenTheme so the splash follows its own light/dark
/// setting. They only go in the non-night styles file, so neither changes with
/// the phone's theme. Android stores the chosen style by name: never rename
/// them.
void _applyAppThemeStyles({
required String file,
required _Android12Splash? light,
required _Android12Splash? dark,
}) {
final stylesFile = File(file);
if (!stylesFile.existsSync()) return;
final stylesDocument = XmlDocument.parse(stylesFile.readAsStringSync());
final resources = stylesDocument.getElement('resources');
if (resources == null) return;

var changed = false;
for (final (name, parent, splash) in [
('NativeSplashLight', '@android:style/Theme.Light.NoTitleBar', light),
('NativeSplashDark', '@android:style/Theme.Black.NoTitleBar', dark),
]) {
var style = resources
.findElements('style')
.where((element) => element.getAttribute('name') == name)
.firstOrNull;
if (splash == null) {
style?.remove();
changed |= style != null;
continue;
}
if (style == null) {
style = XmlElement.tag('style')
..setAttribute('name', name)
..setAttribute('parent', parent);
resources.children.add(style);
}
_setAndroid12SplashItems(style, splash);
changed = true;
}

if (!changed) return;
stylesFile.writeAsStringSync(
'${stylesDocument.toXmlString(pretty: true, indent: ' ')}\n',
);
Expand Down
6 changes: 6 additions & 0 deletions lib/cli_commands.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ void _createSplashByConfig(Map<String, dynamic> config) {
String? android12DarkColor;
String? android12BrandingImage;
String? android12DarkBrandingImage;
bool android12AppThemeStyles = false;

if (config[_Parameter.android12Section] != null) {
final android12Config =
Expand All @@ -154,6 +155,8 @@ void _createSplashByConfig(Map<String, dynamic> config) {
config: android12Config, parameter: _Parameter.brandingImage);
android12DarkBrandingImage = _checkImageExists(
config: android12Config, parameter: _Parameter.brandingDarkImage);
android12AppThemeStyles =
android12Config[_Parameter.appThemeStyles] as bool? ?? false;
}

if (!config.containsKey(_Parameter.android) ||
Expand Down Expand Up @@ -185,6 +188,7 @@ void _createSplashByConfig(Map<String, dynamic> config) {
android12BrandingImagePath: android12BrandingImage,
android12DarkBrandingImagePath:
android12DarkBrandingImage ?? android12BrandingImage,
android12AppThemeStyles: android12AppThemeStyles,
);
} else {
print('Android folder not found, skipping Android splash update...');
Expand Down Expand Up @@ -424,6 +428,7 @@ class _Parameter {
static const android12Section = 'android_12';
static const androidMinSdk = 'android_min_sdk';
static const androidScreenOrientation = 'android_screen_orientation';
static const appThemeStyles = 'app_theme_styles';
static const backgroundImage = 'background_image';
static const backgroundImageAndroid = 'background_image_android';
static const backgroundImageIos = 'background_image_ios';
Expand Down Expand Up @@ -475,6 +480,7 @@ class _Parameter {
android12Section,
androidMinSdk,
androidScreenOrientation,
appThemeStyles,
backgroundImage,
backgroundImageAndroid,
backgroundImageIos,
Expand Down
Loading