Animation Introduction and Implicit Animations
- Objectives
- Boilerplate code
- Animations Introduction
- Implicit Animations
- AnimatedPadding - a first example
- AnimatedContainer
- AnimatedDefaultTextStyle
- Implicit Animations vs. Explicit Animations
- Exercise
- Other resources
- Definitions
Objectives
- To define animations in the Flutter context
- To use a variety of built-in Implicit animation widgets
- To distinguish between Implicit and Explicit Animations
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:
- You type an email and press the “Send” icon. The Icon flies off the screen to indicate the message was sent successfully.
- The user successfully selects a Photo to use as their profile image. The placeholder “?” in their profile slowly blends into the photo they selected.
- The user taps a picture of Play-Doh to add it to their shopping cart. The app navigates from the SelectScreen to the CartScreen, and the Play-Doh image “flies” from the SelectScreen to the CartScreen to show where the item is in their shopping cart.
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:
- AnimatedAlign - change the alignment of a widget within a container
- AnimatedContainer - change multiple Container properties over time, including size, color, or border.
- AnimatedDefaultTextStyle - change the font style of text over time, e.g., font size, color, or plain to boldfaced.
- AnimatedOpacity - opacity/transparency is animated.
- AnimatedPadding - padding is animated.
- AnimatedPhysicalModel - borderRadius and elevation are animated.
- AnimatedPositioned - Change the position of the child within the Stack. Similar to AnimatedAlign but it only works for children of a Stack.
- AnimatedSize - change a widgets size over time.
- AnimatedSwitcher - replace one widget with another and animate the transition.
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:
- The Stateful widget has the value to animate as a state variable, e.g.,
padValue
in our example. - The state variable to animate,
padValue
, is used in theAnimatedPadding
widget’spadding
property. The AnimatedPadding widget is inside thebuild()
method. setState()
is called to change the state variablepadValue
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:
- The Widget’s properties are based on the value of a state variable, in this case, the boolean
isSelected
. - Something triggers a state change. In this case, the GestureDetector toggles
isSelected
. 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:
fgColor
,size
,weight
, andspacing
are state variable that the TextStyle will use.count
is also a state variable that specifies what the other variable values should be.- in the
build()
method, theAnimatedDefaultTextStyle
uses the style state variables to specify the look of the Text. - a GestureDetector registers user taps and changes the
count
. Based on thecount
, 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:
- Do you want your animation to repeat forever or while a condition is true?
- Do you want your animation to pause on demand?
- Do you want to animate multiple widgets together?
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)