Kangabru logo Kangabru logo text
Articles Portfolio

Zero to hero - Part 3 - Awesome Animations in Flutter

I'm deep diving into how I built my app with Flutter. Today I showcase how easy animations are to make and how I used them in my app.
Post banner
June 2020

Contents


Hey bru. Last month I launched my first app Icing Addict - one of 12 startup projects I’m building this year. This series is a deep dive into how I built it.

Just getting started with Flutter? Check out the first post or the game itself here:

Overview

Alright so this post is intended to showcase how I use animations in my Flutter app.

I have tons of examples which include videos, high level overviews, and code to get you started.

As always get in touch if you have any questions. Let’s do this!

Animation basics

My post is intended to inspire you and get you started. As such I won’t go into the nitty gritty details of the animation code.

Instead use these links for more info on Flutter animations (they do a much better job anyway):

Gist References

I’ve uploaded a few gists which I will reference throughout this post. These are:


Scale Animations

Aight let’s start off with scale animations. I use these to pop elements in and out nicely.

Combine these with a delay and you’ve got a great combo to make elements stand out 👌

The first thing you see in my app is the splash screen. I thought why not make it pop?

As you can see the logo animates nicely on load.

I would prefer not to have a splash screen but I use it to load my home screen assets before showing them.

This is how I made it:

I use my AnimateScale and FirstLoad code to achieve this. Note I also use the loadSvg function from last week’s post.

import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'animation_mixins.dart'; // See my code reference for this gist
import 'animation_widgets.dart'; // See my code reference for this gist

class AnimatedLogo extends StatefulWidget {
  @override
  _AnimatedLogoState createState() => _AnimatedLogoState();
}

class _AnimatedLogoState extends State<AnimatedLogo> with FirstLoad {
    bool _isVisible = false;

    /// Preload svgs before animating
    loadSvgs() => Future.wait([
            loadSvg(context, LOGO_RIGHT),
            loadSvg(context, LOGO_LEFT),
            loadSvg(context, LOGO_MID),
            loadSvg(context, LOGO_TEXT),
        ]).then((_) => setState(() => _isVisible = true));

    @override
    Widget build(BuildContext context) {
        onFirstLoad(loadSvgs); // I need context to be initialised so can't use initState

        return Visibility(
            visible: _isVisible,
            child: ConstrainedBox(
                constraints: BoxConstraints(maxHeight: 200),
                child: Stack(
                children: [
                    _AnimatedStar(LOGO_RIGHT, DELAY_3),
                    _AnimatedStar(LOGO_LEFT,  DELAY_2),
                    _AnimatedStar(LOGO_MID,   DELAY_1),
                    _AnimatedStar(LOGO_TEXT, 0),
                ],
            )));
    }
}

class _AnimatedStar extends AnimatedScale {
  _AnimatedStar(String svgPath, int delay)
      : super(
          child: SvgPicture.asset(svgPath),
          curve: Curves.elasticOut,
          delay: Duration(milliseconds: delay),
          duration: Duration(milliseconds: DURATION),
        );
}

Score card

Ok there’s a few things going on here.

I’ll focus on the stars and score animations here. Check out the next section for the popup animation.

To animate the score number I use my AnimatedInt function. It’s just a widget you can use like AnimatedInt(value: 99). Super easy.

The stars are similar to my main logo animation. I use my AnimatedScale widget on each of the star SVGs, and stagger the delay to create the effect.

I use popups to display content related to certain screens.

I like to pop them in and out to provide useful visual context as you can see.

To create this effect I simply wrap the built-in popup and apply a scale transition.

Check out the code below.

import 'package:flutter/material.dart';

// Creates a popup with the given widget, a scale animation, and faded background.
Future<T> showPopupDialog<T>(BuildContext context, Widget child) {
    return showGeneralDialog<T>(
        context: context,
        barrierColor: Colors.black54,
        transitionDuration: const Duration(milliseconds: 200),
        barrierDismissible: false,
        useRootNavigator: true,
        pageBuilder: (BuildContext buildContext, _, __) =>
            SafeArea(child: Builder(builder: (BuildContext context) => child)),
        transitionBuilder: (context, animation, _, child) {
            return ScaleTransition(
                scale: CurvedAnimation(parent: animation, curve: Curves.decelerate),
                child: child,
            );
        },
    );
}

