top of page
  • Writer's pictureRobin Alex Panicker

A guide on Flutter Animations


A guide on Flutter Animations

Animations play a vital role in creating engaging and visually appealing user interfaces in mobile applications. Flutter, a popular open-source UI framework by Google, offers a robust set of tools for creating smooth and expressive animations.


In this comprehensive guide, we'll explore the world of Flutter animations, from the basics to more advanced techniques, accompanied by code samples to help you get started.


1. Introduction to Flutter Animations


Why Animations Matter


Animations provide a more dynamic and engaging user experience, guiding users through interface changes and interactions. They help convey important information, enhance the overall aesthetic of the app, and make interactions more intuitive.


Types of Animations in Flutter


Flutter offers several animation types:

  • Implicit Animations: These animations are built into existing widgets and can be triggered using widget properties, like AnimatedContainer or AnimatedOpacity.

  • Tween Animations: These animations interpolate between two values over a specified duration using Tween objects.

  • Physics-Based Animations: These animations simulate real-world physics, like springs or flings, to create natural-looking motion.

  • Custom Animations: For more complex scenarios, you can create your own custom animations using CustomPainter and AnimationController.

In this guide, we'll cover examples from each category to give you a well-rounded understanding of Flutter animations.


2. Basic Animations


Animated Container


The AnimatedContainer widget is a straightforward way to animate changes to a container's properties, such as its size, color, and alignment.

class BasicAnimatedContainer extends StatefulWidget {
  @override
  _BasicAnimatedContainerState createState() => _BasicAnimatedContainerState();
}

class _BasicAnimatedContainerState extends State<BasicAnimatedContainer> {
  double _width = 100.0;
  double _height = 100.0;
  Color _color = Colors.blue;

  void _animateContainer() {
    setState(() {
      _width = 200.0;
      _height = 200.0;
      _color = Colors.red;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _animateContainer,
      child: Center(
        child: AnimatedContainer(
          duration: Duration(seconds: 1),
          width: _width,
          height: _height,
          color: _color,
        ),
      ),
    );
  }
}


Animated Opacity


The AnimatedOpacity widget allows you to animate the opacity of a widget, making it appear or disappear smoothly.

class BasicAnimatedOpacity extends StatefulWidget {
  @override
  _BasicAnimatedOpacityState createState() => _BasicAnimatedOpacityState();
}

class _BasicAnimatedOpacityState extends State<BasicAnimatedOpacity> {
  bool _visible = true;

  void _toggleVisibility() {
    setState(() {
      _visible = !_visible;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedOpacity(
          duration: Duration(seconds: 1),
          opacity: _visible ? 1.0 : 0.0,
          child: FlutterLogo(size: 150.0),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _toggleVisibility,
          child: Text(_visible ? "Hide Logo" : "Show Logo"),
        ),
      ],
    );
  }
}


3. Tween Animations


Animating Widgets with Tween


Tween animations interpolate between two values over a specified duration. Here's an example of animating the position of a widget using a Tween:

class TweenAnimation extends StatefulWidget {
  @override
  _TweenAnimationState createState() => _TweenAnimationState();
}

class _TweenAnimationState extends State<TweenAnimation> {
  double _endValue = 200.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _endValue = _endValue == 200.0 ? 100.0 : 200.0;
        });
      },
      child: Center(
        child: TweenAnimationBuilder(
          tween: Tween<double>(begin: 100.0, end: _endValue),
          duration: Duration(seconds: 1),
          builder: (BuildContext context, double value, Widget? child) {
            return Container(
              width: value,
              height: value,
              color: Colors.blue,
            );
          },
        ),
      ),
    );
  }
}


Tween Animation Builder


The TweenAnimationBuilder widget is a versatile tool for building animations with Tweens. It allows you to define the tween, duration, and a builder function to create the animated widget.


4. Physics-Based Animations


Using AnimatedBuilder with Curves


Curves define the rate of change in an animation, affecting its acceleration and deceleration. The CurvedAnimation class allows you to apply curves to your animations. Here's an example of using AnimatedBuilder with a curve:

