Adding a Splash Screen to Your Flutter App: A Step-by-Step Guide

A splash screen is a graphical control element that appears on the screen when a mobile app or program is launching. It is usually the first screen that the user sees and is displayed while the application is loading. 

The splash screen typically contains an image, logo, and current version of the software, and it serves as a visual cue to let users know that the app is launching and to give them something to look at while the hardware is loading the software.

Splash screens can also be used as welcome screens to provide a simple initial experience for the user when launching a mobile game or program. This can help create a positive first impression of the app and set the tone for the user's overall experience. Overall, splash screens play an important role in creating a smooth and polished user experience for mobile apps and programs.

Demo 1:

import 'package:flutter/material.dart';

import '../../data/img.dart';
import 'dart:math' as math;

class SplashScreenAnim extends StatefulWidget {
  const SplashScreenAnim({super.key});

  @override
  AnimatedBuilderDemoState createState() => AnimatedBuilderDemoState();
}

class AnimatedBuilderDemoState extends State<SplashScreenAnim>
    with SingleTickerProviderStateMixin {

  late AnimationController _controller; // Define an animation controller

  @override
  void initState() {
    super.initState();
    _controller =  AnimationController( // Initialize the animation controller
      duration: const Duration(seconds: 3),
      vsync: this,
    );
    
    _controller.forward().then((value) { // Start the animation and when it's finished, pop the current screen
      Navigator.of(context).pop();
    });

  }

  @override
  void dispose() {
    _controller.dispose(); // Dispose of the animation controller when the widget is removed from the tree
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body:

      Center(
        child:
        Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [

            AnimatedBuilder( // Use AnimatedBuilder to animate the rotation of the bird image
              animation: _controller,
              child: SizedBox(
                width: 120.0,
                height: 120.0,
                child: Image.asset(Img.get("bird.png")), // The bird image to animate
              ),
              builder: (BuildContext context, Widget? child) {
                return Transform.rotate(
                  angle: _controller.value * 2.0 * math.pi, // Rotate the image by the animation value
                  child: child,
                );
              },
            ),


            Container(height: 15),

            ShaderMask( // Use ShaderMask to add a gradient to the "Flutter UIX" text
              shaderCallback: (Rect bounds) {
                return const LinearGradient(
                  colors: [Color(0xFF0089CF), Color(0xFF00CDBA)],
                  begin: Alignment.topLeft,
                  end: Alignment.bottomRight,
                ).createShader(bounds);
              },
              child: const Text("Flutter UIX", style: TextStyle(fontSize: 40, color: Colors.white, fontWeight: FontWeight.bold),textAlign: TextAlign.center,),
            ),

          ])

      ),
    );
  }
}

Simple splash screen animation using the AnimatedBuilder and AnimationController widgets. The animation consists of rotating an image of a bird while displaying the text "Flutter UIX" in a gradient color.


The widget extends StatefulWidget, which means it has mutable state that can change over time. It also implements the SingleTickerProviderStateMixin, which provides a TickerProvider for the animation controller.

The initState method is called when the widget is first created, and it sets up the animation controller with a duration of 3 seconds and a vsync parameter to synchronize the animation with the device's refresh rate. It then starts the animation by calling the forward() method on the controller. Once the animation is complete, it calls Navigator.of(context).pop() to dismiss the splash screen and return to the previous screen.


The dispose method is called when the widget is removed from the widget tree, and it disposes of the animation controller to free up resources.


The build method returns a Scaffold widget with a Center widget as its body. The Center widget contains a Column widget with two children: an AnimatedBuilder and a ShaderMask.


The AnimatedBuilder widget takes an animation and a child as parameters. The animation is the animation controller created in initState, and the child is a SizedBox widget with an Image.asset widget as its child. The builder parameter of AnimatedBuilder takes a callback function that returns a new widget to display based on the current state of the animation. In this case, the builder rotates the child widget by an angle that increases as the animation progresses.


The ShaderMask widget takes a shaderCallback and a child as parameters. The shaderCallback is a function that returns a Shader to use for the mask. In this case, the shader is a LinearGradient that creates a gradient from the top-left corner to the bottom-right corner of the text bounds. The child is a Text widget that displays the text "Flutter UIX" in white with a bold font, centered on the screen. The ShaderMask applies the gradient as a mask to the text, making it appear in a gradient color.



Demo 2:

splash_screen_loader.dart

import 'dart:async';
import 'package:flutter/material.dart';
import '../progress/progress_dot.dart'; // Importing a custom widget

class SplashScreenLoader extends StatefulWidget {
  const SplashScreenLoader({Key? key}) : super(key: key);

  @override
  State<SplashScreenLoader> createState() => _SplashScreenLoaderState();
}

class _SplashScreenLoaderState extends State<SplashScreenLoader> {

