Category Image

Mobile Dev

Flutter animations - four different ways to implement animations in your flutter app

August 31, 2023
Flutter animations - four different ways to implement animations in your flutter app

Flutter animations - four different ways to implement animations in your flutter app

Introduction:

In the world of mobile app development, animations serve as the glue that binds functionality and aesthetics, breathing life into user interfaces and creating delightful user experiences. As the demand for rich and interactive apps continues to rise, developers are seeking tools and frameworks that facilitate the seamless integration of captivating animations into their projects.

Enter Flutter, a cross-platform UI toolkit developed by Google. Renowned for its ability to create stunning and performant user interfaces, Flutter offers a variety of animation techniques that empower developers to bring their apps to life. In this blog post, we'll embark on a journey through the realm of Flutter animations, exploring four distinct approaches to achieving captivating motion within your apps.

From the simplicity of third-party packages to the intricate control offered by Flutter's built-in animation framework, we'll delve into each method's strengths, weaknesses, and ideal use cases. By the end of this post, you'll have a comprehensive understanding of different animation techniques, enabling you to make informed decisions when animating your Flutter applications.

So, whether you're a newcomer to Flutter seeking to enhance your app's visual appeal or an experienced developer aiming to master advanced animation strategies, join us as we unravel the magic of animations in Flutter. Let's dive into the world of motion, creativity, and user engagement that Flutter animations offer.

See the code for this whole project on: https://github.com/NakuDoka/flutter_animations

1. Flutter's Built-in Animation Framework

Flutter provides a robust built-in animation framework that empowers developers to create smooth and interactive animations. This framework utilizes the concepts of AnimationControllers, Tweens, and AnimatedWidgets to orchestrate animations seamlessly. Let's explore this technique through the following examples.

A. Heart Animation with AnimationController and AnimatedBuilder

Imagine implementing a heart icon that scales and changes color when tapped. Here's how you can achieve this effect using AnimationController and AnimatedBuilder:

class _HeartState extends State<Heart> with SingleTickerProviderStateMixin {
  bool isFav = false;
  late AnimationController _controller;
  late Animation<Color?> _colorAnimation;
  late Animation<double> _sizeAnimation;
  late Animation<double> _curve;

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

    _controller = AnimationController(
      duration: const Duration(milliseconds: 200),
      vsync: this,
    );

    _curve = CurvedAnimation(parent: _controller, curve: Curves.slowMiddle);

    _colorAnimation = ColorTween(begin: Colors.grey[400], end: Colors.red).animate(_curve);

    _sizeAnimation = TweenSequence(<TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 30, end: 50),
        weight: 50,
      ),
      TweenSequenceItem<double>(
        tween: Tween<double>(begin: 50, end: 30),
        weight: 50,
      ),
    ]).animate(_curve);

    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        setState(() {
          isFav = true;
        });
      }
      if (status == AnimationStatus.dismissed) {
        setState(() {
          isFav = false;
        });
      }
    });

// dismiss the animation when widgit exits screen
  @override
  void dispose() {
    super.dispose();
    _controller.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
        animation: _controller,
        builder: (BuildContext context, _) {
          return IconButton(
            icon: Icon(
              Icons.favorite,
              color: _colorAnimation.value,
              size: _sizeAnimation.value,
            ),
            onPressed: () {
              isFav ? _controller.reverse() : _controller.forward();
            },
          );
        });
  }
}

In this example, _controller is an AnimationController responsible for managing the animation's state. _colorAnimation and _sizeAnimation are animations that control the heart icon's color and size respectively. When the icon is tapped, the _controller toggles between forward and reverse animations.

B. TweenAnimationBuilder for Screen Title Animation

Suppose you want to animate the appearance of a screen title with a fading and sliding effect. Here's how you can achieve this using TweenAnimationBuilder:

import 'package:flutter/material.dart';

class ScreenTitle extends StatelessWidget {
  // Constructor and build...

  @override
  Widget build(BuildContext context) {
    return TweenAnimationBuilder(
      tween: Tween<double>(begin: 0, end: 1),
      duration: Duration(milliseconds: 500),
      curve: Curves.easeIn,
      builder: (BuildContext context, double _val, Widget? child) {
        return Opacity(
          opacity: _val,
          child: Padding(padding: EdgeInsets.only(top: _val * 20), child: child),
        );
      },
      child: Text(
        text,
        style: const TextStyle(fontSize: 36, color: Colors.white, fontWeight: FontWeight.bold),
      ),
    );
  }
}

In this example, TweenAnimationBuilder smoothly transitions the opacity and padding of the title text, creating an elegant fading and sliding animation.

C. AnimatedList for Trip List Animation

Animating lists can be captivating, and Flutter's AnimatedList simplifies the process. Consider animating the appearance of a list of trips using SlideTransition:

class _TripListState extends State<TripList> {
  final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
  final List<Widget> _tripTiles = [];

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _addTrips();
    });
  }