class CurvedAnimationDemo extends StatefulWidget {
  @override
  _CurvedAnimationDemoState createState() => _CurvedAnimationDemoState();
}

class _CurvedAnimationDemoState extends State<CurvedAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );
    final Animation curveAnimation = CurvedAnimation(
      parent: _controller,
      curve: Curves.easeInOut,
    );
    _animation = Tween<double>(begin: 0, end: 200).animate(curveAnimation);
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _animation,
        builder: (BuildContext context, Widget? child) {
          return Container(
            width: _animation.value,
            height: 100,
            color: Colors.blue,
          );
        },
      ),
    );
  }

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


Creating a Spring Animation


Spring animations simulate the behavior of a spring, creating a bounce-like effect. Flutter provides the SpringSimulation class for this purpose. Here's an example of creating a spring animation:

class SpringAnimationDemo extends StatefulWidget {
  @override
  _SpringAnimationDemoState createState() => _SpringAnimationDemoState();
}

class _SpringAnimationDemoState extends State<SpringAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Offset> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );

    final SpringDescription spring = SpringDescription(
      mass: 1,
      stiffness: 500,
      damping: 20,
    );

    final SpringSimulation springSimulation = SpringSimulation(
      spring,
      _controller.value,
      1, // The target value
      0, // The velocity
    );

    _animation = Tween<Offset>(begin: Offset.zero, end: Offset(2, 0))
        .animate(_controller);

    _controller.animateWith(springSimulation);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: SlideTransition(
        position: _animation,
        child: Container(
          width: 100,
          height: 100,
          color: Colors.blue,
        ),
      ),
    );
  }

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


5. Complex Animations


Staggered Animations


Staggered animations involve animating multiple widgets with different delays, creating an appealing sequence. The StaggeredAnimation class manages this behavior. Here's an example:

class StaggeredAnimationDemo extends StatefulWidget {
  @override
  _StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}

class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo>
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );

    final StaggeredAnimation staggeredAnimation = StaggeredAnimation(
      controller: _controller,
      itemCount: 3,
    );

    _animation = Tween<double>(begin: 0, end: 200).animate(staggeredAnimation);

    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ListView.builder(
        itemCount: 3,
        itemBuilder: (BuildContext context, int index) {
          return FadeTransition(
            opacity: _animation,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Container(
                width: _animation.value,
                height: 100,
                color: Colors.blue,
              ),
            ),
          );
        },
      ),
    );
  }

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

class StaggeredAnimation extends Animatable<double> {
  final AnimationController controller;
  final int itemCount;

  StaggeredAnimation({
    required this.controller,
    required this.itemCount,
  }) : super();

  @override
  double transform(double t) {
    int itemCount = this.itemCount;
    double fraction = 1.0 / itemCount;
    return (t * itemCount).clamp(0.0, itemCount - 1).toDouble() * fraction;
  }
}


Hero Animations


Hero animations are used to smoothly transition a widget between two screens. They provide a seamless experience as the widget scales and moves from one screen to another. Here's an example:

class HeroAnimationDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        Navigator.of(context).push(
          MaterialPageRoute<void>(
            builder: (BuildContext context) {
              return Scaffold(
                appBar: AppBar(
                  title: const Text('Hero Animation'),
                ),
                body: Center(
                  child: Hero(
                    tag: 'hero-tag',
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.blue,
                    ),
                  ),
                ),
              );
            },
          ),
        );
      },
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Hero Animation'),
        ),
        body: Center(
          child: Hero(
            tag: 'hero-tag',
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ),
        ),
      ),
    );
  }
}


6. Implicit Animations


Animated CrossFade


The AnimatedCrossFade widget smoothly transitions between two children while crossfading between them. It's useful for scenarios like toggling between two pieces of content.

class CrossFadeDemo extends StatefulWidget {
  @override
  _CrossFadeDemoState createState() => _CrossFadeDemoState();
}

class _CrossFadeDemoState extends State<CrossFadeDemo> {
  bool _showFirst = true;