Size

Sometimes content isn’t ready when a screen loads. Instead of having content jump into view, I like to animate the content into view.

You can see in the video that the scores load slightly after the page loads. The container animates nicely to accommodate this.

To do this I wrap content in the built-in AnimatedSize widget. Now any parent container will animate when content changes size!

import 'package:flutter/widgets.dart';

class GrowbleContainer extends StatefulWidget {
    final Widget child:
    GrowbleContainer({this.child});
    createState() => GrowbleContainerState();
}

class GrowbleContainerState extends State<GrowbleContainer> with TickerProviderStateMixin {
    @override
    Widget build(BuildContext context) {
        return AnimatedSize(
            vsync: this, // Provided by TickerProviderStateMixin
            duration: Duration(milliseconds: 200),
            curve: Curves.fastOutSlowIn,
            child: widget.child,
        );
    }
}

Popping buttons

I use a common button throughout my app which I show/hide dynamically.

I thought it’d be nicer for them to pop in and out as you can see.

Much cooler than just having them appear suddenly no?

You can achieve this by wrapping a widget with my AnimatedScale widget and updating the scale dynamically like this.

import 'package:flutter/material.dart';
import 'animation_widgets.dart'; // See my code reference for this gist

class PoppingContainer extends StatefulWidget {
  final Widget child;
  final bool visible;
  PoppingContainer({
    this.child,
    this.visible = true,
  });

  @override
  State<StatefulWidget> createState() => PoppingContainerState();
}

class PoppingContainerState extends State<PoppingContainer> {
  @override
  Widget build(BuildContext context) {
    return AnimatedScale(
      tweenInit: Tween(begin: 1.0, end: 1.0),
      tween: Tween(begin: 0.0, end: widget.visible ? 1.0 : 0.0), // Update scale dynamically
      duration: Duration(milliseconds: 600),
      curve: Curves.elasticOut,
      child: widget.child,
    );
  }
}


Transition Animations

This section covers how I use transitions to make navigating between screens more interesting.

Hero transition

One amazing feature of Flutter is the ‘hero’ animation support. This is where an element will transition between two separate screens.

My video shows an example where my cookie is animated to and from the select screen and game screen.

It’s so incredibly easy to implement that you have no excuse not to use it!

You literally just have to wrap a common widget with the Hero widget on each screen.

Hero(
    tag: "some_unique_string_id",
    child: YourWidget(),
),

The animation is done automagically and handles scaling and translations 👌

Note that you can’t have multiple widgets on one screen with the same ID (like the cookie select screen in the video).

In my case I use the level name so that each ID is unique, but still available to use from the select and game screens.

Fade in

In the hero animation video above you’ll notice there’s a fade transition going on too.

You can implement this easily as follows:

import 'package:flutter/widgets.dart';

/// Fades to a new route rendering the given widget.
class FadeRoute<T> extends PageRouteBuilder<T> {
    FadeRoute(Widget page)
        : super(
            pageBuilder: (_, __, ___) => page,
            transitionDuration: const Duration(milliseconds: 600),
            transitionsBuilder: (context, animation, secondaryAnimation, child) =>
                FadeTransition(opacity: animation, child: child),
            );
}

/// Simple wrapper which fades to a new route rendering the given widget.
Future<T> fadeToRoute<T>(BuildContext context, Widget page) =>
    Navigator.push<T>(context, FadeRoute<T>(page));

// Use manually like this
Navigator.push(context, FadeRoute(MyWidget()));

// Or use the wrapper like this
fadeToRoute(context, MyWidget());

Slide in

Sliding is also quite easy to do. This code slides in and out from the left of the screen

import 'package:flutter/widgets.dart';

/// Slide right to a new route rendering the given widget.
class RightSlideRoute<T> extends PageRouteBuilder<T> {
    RightSlideRoute(Widget page)
        : super(
            pageBuilder: (_, __, ___) => page,
            transitionsBuilder: (_, animation, __, child) => SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(-1, 0),
                    end: Offset.zero,
                  ).animate(animation),
                  child: child,
                ));
}