void _addTrips() {
    // get data from db
    List<Trip> trips = [
      Trip(title: 'Beach Paradise', price: '350', nights: '3', img: 'beach.png'),
      Trip(title: 'City Break', price: '400', nights: '5', img: 'city.png'),
      Trip(title: 'Ski Adventure', price: '750', nights: '2', img: 'ski.png'),
      Trip(title: 'Space Blast', price: '600', nights: '4', img: 'space.png'),
    ];

    Future ft = Future(() {});
    for (var trip in trips) {
      ft = ft.then((data) {
        return Future.delayed(const Duration(milliseconds: 100), () {
          _tripTiles.add(_buildTile(trip));
          _listKey.currentState?.insertItem(_tripTiles.length - 1);
        });
      });
    }
  }

  Widget _buildTile(Trip trip) {
    return ListTile(
      onTap: () {
        Navigator.push(context, MaterialPageRoute(builder: (context) => Details(trip: trip)));
      },
      contentPadding: const EdgeInsets.all(25),
      title: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text('${trip.nights} nights',
              style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.blue[300])),
          Text(trip.title, style: TextStyle(fontSize: 20, color: Colors.grey[600])),
        ],
      ),
      leading: ClipRRect(
        borderRadius: BorderRadius.circular(8.0),
        child: Hero(
          tag: 'location-img-${trip.img}',
          child: Image.asset(
            'images/${trip.img}',
            height: 50.0,
          ),
        ),
      ),
      trailing: Text('\\$${trip.price}'),
    );
  }

final Tween<Offset> _offset = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));

  @override
  Widget build(BuildContext context) {
    return AnimatedList(
        key: _listKey,
        initialItemCount: _tripTiles.length,
        itemBuilder: (context, index, animation) {
          return SlideTransition(
            position: animation.drive(_offset),
            child: _tripTiles[index],
          );
        });
  }
}

In this example, _addTrips method adds trips to the list with a slide-in animation using SlideTransition and _offset. The _offset Tween controls the sliding animation's direction.

The Hero Widget for Seamless Transitions

In Flutter, the Hero widget allows smooth transitions between two screens by animating shared elements. This is achieved by providing the same tag to elements on both screens. Let's see how you're using it:

Page 1 (List of Trips):

Hero(
  tag: 'location-img-${trip.img}',
  child: Image.asset(
    'images/${trip.img}',
    height: 50.0,
  ),
),

Page 2 (Details Screen):

Hero(
  tag: 'location-img-${trip.img}',
  child: Image.asset(
    'images/${trip.img}',
    height: 360,
    fit: BoxFit.cover,
    alignment: Alignment.topCenter,
  ),
),

By using the same tag for the image on both screens, Flutter smoothly transitions the image between the list and details pages, providing a visually pleasing effect

Conclusion

Flutter's built-in animation framework offers a powerful arsenal of tools for creating visually appealing and interactive animations. By combining AnimationControllers, Tweens, and AnimatedBuilders, developers can effortlessly achieve captivating motion effects that enhance the user experience. This technique provides granular control over animations and enables developers to customize their app's look and feel down to the finest details. Link to package: https://pub.dev/packages/flutter_animate

In the next section, we'll explore another popular animation method: flutter_animate

2. Flutter_Animate Package

To explore another method of achieving animations in Flutter, I decided to recreate the same animations we discussed earlier using the flutter_animate package. This package offers a straightforward way to animate widgets with various effects, making it an attractive choice for developers seeking simplicity in their animations.

A. Screen Title Animation

In the previous section, we used TweenAnimationBuilder to create a smooth title animation. With flutter_animate, the same animation can be achieved with a more concise syntax:

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

class ScreenTitle extends StatelessWidget {
  final String text;

  const ScreenTitle({super.key, required this.text});

  @override
  Widget build(BuildContext context) {
    return Text(
      text,
      style: TextStyle(fontSize: 36, color: Colors.white, fontWeight: FontWeight.bold),
    )
        .animate()
        .fadeIn(duration: Duration(milliseconds: 500), curve: Curves.easeIn)
        .slideY(duration: Duration(milliseconds: 500), curve: Curves.easeIn);
  }
}

Here, the flutter_animate package streamlines the animation creation process, offering a convenient and readable way to apply both fade-in and vertical sliding effects to the title.

B. Trip List Animation

Similarly, the TripList animation using flutter_animate is achieved with a simpler structure:

dartCopy code
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

class TripList extends StatefulWidget {
  // ... (constructor and other methods)

  @override
  Widget build(BuildContext context) {
    return Column(
      children: AnimateList(
          delay: 100.ms,
          interval: 100.ms,
          effects: [SlideEffect(begin: Offset(1, 0), end: Offset(0, 0))],
          children: _tripTiles),
    );
  }
}

The AnimateList widget, part of flutter_animate, allows us to apply a slide-in effect to each trip tile in the list seamlessly. The delay, interval, and effects parameters make it easy to customize the animation's timing and characteristics.

C. Heart Animation

One interesting point to note is that I was unable to recreate the heart icon animation using flutter_animate. This illustrates the importance of choosing the appropriate animation method for the task at hand. The rich functionalities provided by Flutter's built-in animation framework might be more suitable for complex interactions like the heart animation.

In Conclusion