  void _toggle() {
    setState(() {
      _showFirst = !_showFirst;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedCrossFade(
          firstChild: FlutterLogo(size: 150),
          secondChild: Container(color: Colors.blue, width: 150, height: 150),
          crossFadeState:
              _showFirst ? CrossFadeState.showFirst : CrossFadeState.showSecond,
          duration: Duration(seconds: 1),
        ),
        ElevatedButton(
          onPressed: _toggle,
          child: Text(_showFirst ? 'Show Second' : 'Show First'),
        ),
      ],
    );
  }
}


Animated Switcher


The AnimatedSwitcher widget allows smooth transitions between different children based on a key. It's commonly used for transitions like swapping widgets.

class SwitcherDemo extends StatefulWidget {
  @override
  _SwitcherDemoState createState() => _SwitcherDemoState();
}

class _SwitcherDemoState extends State<SwitcherDemo> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedSwitcher(
          duration: Duration(seconds: 1),
          child: Text(
            '$_counter',
            key: ValueKey<int>(_counter),
            style: TextStyle(fontSize: 48),
          ),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _incrementCounter,
          child: Text('Increment'),
        ),
      ],
    );
  }
}


7. Custom Animations


CustomPainter and AnimationController


The combination of CustomPainter and AnimationController allows you to create complex animations and draw custom shapes. Here's an example of a rotating custom animation using CustomPainter:

class CustomPainterAnimation extends StatefulWidget {
  @override
  _CustomPainterAnimationState createState() => _CustomPainterAnimationState();
}

class _CustomPainterAnimationState extends State<CustomPainterAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    )..repeat();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
        animation: _controller,
        builder: (BuildContext context, Widget? child) {
          return CustomPaint(
            painter: RotatingPainter(_controller.value),
            child: Container(width: 150, height: 150),
          );
        },
      ),
    );
  }

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

class RotatingPainter extends CustomPainter {
  final double rotation;

  RotatingPainter(this.rotation);

  @override
  void paint(Canvas canvas, Size size) {
    canvas.translate(size.width / 2, size.height / 2);
    canvas.rotate(rotation * 2 * pi);
    final rect = Rect.fromCenter(
      center: Offset(0, 0),
      width: size.width * 0.8,
      height: size.height * 0.8,
    );
    final paint = Paint()..color = Colors.blue;
    canvas.drawRect(rect, paint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}


Creating a Flip Card Animation


Using a combination of Transform, GestureDetector, and AnimationController, you can create a flip card animation.

class FlipCardDemo extends StatefulWidget {
  @override
  _FlipCardDemoState createState() => _FlipCardDemoState();
}

class _FlipCardDemoState extends State<FlipCardDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  bool _isFront = true;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(milliseconds: 500),
    );
  }

  void _flipCard() {
    if (_isFront) {
      _controller.forward();
    } else {
      _controller.reverse();
    }
    _isFront = !_isFront;
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: _flipCard,
        child: AnimatedBuilder(
          animation: _controller,
          builder: (BuildContext context, Widget? child) {
            final double rotationValue = _controller.value;
            final double rotationAngle = _isFront ? rotationValue : (1 - rotationValue);
            final frontRotation = Matrix4.identity()
              ..setEntry(3, 2, 0.001)
              ..rotateY(pi * rotationAngle);
            final backRotation = Matrix4.identity()
              ..setEntry(3, 2, 0.001)
              ..rotateY(pi * (rotationAngle - 1));
            return Stack(
              children: [
                _buildCard(frontRotation, 'Front', Colors.blue),
                _buildCard(backRotation, 'Back', Colors.red),
              ],
            );
          },
        ),
      ),
    );
  }

  Widget _buildCard(Matrix4 transform, String text, Color color) {
    return Center(
      child: Transform(
        transform: transform,
        alignment: Alignment.center,
        child: Container(
          width: 200,
          height: 300,
          color: color,
          alignment: Alignment.center,
          child: Text(
            text,
            style: TextStyle(fontSize: 24, color: Colors.white),
          ),
        ),
      ),
    );
  }

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


8. Performance Optimization


Using the AnimationController's vsync


When creating an AnimationController, it's essential to provide a vsync parameter. This parameter helps in syncing the animation frame rate with the device's refresh rate, enhancing performance and reducing unnecessary updates.