  @override
  void initState() {
    super.initState();
    // This timer waits for 3 seconds and then pops the current screen from the navigation stack
    Timer(
        const Duration(seconds: 3),
        () =>   Navigator.of(context).pop() //Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const OnBoard()))
    );
  }

  late AnimationController lottieController;

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body: Container(
        width: double.infinity,
        height: double.infinity,
        decoration: const BoxDecoration(
          gradient: LinearGradient(
              begin: Alignment.topRight,
              end: Alignment.bottomLeft,
              colors: [
                Colors.black,
                Colors.black87,
              ]
          ),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Column(
              children: [

                const SizedBox(height: 16.0 * 2),
                Image.asset(
                  "assets/images/bird.png",
                  height: 150.0,
                  width: 150.0,
                ),

                Align(
                  alignment: Alignment.center,
                  child: Container(
                    width: 105,
                    height: 100,
                    alignment: Alignment.center,
                    child: const WidgetDotBounce(color: Colors.green, size: 20.0), // Using a custom widget
                  ),
                ),

                const SizedBox(height: 16.0 * 2),
                const Text(
                  "Build apps for any screen",
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: Colors.grey,
                    letterSpacing: 2.0,
                    fontWeight: FontWeight.bold,
                    fontSize: 16.0,
                  ),
                ),
                Image.asset(
                  "assets/images/bird_flutter.png",
                  height: 200.0,
                  //width: 300.0,
                ),

              ],
            ),
          ],
        ),
      ),
    );
  }
}

Splash screen with a loading animation. It starts with a gradient background and an image of a bird at the center. Then, it displays a loading animation using the WidgetDotBounce widget from a custom ProgressDot module. The loading animation is presented as a green dot that bounces continuously.

The widget also displays a text "Build apps for any screen" below the loading animation, and another image of a bird with the Flutter logo at the bottom of the screen.


The initState method uses a timer to pop the splash screen after three seconds and move to the next screen.


Overall, this widget provides a simple and visually appealing way to display a splash screen with a loading animation in a Flutter app.

..

progress_dot.dart

Dot bounce progress animation

import 'package:flutter/widgets.dart';
import 'delay_tween.dart';

/*
 Dot bounce progress animation
 */

class WidgetDotBounce extends StatefulWidget {

  const WidgetDotBounce({
    Key? key,
    this.color,
    this.size = 18.0, this.count = 3,
    this.itemBuilder,
    this.duration = const Duration(milliseconds: 300),
    this.controller,
  })  : assert(!(itemBuilder is IndexedWidgetBuilder && color is Color) && !(itemBuilder == null && color == null),
  'You should specify either a itemBuilder or a color'),
        super(key: key);

  final Color? color;
  final double size;
  final int count;
  final IndexedWidgetBuilder? itemBuilder;
  final Duration duration;
  final AnimationController? controller;

  @override
  WidgetDotBounceState createState() => WidgetDotBounceState();

}

class WidgetDotBounceState extends State<WidgetDotBounce> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = (widget.controller ?? AnimationController(
        vsync: this, duration: Duration(milliseconds: widget.count * widget.duration.inMilliseconds))
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox.fromSize(
        size: Size(widget.size * 3.2, widget.size * 2),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: List.generate(widget.count, (i) {
            return SlideTransition(
              position: DelayTweenOffset(begin: const Offset(0, 0.5), end: const Offset(0, -0.5), delay: i * 0.2).animate(_controller),
              child: SizedBox.fromSize(size: Size.square(widget.size * 0.5), child: _itemBuilder(i)),
            );
          }),
        ),
      ),
    );
  }

  Widget _itemBuilder(int index) => widget.itemBuilder != null
      ? widget.itemBuilder!(context, index)
      : DecoratedBox(decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle));
}

/*
 Dot grow progress animation
 */

class WidgetDotGrow extends StatefulWidget {

  const WidgetDotGrow({
    Key? key,
    this.color,
    this.size = 18.0, this.count = 3,
    this.itemBuilder,
    this.duration = const Duration(milliseconds: 300),
    this.controller,
  })  : assert(!(itemBuilder is IndexedWidgetBuilder && color is Color) && !(itemBuilder == null && color == null),
  'You should specify either a itemBuilder or a color'),
        super(key: key);

  final Color? color;
  final double size;
  final int count;
  final IndexedWidgetBuilder? itemBuilder;
  final Duration duration;
  final AnimationController? controller;

  @override
  WidgetDotGrowState createState() => WidgetDotGrowState();

}

class WidgetDotGrowState extends State<WidgetDotGrow> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = (widget.controller ?? AnimationController(
        vsync: this, duration: Duration(milliseconds: widget.count * widget.duration.inMilliseconds))
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox.fromSize(
        size: Size(widget.size * 3.5, widget.size * 2),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: List.generate(widget.count, (i) {
            return ScaleTransition(
              scale: DelayTween(begin: 1.0, end: 1.3, delay: i * 0.2).animate(_controller),
              child: SizedBox.fromSize(size: Size.square(widget.size * 0.5), child: _itemBuilder(i)),
            );
          }),
        ),
      ),
    );
  }

  Widget _itemBuilder(int index) => widget.itemBuilder != null
      ? widget.itemBuilder!(context, index)
      : DecoratedBox(decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle));
}