Exploring the flutter_animate package provides insights into a simplified approach to animations. While it proves effective for certain animations, more intricate interactions might still require Flutter's native animation capabilities. Knowing when to choose between different animation techniques is key to creating fluid and engaging user experiences.

In the next section, we'll delve into another exciting realm of animation: Rive for Flutter.

3. Rive for Flutter

Rive is an exciting animation and design tool that seamlessly integrates with Flutter, enabling developers to create complex animations and interactive designs for their apps. With Rive, animations can be built and edited visually, offering a rich set of features for creating captivating user experiences. Read more here: https://rive.app/

A. Using Rive for One-Shot Animations

To illustrate Rive's capabilities, I decided to implement a loading animation using the RiveAnimation widget. This example demonstrates a simple loading animation that plays once and then stops.

class RiveAnimationTest extends StatefulWidget {
  /// Controller for playback
  late RiveAnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = OneShotAnimation(
      'walk',
      autoplay: true,
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // ... (other widgets)
        RiveAnimation.asset(
          'rives/load.riv',
          controllers: [_controller],
          fit: BoxFit.fitHeight,
        ),
        // ... (other widgets)
      ],
    );
  }
}

In this case, the RiveAnimation.asset widget displays an animation from the 'rives/load.riv' file. The animation is controlled by the _controller, which plays the animation once and stops.

B. Leveraging Rive's State Machines

Rive's power truly shines when utilizing state machines to manage complex animations. For instance, consider the scenario where users choose between two modes, 'Numbers' and 'Images', each accompanied by a different animation.

class RiveStateTest extends StatefulWidget {
  SMINumber? _bump;

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

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

  bool isNumbers = false;

  void _onRiveInit(Artboard artboard) {
    final controller = StateMachineController.fromArtboard(artboard, 'loop');
    artboard.addController(controller!);
    _bump = controller.findInput<double>('state') as SMINumber;
  }

  void _hitBump(int value) => _bump?.value = value.toDouble();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        // ... (other widgets)
        RiveAnimation.asset(
          'rives/choose_mode.riv',
          stateMachines: const ['loop'],
          onInit: _onRiveInit,
          fit: BoxFit.fitHeight,
        ),
        // ... (other widgets)
      ],
    );
  }
}

In this case, the RiveAnimation.asset widget is configured to work with state machines. The animation is driven by a state machine called 'loop', which enables dynamic transitions and interactions within the animation.

C. The Difference: One-Shot vs. State Machines

The key distinction between the two examples lies in their behavior. While the first example using OneShotAnimation plays a single animation, the second example using Rive's state machines offers more intricate control. State machines allow dynamic transitions between different states of the animation, offering a more advanced and interactive experience.

In Conclusion

Rive adds a new dimension to Flutter animations by offering both simple and complex animation capabilities. Whether you're looking to create a quick loading animation or build complex state-based interactions, Rive seamlessly integrates with Flutter, providing developers with powerful tools to elevate their app's visual appeal and user engagement.

In the next section, we'll explore the world of third-party animation packages found on pub.dev.

4. Exploring Third-party Animation Packages

Flutter's vibrant ecosystem on pub.dev offers an array of third-party animation packages, each tailored to different animation needs. These packages provide developers with ready-made solutions for adding delightful animations to their apps with minimal effort.

Exploring these packages can often save development time and effort, while still achieving engaging and visually appealing results. Let's take a quick look at some examples:

animated_toggle_switch:

auto_animated:

animated_text_kit:

5. Comparative Analysis

When to Choose Which Technique:

  • Flutter's Built-in Framework: Choose this for complex and interactive animations where fine-tuned control is required. This method offers robust performance and is well-supported by the Flutter community.
  • flutter_animate Package: Opt for this when you want to quickly add simple animations with minimal code. While it's easier to use, it might be less flexible for complex animations.
  • Rive for Flutter: Use Rive for sophisticated vector-based animations and interactive designs. State machines in Rive provide advanced control over animations, making it ideal for complex scenarios.

6. Best Practices for Animation in Flutter

Here are some best practices to keep in mind when working with animations in Flutter:

  • Subtlety and Smoothness: Focus on creating animations that enhance the user experience without overwhelming them. Smooth and gradual animations are often more pleasing to the eye.
  • Animation States: Efficiently manage animation states to avoid unnecessary resource consumption. Utilize widgets like AnimatedBuilder to animate specific parts of your UI, optimizing performance.
  • Performance Optimization: Consider performance implications when choosing animation techniques. Use techniques like AnimatedContainer to leverage Flutter's hardware acceleration for smoother animations.

Conclusion

In this exploration of animation techniques in Flutter, we've covered a spectrum of options to breathe life into your app's UI. From Flutter's built-in framework to external packages like flutter_animate and the powerful Rive tool, each technique offers a unique approach to animations.

As Flutter continues to evolve, developers have an ever-expanding toolkit to create captivating user experiences. Remember, the best technique depends on your project's needs and complexity. Embrace the dynamic Flutter community and resources to continue mastering the art of animations and taking your Flutter apps to new heights.

Whether you're aiming for elegance, interactivity, or a combination of both, Flutter's animation capabilities are at your disposal.

Happy animating!

Share