Animation Introduction and Implicit Animations

  1. Objectives
  2. Code along
  3. Boilerplate code
  4. Animations Introduction
  5. Implicit Animations
    1. Implicit Animation Widgets
  6. AnimatedPadding - a first example
  7. AnimatedContainer
    1. Animation Curves
  8. AnimatedDefaultTextStyle
  9. Implicit Animations vs. Explicit Animations
  10. Exercise
  11. Other resources
  12. Definitions

Objectives

Code along

Recording link: https://youtu.be/5uNXR_HpxS0

Boilerplate code

Create a new Flutter project and call it “animation_samples”. Paste the following boilerplate code over main.dart:

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    title: "Animation Demo",
    home: HomeScreen(),
  ));
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Animation Home")),
      body: const Placeholder(),
    );
  }
}

Animations Introduction

Colloquially, we associate the word animation with a changing object, often a picture that changes to convey motion. In Flutter, animation specifically refers to redrawing a widget over a period of time to reflect changes in the properties of that widget, such as size, position, or color.

If, when you hear animation, you think of cartoonish mascot changing its expression, that is best accomplished by using a graphic design tool to create a GIF or WEBM image file and then importing the image into your project. Animations of complex 2D or 3D games are achieved by using a game engine such as Unity. These are beyond our scope.

Animating the properties of widgets can achieve some powerful effects. But, with great power comes great responsibility. As a general rule, you should only use animations to: a) provide user feedback, or b) assist the user in navigating through a task. For example:

If you use animation, ensure that it benefits the user. Humans have evolved such that visual motion cues disrupt our attention. Flippant or unnecessary animations are a distraction.

Implicit Animations

The simplest form of Flutter animation is an implicit animation. Implicit animations are changes to a widget property over a period with a defined start and end point. For example, you tap an an Image and it grows to fill the screen.

Time is an important element to all animations. Implicit animations have a well-defined start, usually an event like a user tap or a file read, and a well-defined end when the property reaches its final value.

For example, suppose you have an Image widget that starts out 100px wide and 100px tall. You animate that Image widget so that it grows to 200px wide and 200px tall when the user taps on it. You can consider the widget to have an internal for-loop that increments the height and width values from 100 to 200 over a fixed period of time. Flutter redraws the widget with every tick of the time clock, gradually getting bigger with each tick, and that is what produces the animation.

So, again, animations are changing some widget property over time, and Flutter redrawing the widget with every tick of the time clock.

Implicit Animation Widgets

Flutter comes with a variety of built-in animation widgets that animate common properties. These implicit animation widgets are the easiest way to get animations into your app.

They are:

Note that all of these widgets also have non-animated versions: Positioned, Padding, Align, Container, etc.

The samples below demonstrate a few of these AnimatedX widgets.

AnimatedPadding - a first example

The Padding widget provides space around a child widget. The AnimatedPadding widget animates the change in space from a starting padding value to and ending padding value.

Add the following code to your main.dart:

// This code is adapted from the official Flutter example
class AnimatedPaddingExample extends StatefulWidget {
  const AnimatedPaddingExample({super.key});

  @override
  State<AnimatedPaddingExample> createState() => _AnimatedPaddingExampleState();
}

class _AnimatedPaddingExampleState extends State<AnimatedPaddingExample> {
  double padValue = 0.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        AnimatedPadding(
          padding: EdgeInsets.all(padValue),
          duration: const Duration(seconds: 2),
          child: Container(
            width: 350,
            height: 100,
            color: Colors.blue,
          ),
        ),
        Text('Padding: $padValue'),
        ElevatedButton(
            child: const Text('Change padding'),
            onPressed: () {
              setState(() {
                if (padValue == 0.0) {
                  padValue = 100.0;
                } else {
                  padValue = 0.0;
                }
              });
            }),
      ],
    );
  }
}

Change the body of your HomeScreen’s Scaffold to point to the AnimatedPaddingExample widget.

Notice how the AnimatedPadding widget has a padding property based on the padValue double. It also has a duration property which specifies the period of time over which the animation should take place. Currently, the duration is two seconds, but you could specify milliseconds, microseconds, hours, or days. These other time durations don’t make sense in the context of animation, but Duration has other uses in Flutter.

