diff --git a/packages/flame_splash_screen/.metadata b/packages/flame_splash_screen/.metadata
new file mode 100644
index 00000000000..162b83e084b
--- /dev/null
+++ b/packages/flame_splash_screen/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 68841bdab9840b5d72499f9cdd80b69894046ed0
+ channel: master
+
+project_type: package
diff --git a/packages/flame_splash_screen/CHANGELOG.md b/packages/flame_splash_screen/CHANGELOG.md
new file mode 100644
index 00000000000..990027d8268
--- /dev/null
+++ b/packages/flame_splash_screen/CHANGELOG.md
@@ -0,0 +1,8 @@
+## 0.1.0
+
+- Added non nullability support
+- Update to new logo
+
+## 0.0.1
+
+- Initial implementation with theme, controller and of course, the splash screen widget.
diff --git a/packages/flame_splash_screen/LICENSE b/packages/flame_splash_screen/LICENSE
new file mode 100644
index 00000000000..3897c4d092d
--- /dev/null
+++ b/packages/flame_splash_screen/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2021 Blue Fire
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/packages/flame_splash_screen/README.md b/packages/flame_splash_screen/README.md
new file mode 100644
index 00000000000..1a839a2deae
--- /dev/null
+++ b/packages/flame_splash_screen/README.md
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+Style your [Flame](https://github.com/flame-engine/flame) game with a beautiful splash screen.
+
+This package includes a `FlameSplashScreen` widget.
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Install
+
+Add `flame_splash_screen` as a dependency to
+[your pubspec.yaml file](https://pub.dev/packages/flame_splash_screen/install).
+
+Import the widget:
+
+```dart
+import 'package:flame_splash_screen/flame_splash_screen.dart';
+```
+
+
+## Usage
+
+The splash screen is a widget that can be used to show the splash screen.
+
+
+### Simple usage
+
+There is just two required params:
+
+- `onFinish`, a callback that is executed when all animations from the splash screen is over.
+- `theme`, than can be either `FlameSplashTheme.dark` or `FlameSplashTheme.white`.
+
+```dart
+FlameSplashScreen(
+ theme: FlameSplashTheme.dark,
+ onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),
+)
+```
+
+
+#### Adding your own content
+
+You can pass your own logo (or/and anything else) to be shown before or after the Flame's logo.
+
+```dart
+FlameSplashScreen(
+ theme: FlameSplashTheme.dark,
+ showBefore: (BuildContext context) {
+ return Text("To be shown before flame animation");
+ },
+ onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),
+)
+```
+
+```dart
+FlameSplashScreen(
+ theme: FlameSplashTheme.dark,
+ showAfter: (BuildContext context) {
+ return Text("To be shown after flame animation");
+ },
+ onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),
+)
+```
+
+Remember: you can also specify both `showBefore` and `showAfter` at the same time.
+
+
+#### Changing theme
+
+By default the splash screen has a dark background. You can change it by specifying the `white`
+theme.
+
+Aside from `FlameSplashTheme.dark`, you can pass `FlameSplashTheme.white` for a white background.
+
+```dart
+FlameSplashScreen(
+ theme: FlameSplashTheme.white,
+ onFinish: (BuildContext context) => Navigator.pushNamed(context, '/your-game-initial-screen'),
+)
+```
+
+You can create your own theme passing a custom logo builder (changing flames logo for another one)
+and a background decoration
+
+
+### Usage with controller
+
+Controller enables `FlameSplashScreen` to be customized regarding animation duration and when it
+starts.
+
+There is duration params and `autoStart` (which is true by default).
+
+To use it, make the controller lives as much as a widget state:
+
+```dart
+class SplashScreenGameState extends State {
+ FlameSplashController controller;
+ @override
+ void initState() {
+ super.initState();
+ controller = FlameSplashController(
+ fadeInDuration: Duration(seconds: 1),
+ fadeOutDuration: Duration(milliseconds: 250),
+ waitDuration: Duration(seconds: 2),
+ autoStart: false,
+ );
+ }
+
+ @override
+ void dispose() {
+ controller.dispose(); // dispose it when necessary
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: FlameSplashScreen(
+ showBefore: (BuildContext context) {
+ return Text("Before the logo");
+ },
+ showAfter: (BuildContext context) {
+ return Text("After the logo");
+ },
+ theme: FlameSplashTheme.white,
+ onFinish: (context) => Navigator.pushNamed(context, '/the-game-initial-screen'),
+ controller: controller,
+ ),
+ );
+ }
+}
+```
diff --git a/packages/flame_splash_screen/analysis_options.yaml b/packages/flame_splash_screen/analysis_options.yaml
new file mode 100644
index 00000000000..ba5631f3b8a
--- /dev/null
+++ b/packages/flame_splash_screen/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flame_lint/analysis_options.yaml
\ No newline at end of file
diff --git a/packages/flame_splash_screen/assets/layer1.png b/packages/flame_splash_screen/assets/layer1.png
new file mode 100644
index 00000000000..25e9a072f5b
Binary files /dev/null and b/packages/flame_splash_screen/assets/layer1.png differ
diff --git a/packages/flame_splash_screen/assets/layer2.png b/packages/flame_splash_screen/assets/layer2.png
new file mode 100644
index 00000000000..29f4eb134b5
Binary files /dev/null and b/packages/flame_splash_screen/assets/layer2.png differ
diff --git a/packages/flame_splash_screen/assets/layer3.png b/packages/flame_splash_screen/assets/layer3.png
new file mode 100644
index 00000000000..777e240ee86
Binary files /dev/null and b/packages/flame_splash_screen/assets/layer3.png differ
diff --git a/packages/flame_splash_screen/demo.gif b/packages/flame_splash_screen/demo.gif
new file mode 100644
index 00000000000..81c4ef7668e
Binary files /dev/null and b/packages/flame_splash_screen/demo.gif differ
diff --git a/packages/flame_splash_screen/example/.metadata b/packages/flame_splash_screen/example/.metadata
new file mode 100644
index 00000000000..5caddb32abe
--- /dev/null
+++ b/packages/flame_splash_screen/example/.metadata
@@ -0,0 +1,30 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "12fccda598477eddd19f93040a1dba24f915b9be"
+ channel: "stable"
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 12fccda598477eddd19f93040a1dba24f915b9be
+ base_revision: 12fccda598477eddd19f93040a1dba24f915b9be
+ - platform: linux
+ create_revision: 12fccda598477eddd19f93040a1dba24f915b9be
+ base_revision: 12fccda598477eddd19f93040a1dba24f915b9be
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/packages/flame_splash_screen/example/README.md b/packages/flame_splash_screen/example/README.md
new file mode 100644
index 00000000000..1b3bb74fdf1
--- /dev/null
+++ b/packages/flame_splash_screen/example/README.md
@@ -0,0 +1,3 @@
+# Splash screen example app
+
+Example of usage
diff --git a/packages/flame_splash_screen/example/analysis_options.yaml b/packages/flame_splash_screen/example/analysis_options.yaml
new file mode 100644
index 00000000000..ba5631f3b8a
--- /dev/null
+++ b/packages/flame_splash_screen/example/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flame_lint/analysis_options.yaml
\ No newline at end of file
diff --git a/packages/flame_splash_screen/example/lib/main.dart b/packages/flame_splash_screen/example/lib/main.dart
new file mode 100644
index 00000000000..c846560af2b
--- /dev/null
+++ b/packages/flame_splash_screen/example/lib/main.dart
@@ -0,0 +1,68 @@
+import 'package:flame_splash_screen/flame_splash_screen.dart';
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: const SplashScreenGame(),
+ theme: ThemeData.dark(),
+ debugShowCheckedModeBanner: false,
+ );
+ }
+}
+
+class OtherScreen extends StatelessWidget {
+ const OtherScreen({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: ElevatedButton(
+ child: const Text('Come again'),
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(builder: (context) => const SplashScreenGame()),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class SplashScreenGame extends StatefulWidget {
+ const SplashScreenGame({super.key});
+
+ @override
+ SplashScreenGameState createState() => SplashScreenGameState();
+}
+
+class SplashScreenGameState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: FlameSplashScreen(
+ showBefore: (BuildContext context) {
+ return const Text('Before logo');
+ },
+ showAfter: (BuildContext context) {
+ return const Text('After logo');
+ },
+ theme: FlameSplashTheme.dark,
+ onFinish: (context) => Navigator.pushReplacement(
+ context,
+ MaterialPageRoute(builder: (context) => const OtherScreen()),
+ ),
+ ),
+ );
+ }
+}
diff --git a/packages/flame_splash_screen/example/pubspec.yaml b/packages/flame_splash_screen/example/pubspec.yaml
new file mode 100644
index 00000000000..22cadbb3543
--- /dev/null
+++ b/packages/flame_splash_screen/example/pubspec.yaml
@@ -0,0 +1,20 @@
+name: flame_splash_screen_example
+description: Flame Splash Screen Example
+publish_to: none
+
+version: 1.0.0+1
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+ flutter: ">=3.13.0"
+
+dependencies:
+ flame_splash_screen: ^0.1.0
+ flutter:
+ sdk: flutter
+
+dev_dependencies:
+ flame_lint: ^1.1.1
+ flutter_test:
+ sdk: flutter
+
diff --git a/packages/flame_splash_screen/lib/flame_splash_screen.dart b/packages/flame_splash_screen/lib/flame_splash_screen.dart
new file mode 100644
index 00000000000..efc690a8031
--- /dev/null
+++ b/packages/flame_splash_screen/lib/flame_splash_screen.dart
@@ -0,0 +1,3 @@
+export 'src/controller.dart';
+export 'src/splash.dart';
+export 'src/theme.dart';
diff --git a/packages/flame_splash_screen/lib/src/controller.dart b/packages/flame_splash_screen/lib/src/controller.dart
new file mode 100644
index 00000000000..4999928b23a
--- /dev/null
+++ b/packages/flame_splash_screen/lib/src/controller.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/widgets.dart';
+import 'package:meta/meta.dart';
+
+/// Controller enables you to start the animation whenever you want with
+/// [autoStart] option and customize animation duration as well.
+class FlameSplashController {
+ FlameSplashController({
+ Duration fadeInDuration = const Duration(milliseconds: 750),
+ Duration waitDuration = const Duration(seconds: 2),
+ Duration fadeOutDuration = const Duration(milliseconds: 450),
+ this.autoStart = true,
+ }) : stepController = FlameSplashControllerStep(0),
+ durations = FlameSplashDurations(
+ fadeInDuration,
+ waitDuration,
+ fadeOutDuration,
+ );
+
+ /// Defines if you want to start the animations right after widget mount.
+ final bool autoStart;
+
+ @internal
+ final FlameSplashDurations durations;
+
+ @internal
+ final FlameSplashControllerStep stepController;
+
+ FlameSplashControllerState _state = FlameSplashControllerState.idle;
+ bool _hasSetup = false;
+ int _stepsAmount = 0;
+ late void Function() _onFinish;
+
+ /// Displays the actual state of the controller regarding the animation.
+ FlameSplashControllerState get state => _state;
+ @internal
+ set state(FlameSplashControllerState newState) => _state = newState;
+
+ /// Method used to start the animation, do not call if you set [autoStart] to
+ /// true.
+ void start() {
+ assert(
+ _hasSetup,
+ 'This controller is not being used by any FlameSplashScreen widget. '
+ 'Start it only after widget mount.',
+ );
+ assert(
+ _state != FlameSplashControllerState.started,
+ 'This controller has been already started, verify if autoStart has been '
+ 'specified',
+ );
+
+ _state = FlameSplashControllerState.started;
+ _tickStep(0);
+ }
+
+ /// Called by the [start] method; this is only exposed for testing purposes.
+ @internal
+ void setup(
+ int steps,
+ void Function() onFinish,
+ ) {
+ _onFinish = onFinish;
+ _stepsAmount = steps;
+ _hasSetup = true;
+ if (autoStart) {
+ start();
+ }
+ }
+
+ Future _tickStep(int index) async {
+ stepController.value = index;
+ await Future.delayed(durations.total);
+ final finished = index >= _stepsAmount - 1;
+ if (finished) {
+ _state = FlameSplashControllerState.finished;
+ _onFinish();
+ return;
+ }
+ _tickStep(index + 1);
+ }
+
+ /// Properly disposes of this controller.
+ /// Must be called after no longer used.
+ void dispose() {
+ stepController.dispose();
+ }
+}
+
+/// Represents the state of the splash screen.
+enum FlameSplashControllerState {
+ /// Not started yet, but ready to start.
+ /// Note that if autoStart is set, this stage will be skipped.
+ idle,
+
+ /// Started and currently running through the steps.
+ started,
+
+ /// Finished to run through all steps.
+ finished,
+}
+
+class FlameSplashControllerStep extends ValueNotifier {
+ FlameSplashControllerStep(super.value);
+}
+
+class FlameSplashDurations {
+ const FlameSplashDurations(
+ this.fadeInDuration,
+ this.waitDuration,
+ this.fadeOutDuration,
+ );
+
+ final Duration fadeInDuration;
+ final Duration waitDuration;
+ final Duration fadeOutDuration;
+
+ Duration get total {
+ return fadeInDuration + fadeOutDuration + waitDuration;
+ }
+}
diff --git a/packages/flame_splash_screen/lib/src/splash.dart b/packages/flame_splash_screen/lib/src/splash.dart
new file mode 100644
index 00000000000..b94d86451d7
--- /dev/null
+++ b/packages/flame_splash_screen/lib/src/splash.dart
@@ -0,0 +1,187 @@
+import 'package:flame_splash_screen/flame_splash_screen.dart';
+import 'package:flutter/widgets.dart';
+
+/// A stateful widget to show a splash screen animation for flame games
+class FlameSplashScreen extends StatefulWidget {
+ /// Creates a [FlameSplashScreen].
+ const FlameSplashScreen({
+ required this.onFinish,
+ required this.theme,
+ this.showBefore,
+ this.showAfter,
+ this.controller,
+ super.key,
+ });
+
+ /// Gives extra controller over the splash animation.
+ final FlameSplashController? controller;
+
+ /// Enables to set a different theme other than the default.
+ final FlameSplashTheme theme;
+
+ /// The only required option, callback to be invoked when animation finished
+ final ValueChanged onFinish;
+
+ /// Adds an extra step to the animation showing a widget, can be other logo.
+ ///
+ /// Shown before flame logo.
+ final WidgetBuilder? showBefore;
+
+ /// Adds an extra step to the animation showing a widget, can be other logo.
+ ///
+ /// Shown after flame logo.
+ final WidgetBuilder? showAfter;
+
+ @override
+ FlameSplashScreenState createState() => FlameSplashScreenState();
+}
+
+/// The state for the [FlameSplashScreen] that holds the [controller] for
+/// controlling the animation durations and whether it should automatically
+/// start. Also contains the list of steps that it should animate through.
+class FlameSplashScreenState extends State {
+ /// The [controller] for controlling the animation durations and whether it
+ /// should automatically start.
+ late FlameSplashController controller;
+
+ /// The list of steps that it should animate through.
+ late List steps;
+ bool _externallyControlled = false;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _externallyControlled = widget.controller != null;
+ controller = widget.controller ?? FlameSplashController();
+
+ _computeSteps();
+ controller.setup(steps.length, () => widget.onFinish(context));
+ }
+
+ void _computeSteps() {
+ setState(() {
+ steps = [
+ if (widget.showBefore != null) widget.showBefore!,
+ widget.theme.logoBuilder,
+ if (widget.showAfter != null) widget.showAfter!,
+ ];
+ });
+ }
+
+ @override
+ void didUpdateWidget(FlameSplashScreen oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ final hasStepsChanged = widget.showBefore != oldWidget.showBefore ||
+ widget.showAfter != oldWidget.showAfter ||
+ widget.theme.logoBuilder != oldWidget.theme.logoBuilder;
+ if (hasStepsChanged &&
+ controller.state != FlameSplashControllerState.started) {
+ _computeSteps();
+ controller.start();
+ }
+ }
+
+ @override
+ void dispose() {
+ if (!_externallyControlled) {
+ controller.dispose();
+ }
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ constraints: widget.theme.constraints,
+ decoration: widget.theme.backgroundDecoration,
+ child: Center(
+ child: ValueListenableBuilder(
+ valueListenable: controller.stepController,
+ builder: (context, currentStep, _) {
+ return _SplashScreenStep(
+ builder: steps[currentStep],
+ durations: controller.durations,
+ key: ObjectKey(currentStep),
+ );
+ },
+ ),
+ ),
+ );
+ }
+}
+
+class _SplashScreenStep extends StatefulWidget {
+ const _SplashScreenStep({
+ required this.builder,
+ required this.durations,
+ super.key,
+ });
+
+ final WidgetBuilder builder;
+ final FlameSplashDurations durations;
+
+ @override
+ __SplashScreenStepState createState() => __SplashScreenStepState();
+}
+
+class __SplashScreenStepState extends State<_SplashScreenStep>
+ with TickerProviderStateMixin {
+ late AnimationController controller;
+ late Animation opacityAnimation;
+ double opacity = 0.0;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = AnimationController(
+ vsync: this,
+ )..addListener(handleAnimation);
+ opacityAnimation = Tween(
+ begin: 0.0,
+ end: 1.0,
+ ).animate(controller);
+ startAnimation();
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ void handleAnimation() {
+ setState(() {
+ opacity = opacityAnimation.value;
+ });
+ }
+
+ Future startAnimation() async {
+ // fade in
+ controller
+ ..value = 0.0
+ ..duration = widget.durations.fadeInDuration;
+ await controller.forward();
+ await Future.delayed(widget.durations.waitDuration);
+ controller
+ ..value = 1.0
+ ..duration = widget.durations.fadeOutDuration
+ ..reverse();
+ }
+
+ @override
+ void didUpdateWidget(_SplashScreenStep oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (oldWidget.key != widget.key) {
+ startAnimation();
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Opacity(
+ opacity: opacity,
+ child: widget.builder(context),
+ );
+ }
+}
diff --git a/packages/flame_splash_screen/lib/src/theme.dart b/packages/flame_splash_screen/lib/src/theme.dart
new file mode 100644
index 00000000000..c443d72432f
--- /dev/null
+++ b/packages/flame_splash_screen/lib/src/theme.dart
@@ -0,0 +1,138 @@
+import 'package:flutter/widgets.dart';
+
+/// This widget builds up the Flame logo composed of 3 layers,
+/// that are rendered via separate PNG files under the assets directory.
+class AnimatedLogo extends AnimatedWidget {
+ /// Create this widget providing the animation parameter to control
+ /// the opacity of the flame.
+ const AnimatedLogo({
+ required Animation animation,
+ super.key,
+ }) : super(listenable: animation);
+
+ @override
+ Widget build(BuildContext context) {
+ final animation = listenable as Animation;
+ return Stack(
+ fit: StackFit.expand,
+ alignment: Alignment.center,
+ children: [
+ Image.asset(
+ 'assets/layer1.png',
+ package: 'flame_splash_screen',
+ ),
+ Opacity(
+ opacity: animation.value,
+ child: Image.asset(
+ 'assets/layer2.png',
+ package: 'flame_splash_screen',
+ ),
+ ),
+ Image.asset(
+ 'assets/layer3.png',
+ package: 'flame_splash_screen',
+ ),
+ ],
+ );
+ }
+}
+
+/// Creates and controls an [AnimatedLogo], making sure to provide the required
+/// animation and to properly dispose of itself after usage.
+class LogoComposite extends StatefulWidget {
+ /// Creates a [LogoComposite].
+ const LogoComposite({super.key});
+
+ @override
+ LogoCompositeState createState() => LogoCompositeState();
+}
+
+/// The state holding the state of the animated logo and its controller.
+class LogoCompositeState extends State
+ with SingleTickerProviderStateMixin {
+ /// The state of the animated logo.
+ late Animation animation;
+
+ /// The controller for the animated logo.
+ late AnimationController controller;
+
+ @override
+ void initState() {
+ super.initState();
+ controller = AnimationController(
+ duration: const Duration(milliseconds: 500),
+ vsync: this,
+ );
+ animation = Tween(begin: 0.0, end: 1.0).animate(controller);
+
+ controller.addStatusListener((status) {
+ if (status == AnimationStatus.completed) {
+ controller.reverse();
+ } else if (status == AnimationStatus.dismissed) {
+ controller.forward();
+ }
+ });
+
+ controller.forward();
+ }
+
+ @override
+ void dispose() {
+ controller.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) => AnimatedLogo(animation: animation);
+}
+
+Widget _logoBuilder(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ return FractionalTranslation(
+ translation: const Offset(0, -0.25),
+ child: ConstrainedBox(
+ constraints: BoxConstraints.loose(const Size(300, 300)),
+ child: const LogoComposite(),
+ ),
+ );
+ },
+ );
+}
+
+/// Wraps the splash screen layout options.
+/// There is two predefined themes [FlameSplashTheme.dark] and
+/// [FlameSplashTheme.white].
+class FlameSplashTheme {
+ /// Creates a customized theme. [logoBuilder] returns the widget that will be
+ /// rendered in place of main step of the animation.
+ const FlameSplashTheme({
+ required this.backgroundDecoration,
+ required this.logoBuilder,
+ this.constraints = const BoxConstraints.expand(),
+ });
+
+ /// Decoration to be applied to the widget underneath the Flame logo.
+ /// It can be used to set the background color, among other parameters.
+ final BoxDecoration backgroundDecoration;
+
+ /// A lambda to build the widget representing the logo itself.
+ /// By default this will be wired to use the [LogoComposite] widget.
+ final WidgetBuilder logoBuilder;
+
+ /// Th constraints of the outside box, defaults to [BoxConstraints.expand].
+ final BoxConstraints constraints;
+
+ /// One of the two default themes provided; this is optimal of light mode
+ /// apps.
+ static FlameSplashTheme white = const FlameSplashTheme(
+ backgroundDecoration: BoxDecoration(color: Color(0xFFFFFFFF)),
+ logoBuilder: _logoBuilder,
+ );
+
+ /// One of the two default themes provided; this is optimal of dark mode apps.
+ static FlameSplashTheme dark = const FlameSplashTheme(
+ backgroundDecoration: BoxDecoration(color: Color(0xFF000000)),
+ logoBuilder: _logoBuilder,
+ );
+}
diff --git a/packages/flame_splash_screen/pubspec.yaml b/packages/flame_splash_screen/pubspec.yaml
new file mode 100644
index 00000000000..c939bffdd08
--- /dev/null
+++ b/packages/flame_splash_screen/pubspec.yaml
@@ -0,0 +1,27 @@
+name: flame_splash_screen
+description: Style your flame game with a beautiful splash screen with logo reveal. Simple to use but still customizable.
+version: 0.1.0
+homepage: https://github.com/flame-engine/flame/tree/main/packages/flame_splash_screen
+funding:
+ - https://opencollective.com/blue-fire
+ - https://github.com/sponsors/bluefireteam
+ - https://patreon.com/bluefireoss
+
+environment:
+ sdk: ">=3.0.0 <4.0.0"
+ flutter: ">=3.13.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ meta: ^1.9.1
+
+dev_dependencies:
+ flame_lint: ^1.1.1
+ flutter_test:
+ sdk: flutter
+ mockito: ^5.4.2
+
+flutter:
+ assets:
+ - assets/
diff --git a/packages/flame_splash_screen/test/controller_test.dart b/packages/flame_splash_screen/test/controller_test.dart
new file mode 100644
index 00000000000..4aa158e1b4d
--- /dev/null
+++ b/packages/flame_splash_screen/test/controller_test.dart
@@ -0,0 +1,70 @@
+import 'package:flame_splash_screen/flame_splash_screen.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+
+class OnFinishContainer {
+ void onFinish() {}
+}
+
+class MockOnFinish extends Mock implements OnFinishContainer {}
+
+void main() {
+ group('Without autostart', () {
+ late FlameSplashController controller;
+ setUp(() {
+ controller = FlameSplashController(
+ fadeInDuration: const Duration(milliseconds: 500),
+ fadeOutDuration: const Duration(milliseconds: 500),
+ waitDuration: const Duration(milliseconds: 500),
+ autoStart: false,
+ );
+ });
+ test('Do not let it start before setup', () async {
+ expect(() => controller.start(), throwsAssertionError);
+ controller.setup(1, () {});
+ controller.start();
+ });
+ test('Idle after setup', () {
+ controller.setup(1, () {});
+ expect(controller.state, FlameSplashControllerState.idle);
+ });
+ test('Started after .start()', () {
+ controller.setup(1, () {});
+ expect(controller.state, FlameSplashControllerState.idle);
+ controller.start();
+ expect(controller.state, FlameSplashControllerState.started);
+ });
+ test('Calls onFinish after steps', () async {
+ final onFinishContainer = MockOnFinish();
+ controller.setup(3, onFinishContainer.onFinish);
+ controller.start();
+ await untilCalled(onFinishContainer.onFinish());
+ expect(controller.state, FlameSplashControllerState.finished);
+ });
+ });
+ group('With autostart', () {
+ late FlameSplashController controller;
+ setUp(() {
+ controller = FlameSplashController(
+ fadeInDuration: const Duration(milliseconds: 500),
+ fadeOutDuration: const Duration(milliseconds: 500),
+ waitDuration: const Duration(milliseconds: 500),
+ );
+ });
+ test('Started automatically', () {
+ controller.setup(1, () {});
+ expect(controller.state, FlameSplashControllerState.started);
+ });
+ test('Calls onFinish after steps', () async {
+ final onFinishContainer = MockOnFinish();
+ controller.setup(3, onFinishContainer.onFinish);
+ await untilCalled(onFinishContainer.onFinish());
+ expect(controller.state, FlameSplashControllerState.finished);
+ });
+
+ test('Do not let it start again', () async {
+ controller.setup(1, () {});
+ expect(() => controller.start(), throwsAssertionError);
+ });
+ });
+}
diff --git a/packages/flame_splash_screen/test/theme_test.dart b/packages/flame_splash_screen/test/theme_test.dart
new file mode 100644
index 00000000000..e4bd4596ab6
--- /dev/null
+++ b/packages/flame_splash_screen/test/theme_test.dart
@@ -0,0 +1,26 @@
+import 'package:flame_splash_screen/flame_splash_screen.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ test('white', () {
+ final theme = FlameSplashTheme.white;
+ expect(
+ theme.constraints,
+ const BoxConstraints.expand(),
+ );
+ expect(
+ theme.backgroundDecoration,
+ const BoxDecoration(color: Color(0xFFFFFFFF)),
+ );
+ });
+
+ test('dark', () {
+ final theme = FlameSplashTheme.dark;
+ expect(theme.constraints, const BoxConstraints.expand());
+ expect(
+ theme.backgroundDecoration,
+ const BoxDecoration(color: Color(0xFF000000)),
+ );
+ });
+}