/*
 Dot fade progress animation
 */

class WidgetDotFade extends StatefulWidget {

  const WidgetDotFade({
    Key? key,
    this.color,
    this.size = 18.0,
    this.count = 3,
    this.itemBuilder,
    this.duration = const Duration(milliseconds: 500),
    this.controller,
  })  : assert(!(itemBuilder is IndexedWidgetBuilder && color is Color) && !(itemBuilder == null && color == null),
  'You should specify either a itemBuilder or a color'),
        super(key: key);

  final Color? color;
  final double size;
  final int count;
  final IndexedWidgetBuilder? itemBuilder;
  final Duration duration;
  final AnimationController? controller;

  @override
  WidgetDotFadeState createState() => WidgetDotFadeState();

}

class WidgetDotFadeState extends State<WidgetDotFade> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();

    _controller = (widget.controller ?? AnimationController(
        vsync: this, duration: Duration(milliseconds: widget.count * widget.duration.inMilliseconds))
    )..repeat();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox.fromSize(
        size: Size(widget.size * 3.2, widget.size * 2),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: List.generate(widget.count, (i) {
            return FadeTransition(
              opacity: DelayTween(begin: 0.1, end: 1.0, delay: i * 0.2).animate(_controller),
              child: SizedBox.fromSize(size: Size.square(widget.size * 0.5), child: _itemBuilder(i)),
            );
          }),
        ),
      ),
    );
  }

  Widget _itemBuilder(int index) => widget.itemBuilder != null
      ? widget.itemBuilder!(context, index)
      : DecoratedBox(decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle));
}

Custom widgets for creating progress animations using dots, each with a different animation effect: bounce, grow, and fade.

The WidgetDotBounce, WidgetDotGrow, and WidgetDotFade classes all extend StatefulWidget, meaning that they have mutable state that can change over time. Each widget also has a corresponding State class, which manages the state of the widget.

All three classes have similar constructor parameters, such as color, size, and count, which control the appearance of the dots. itemBuilder is an optional parameter that can be used to define a custom widget to use for each dot. duration is the length of time for each animation cycle, and controller is an optional AnimationController that can be used to manually control the animation.

Each widget's State class has an AnimationController that is created in the initState() method and is disposed of in the dispose() method. The build() method of each widget creates a Row of dots, each of which is wrapped in a SlideTransition, ScaleTransition, or FadeTransition widget to create the desired animation effect. If itemBuilder is provided, it is used to create the dot widget; otherwise, a DecoratedBox widget with the specified color and shape is used.


Load image from folder assets/images/

/*Load image from folder assets/images/
 */
class Img {
  static String get(String name){
    return 'assets/images/'+name;
  }
}

..

delay_tween.dart

// Import the sin and pi functions from the math library with the name "math"
import 'dart:math' as math show sin, pi;

// Import the Animation class from the flutter/animation library
import 'package:flutter/animation.dart';

// Define a DelayTween class that extends the Tween class for double values
class DelayTween extends Tween<double> {

  // The constructor for the DelayTween class
  DelayTween({double? begin, double? end, required this.delay}) : super(begin: begin, end: end);

  // The delay value for the animation
  final double delay;

  // Override the lerp method to adjust the animation's progress based on the delay value
  @override
  double lerp(double t) {
    // Use the sin function from the math library to calculate a sine wave based on the current time and delay
    // Add 1 to the result to make sure it's always positive, then divide by 2 to get a value between 0 and 1
    return super.lerp((math.sin((t - delay) * 2 * math.pi) + 1) / 2);
  }

  // Override the evaluate method to call the lerp method with the animation's current value
  @override
  double evaluate(Animation<double> animation) => lerp(animation.value);
}

// Define a DelayTweenOffset class that extends the Tween class for Offset values
class DelayTweenOffset extends Tween<Offset> {

  // The constructor for the DelayTweenOffset class
  DelayTweenOffset({Offset? begin, Offset? end, required this.delay}) : super(begin: begin, end: end);

  // The delay value for the animation
  final double delay;

  // Override the lerp method to adjust the animation's progress based on the delay value
  @override
  Offset lerp(double t) {
    // Use the sin function from the math library to calculate a sine wave based on the current time and delay
    // Add 1 to the result to make sure it's always positive, then divide by 2 to get a value between 0 and 1
    return super.lerp((math.sin((t - delay) * 2 * math.pi) + 1) / 2);
  }

  // Override the evaluate method to call the lerp method with the animation's current value
  @override
  Offset evaluate(Animation<double> animation)  => lerp(animation.value);

}

..

..

..

Comments