When the widget first loads, it’s padValue is initialized to 0. Clicking the button toggle the padValue to 100.0 and triggers a re-build through the setState() call. The AnimatedPadding then effectively increments the actual padding value from 0.0 to 100.0 over the 2 second duration, redrawing the actual screen with every tick of the clock to create the animation.

Important note: All Animated widgets must appear in the build method of some sort of Stateful widget. In our example:

  1. The Stateful widget has the value to animate as a state variable, e.g., padValue in our example.
  2. The state variable to animate, padValue, is used in the AnimatedPadding widget’s padding property. The AnimatedPadding widget is inside the build() method.
  3. setState() is called to change the state variable padValue when the user presses the button.

So we have the three ingredients for a stateful widget: a state variable, a build() method that uses that state variable, and a trigger calling setState().

The AnimatedAlign, AnimatedOpacity, AnimatedPositioned, AnimatedPhysicalModel, and AnimatedSize are all very similar to AnimatedPadding.

AnimatedContainer

We introduced the versatile Container class in a previous lab. The AnimatedContainer allows you to animate changes to multiple Container properties over the same period of time.

Add the following custom stateful Widget to main.dart:

// This code is adapted from the official Flutter example.
class AnimatedContainerExample extends StatefulWidget {
  const AnimatedContainerExample({super.key});

  @override
  State<AnimatedContainerExample> createState() =>
      _AnimatedContainerExampleState();
}

class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
  bool selected = false;

  @override
  Widget build(BuildContext context) {
    // This GestureDetector toggles our state variable, isSelected,
    // and invokes setState() to trigger a redraw of the widget.
    // The code below is the canonical way for doing setState
    return GestureDetector(
      onTap: () {
        // The code below is the canonical way for doing setState
        // You could put the following lines for the same effect.
        // selected = !selected;
        // setState(() {});
        setState(() {
          selected = !selected;
        });
      },
      child: Center(
        child: AnimatedContainer(
          // Here is the third ingredient to a stateful widget: the widget's
          // properties are based on the state variable using a ternary op.
          // 
          // AnimatedContainer see that the property changes and animates 
          // from old to new.
          width: selected ? 200.0 : 100.0,
          height: selected ? 100.0 : 200.0,
          color: selected ? Colors.red : Colors.blue,
          alignment:
              selected ? Alignment.center : AlignmentDirectional.topCenter,
          duration: const Duration(seconds: 2),
          // We can specify a curve to make the animation more exciting.
          curve:
              Curves.fastOutSlowIn, // Curves.linear is used if not specified.
          child: const FlutterLogo(size: 75),
        ),
      ),
    );
  }
}

Change the body of your HomeScreen’s Scaffold to point to the AnimatedContainerExample widget.

As with the AnimatedPadding, the AnimatedContainer animates its property changes over a specified duration, moving from the old values to the new values. In this case, multiple properties are animated: width, height, color, and alignment. As with any StatefulWidget, the flow of events is:

  1. The Widget’s properties are based on the value of a state variable, in this case, the boolean isSelected.
  2. Something triggers a state change. In this case, the GestureDetector toggles isSelected.
  3. setState() is called to trigger a rebuild of the widget.

AnimatedContainer detects the new property values and animates them over its duration.

Animation Curves

We introduce another variable called curve. All AnimatedX widgets have a curve property. This curve controls how the property value (e.g., height, color) changes during the animation. The start and end values will are always what you specify in code, but the intermediate values during animation can follow a curve for interesting effects.

By default, animations use a linear curve (a line) that evenly animates from start to end. In this example, we use Curves.fastOutSlowIn, which animates the changes quickly at teh start and then slows the changes near the stop. Look closely and you can see the difference in your emulator. The actual curves are illustrated below:

Here is a list of the common animation curves at your disposal.

AnimatedDefaultTextStyle

The AnimatedDefaultTextStyle is useful for, you guessed it, animating changes to text style, so properties like font size, font weight, foreground and background colors – anything that can appear in a TextStyle widget. The word Default appears because this widget uses the app’s theme for text styles by default – we have not yet covered app themes, but we will in the future.