/// Simple wrapper which slide right to a new route rendering the given widget.
Future<T> slideToRoute<T>(BuildContext context, Widget page) =>
    Navigator.push<T>(context, RightSlideRoute<T>(page));

// Use manually like this
Navigator.push(context, RightSlideRoute(MyWidget()));

// Or use the wrapper like this
slideToRoute(context, MyWidget());

Other Animations

Here are some other ways I use animations in my app.

Content slider

Why flip between images when you can slide between them?

I use a great library called flutter_swiper which provides all sorts of sliding magic.

Here you can see I use it to navigate between content. You can interact with it too!

One extra thing here is how I’m animating the background. I use the built-in AnimatedContainer and simply update the colour when the slider updates.

This code shows how I set up the slider widget and animate the background as it slides:

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

class SwiperExample extends StatefulWidget {
  final List<String> images;
  final List<Color> colors;
  SwiperExample({this.images, this.colors});

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

class SwiperExampleState extends State<SwiperExample> {
  int _imageIndex = 0;

  @override
  Widget build(BuildContext context) {
    var color = widget.colors[_imageIndex % widget.colors.length];

    return AnimatedContainer(
      color: color,
      duration: Duration(milliseconds: 1500),
      child: Swiper(
        loop: true,
        autoplay: true,
        autoplayDelay: _TRANSITION_DURATION,
        onIndexChanged: (index) => setState(() => _imageIndex = index),
        itemCount: widget.images.length,
        itemBuilder: (BuildContext context, int index) {
          var image = widget.images[index];
          return Image.asset(image);
        },
      ),
    );
  }
}

List slider

I built a custom colour picker for my app. It’s a simple scrollable list which you can see in the video.

A problem I had when I launched was that users didn’t realise the list was scrollable!

So I added a nice little animation which indicates that there are more colours to choose from.

As you can see the widget scrolls slightly when the list is loaded.

Check out my list slider gist which contains the widget. Use it like any another list: ListPicker(children: <Widget>[...]).

It basically wraps the built-in SingleChildScrollView widget and performs a scroll on load. It’s was a little tricky to get right but it was worth it.

Button state toggle

I created my own button widget to achieve a particular style I was after.

Unfortunately that meant I had to roll my own animations too!

You can see in the video that I have a subtle press state animation as the user clicks the button.

To implement this I use the AnimatedContainer widget and adjust the shadow size and margin to make it looked pressed.

Check out this code which creates the shadow look, and animates it when pressed or not.

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

const double _SHADOW_SIZE_MIN = 2.0;
const double _SHADOW_SIZE_MAX = 6.0;

class ShadowContainer extends StatelessWidget {
  final Widget child;
  final bool isPressed;
  final EdgeInsetsGeometry padding, margin;

  ShadowContainer({
    @required this.child,
    this.isPressed: false,
    this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    this.margin = const EdgeInsets.fromLTRB(5, 5, 5, 10),
  });

  @override
  Widget build(BuildContext context) {

    // Make the shadow large when normal and small when pressed
    var shadowOffset = isPressed ? _SHADOW_SIZE_MIN : _SHADOW_SIZE_MAX;

    // Adjust the button down when pressed and up when normal
    var marginOffset = isPressed ? _SHADOW_SIZE_MAX - _SHADOW_SIZE_MIN : 0;
    var marginTop = EdgeInsets.only(top: marginOffset);
    var marginBot = EdgeInsets.only(bottom: marginOffset);

    return AnimatedContainer(
        child: child,
        padding: padding,
        margin: margin.add(marginTop).subtract(marginBot),
        duration: Duration(milliseconds: 30),
        decoration: new BoxDecoration(boxShadow: [
          BoxShadow(
              color: Colors.black,
              offset: Offset.fromDirection(pi / 2, shadowOffset))
        ]));
  }
}

Well that’s it! Hope you learned something or were inspired to add animations to your app.

Once you get the hang of them Flutter animations are quite simple to implement. All you have to do is start!

As always reach out if have questions and see you next time!