class VsyncAnimation extends StatefulWidget {
  @override
  _VsyncAnimationState createState() => _VsyncAnimationState();
}

class _VsyncAnimationState extends State<VsyncAnimation>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this, // Pass `this` as the vsync parameter
      duration: Duration(seconds: 2),
    );
  }

  // ...
}


Avoiding Unnecessary Rebuilds


To avoid unnecessary rebuilds of widgets, you can use AnimatedBuilder or ValueListenableBuilder. These widgets rebuild only when the animation value changes, improving overall performance.

class AvoidRebuildsDemo extends StatefulWidget {
  @override
  _AvoidRebuildsDemoState createState() => _AvoidRebuildsDemoState();
}

class _AvoidRebuildsDemoState extends State<AvoidRebuildsDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 2),
    );

    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ValueListenableBuilder(
        valueListenable: _animation,
        builder: (BuildContext context, double value, Widget? child) {
          return Transform.scale(
            scale: value,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          );
        },
      ),
    );
  }

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


9. Chaining and Sequencing Animations


Using Future.delayed


You can chain animations by using Future.delayed. This creates a delayed effect, allowing one animation to start after the previous one completes.

class DelayedAnimationDemo extends StatefulWidget {
  @override
  _DelayedAnimationDemoState createState() => _DelayedAnimationDemoState();
}

class _DelayedAnimationDemoState extends State<DelayedAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation1;
  late Animation<double> _animation2;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation1 = Tween<double>(begin: 0, end: 1).animate(_controller);

    _animation2 = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(
        parent: _controller,
        curve: Interval(0.5, 1.0), // Starts after the first animation
      ),
    );

    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ScaleTransition(
            scale: _animation1,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ),
          SizedBox(height: 20),
          ScaleTransition(
            scale: _animation2,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
          ),
        ],
      ),
    );
  }

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


Using AnimationController's addListener


The addListener method of AnimationController can be used to sequence animations, triggering the second animation when the first animation completes.

class SequenceAnimationDemo extends StatefulWidget {
  @override
  _SequenceAnimationDemoState createState() => _SequenceAnimationDemoState();
}

class _SequenceAnimationDemoState extends State<SequenceAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation1;
  late Animation<double> _animation2;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 1),
    );

    _animation1 = Tween<double>(begin: 0, end: 1).animate(_controller)
      ..addListener(() {
        if (_animation1.isCompleted) {
          _controller.reset(); // Reset the controller to restart
          _controller.forward(); // Start the second animation
        }
      });

    _animation2 = Tween<double>(begin: 0, end: 1).animate(_controller);

    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          ScaleTransition(
            scale: _animation1,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.blue,
            ),
          ),
          SizedBox(height: 20),
          ScaleTransition(
            scale: _animation2,
            child: Container(
              width: 100,
              height: 100,
              color: Colors.red,
            ),
          ),
        ],
      ),
    );
  }

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


10. Conclusion and Further Learning


Flutter's animation capabilities allow you to create stunning, dynamic user interfaces that engage users and enhance their experience. This guide covered a wide range of animation techniques, from basic animations and tween animations to physics-based simulations and complex custom animations.


As you continue your journey with Flutter animations, consider exploring more advanced topics like Flare animations for vector graphics, using Rive for more complex animations, and experimenting with implicit animations for seamless UI changes.


Remember, mastering Flutter animations takes practice and experimentation. With dedication and creativity, you can bring your app's UI to life and create memorable user experiences that leave a lasting impression.


Happy animating! 🚀


Note: The code samples provided in this blog post are simplified for illustrative purposes. Actual implementation may require additional considerations and optimizations.

Blog for Mobile App Developers, Testers and App Owners

 

This blog is from Finotes Team. Finotes is a lightweight mobile APM and bug detection tool for iOS and Android apps.

In this blog we talk about iOS and Android app development technologies, languages and frameworks like Java, Kotlin, Swift, Objective-C, Dart and Flutter that are used to build mobile apps. Read articles from Finotes team about good programming and software engineering practices, testing and QA practices, performance issues and bugs, concepts and techniques. 

bottom of page