If you recall, the general recipe for styling a Text widget looks like this (don’t copy this code):

// Don't copy this code
const Text(
  "I love animation!",
  style: TextStyle(fontSize: 24, color: Colors.blue),
),

To animate text, you make AnimatedDefaultTextStyle the parent, specify the style attribute based on widget state, and specify a child to be the Text that the animating style will be applied to. Copy the following code to main.dart:

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

  @override
  State<AnimatedTextStyleWidget> createState() =>
      _AnimatedTextStyleWidgetState();
}

class _AnimatedTextStyleWidgetState extends State<AnimatedTextStyleWidget> {
  // My "style" state variables that the style uses when building.
  // The initial values are the default vales for these properties.
  Color fgColor = Colors.blueAccent;
  double size = 14.0;
  FontWeight weight = FontWeight.normal;
  double spacing = 1.0;

  // This is also a state variable. I use it to set the values of the variables above.
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          // Change the state style variables based on the number of taps.
          count = (count + 1) % 3;
          switch (count) {
            case 0:
              fgColor = Colors.blueAccent;
              size = 14.0;
              weight = FontWeight.normal;
              spacing = 1.0;
              break;
            case 1:
              fgColor = Colors.deepPurple;
              size = 24.0;
              weight = FontWeight.w500;
              spacing = 6.0;
              break;
            default:
              fgColor = Colors.deepOrange;
              size = 48.0;
              weight = FontWeight.bold;
              spacing = 12.0;
          }
        });
      },
      child: Center(
        child: Column(
          children: [
            const Text(
              "I love animation!",
              style: TextStyle(fontSize: 18, color: Colors.blueAccent),
            ),
            AnimatedDefaultTextStyle(
              duration: const Duration(seconds: 1, milliseconds: 500),
              // Draw the style based on the style state variables.
              style: TextStyle(
                color: fgColor,
                fontSize: size,
                fontWeight: weight,
                wordSpacing: spacing,
              ),
              curve: Curves.easeInOutBack,
              child: const Text("I love it more!"),
            ),
            Text("Count: $count")
          ],
        ),
      ),
    );
  }
}

Change the body of your HomeScreen’s Scaffold to point to the AnimatedTextStyleWidget widget.

The animation process is the same as before, but this time I am toggling between three “states” of the text instead of just two. A boolean isSelected isn’t sufficient to account for three states, so I use a count variable that takes on values 0-2 to indicate which state I am in.

Here is the process as before:

  1. fgColor, size, weight, and spacing are state variable that the TextStyle will use. count is also a state variable that specifies what the other variable values should be.
  2. in the build() method, the AnimatedDefaultTextStyle uses the style state variables to specify the look of the Text.
  3. a GestureDetector registers user taps and changes the count. Based on the count, we change our state style variables to different values. setState() is called onTap to trigger a redraw and animation.

As with the AnimatedContainer example, I specify both a duration (required) and a curve for the AnimatedDefaultTextStyle. When a re-build is triggered by setState(), the AnimatedDefaultTextStyle sees that the new style state variables are different and animates the changes of its properties.

Implicit Animations vs. Explicit Animations

Implicit animations are by far the easiest way to incorporate animations into your Flutter project. However, you may want a different type of animation, for example:

If you said “yes” to any of these, then you need an explicit animation. Explicit animations require a *controller (see the PageView lab), and you must manage the animation lifecycle. There are several built-in explicit animation widgets that focus on how you want a widget to change.

We will demonstrate explicit animations in the next lab.

Exercise

For fun(?), have a look at the AnimatedSwitcher class and use it to simplify and animate the Image Swapper screen from Assignment 3.

Other resources

Definitions

Animation
Redrawing a widget over a period of time to reflect changes in the properties of that widget
Implicit animation
A set of built-in widgets (e.g., AnimatedContainer, AnimatedPadding) that animate a widget property from a start value to and end value. Implicit animations trade control for convenience—they manage animation effects so that you don’t have to.(reference)
Explicit animation
Explicit animations are a set of controls for telling Flutter how to rapidly rebuild the widget tree while changing widget properties to create animation effects. This approach enables you to create effects that you can’t achieve using implicit animations. (reference)