1. Introduction
Animations are a great way to improve the user experience of your app, communicate important information to the user, and make your app more polished and enjoyable to use.
Overview of Flutter's animation framework
Flutter displays animation effects by re-building a portion of the widget tree on each frame. It provides pre-built animation effects and other APIs to make creating and composing animations easier.
- Implicit animations are pre-built animation effects that run the entire animation automatically. When the target value of the animation changes, it runs the animation from the current value to the target value, and displays each value in between so that the widget animates smoothly. Examples of implicit animations include
AnimatedSize
,AnimatedScale
, andAnimatedPositioned
. - Explicit animations are also pre-built animation effects, but require an
Animation
object in order to work. Examples includeSizeTransition
,ScaleTransition
orPositionedTransition
. - Animation is a class that represents a running or stopped animation, and is composed of a value representing the target value the animation is running to, and the status, which represents the current value the animation is displaying on screen at any given time. It is a subclass of
Listenable
, and notifies its listeners when the status changes while the animation is running. - AnimationController is a way to create an Animation and control its state. Its methods such as
forward()
,reset()
,stop()
, andrepeat()
can be used to control the animation without the need to define the animation effect that is being displayed, such as the scale, size, or position. - Tweens are used to interpolate values between a beginning and end value, and can represent any type, such as a double,
Offset
, orColor
. - Curves are used to adjust the rate of change of a parameter over time. When an animation runs, it's common to apply an easing curve to make the rate of change faster or slower at the beginning or end of the animation. Curves take an input value between 0.0 and 1.0 and return an output value between 0.0 and 1.0.
What you'll build
In this codelab, you're going to build a multiple-choice quiz game that features various animation effects and techniques.
You'll see how to...
- Build a widget that animates its size and color
- Build a 3D card flip effect
- Use fancy pre-built animation effects from the animations package
- Add predictive back gesture support available on the latest version of Android
What you'll learn
In this codelab you'll learn:
- How to use implicitly animated effects to achieve great looking animations without requiring a lot of code.
- How to use explicitly animated effects to configure your own effects using pre-built animated widgets such as
AnimatedSwitcher
or anAnimationController
. - How to use
AnimationController
to define your own widget that displays a 3D effect. - How to use the
animations
package to display fancy animation effects with minimal setup.
What you'll need
- The Flutter SDK
- An IDE, such as VSCode or Android Studio / IntelliJ
2. Set up your Flutter development environment
You need two pieces of software to complete this lab — the Flutter SDK and an editor.
You can run the codelab using any of these devices:
- A physical Android (recommended for implementing predictive back in step 7) or iOS device connected to your computer and set to Developer mode.
- The iOS simulator (requires installing Xcode tools).
- The Android Emulator (requires setup in Android Studio).
- A browser (Chrome is required for debugging).
- A Windows, Linux, or macOS desktop computer. You must develop on the platform where you plan to deploy. So, if you want to develop a Windows desktop app, you must develop on Windows to access the appropriate build chain. There are operating system-specific requirements that are covered in detail on docs.flutter.dev/desktop.
Verify your installation
To verify that your Flutter SDK is configured correctly, and you have at least one of the above target platforms installed, use the Flutter Doctor tool:
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.2, on macOS 14.6.1 23G93 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Android Studio [✓] IntelliJ IDEA Ultimate Edition [✓] VS Code [✓] Connected device (4 available) [✓] Network resources • No issues found!
3. Run the starter app
Download the starter app
Use git
to clone the start app from the flutter/samples repository on GitHub.
$ git clone https://github.com/flutter/codelabs.git $ cd codelabs/animations/step_01/
Alternatively, you can download the source code as a .zip file.
Run the app
To run the app, use the flutter run
command and specify a target device, such as android
, ios
, or chrome
. For the full list of supported platforms, see the Supported platforms page.
$ flutter run -d android
You can also run and debug the app using your IDE of choice. See the official Flutter documentation for more information.
Tour the code
The starter app is a multiple-choice quiz game that consists of two screens following the model-view-view-model, or MVVM design pattern. The QuestionScreen
(View) uses the QuizViewModel
(View-Model) class to ask the user multiple choice questions from the QuestionBank
(Model) class.
- home_screen.dart - Displays a screen with a New Game button
- main.dart - Configures the
MaterialApp
to use Material 3 and show the home screen - model.dart - Defines the core classes used throughout the app
- question_screen.dart - Displays the UI for the quiz game
- view_model.dart - Stores the state and logic for the quiz game, displayed by the
QuestionScreen
The app doesn't support any animated effects yet, except for the default view transition displayed by Flutter's Navigator
class when the user presses the New Game button.
4. Use implicit animation effects
Implicit animations are a great choice in many situations, since they don't require any special configuration. In this section, you will update the StatusBar
widget so that it displays an animated scoreboard. To find common implicit animation effects, browse the ImplicitlyAnimatedWidget API documentation.
Create the unanimated scoreboard widget
Create a new file, lib/scoreboard.dart
with the following code:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
Icon(
Icons.star,
size: 50,
color:
score < i + 1 ? Colors.grey.shade400 : Colors.yellow.shade700,
)
],
),
);
}
}
Then add the Scoreboard
widget in the StatusBar
widget's children, replacing the Text
widgets that previously showed the score and total question count. Your editor should automatically add the required import "scoreboard.dart"
at the top of the file.
lib/question_screen.dart
class StatusBar extends StatelessWidget {
final QuizViewModel viewModel;
const StatusBar({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Scoreboard( // NEW
score: viewModel.score, // NEW
totalQuestions: viewModel.totalQuestions, // NEW
),
],
),
),
);
}
}
This widget displays a star icon for each question. When a question is answered correctly, another star lights up instantly without any animation. In the following steps, you'll help inform the user that their score changed by animating its size and color.
Use an implicit animation effect
Create a new widget called AnimatedStar
that uses an AnimatedScale
widget to change the scale
amount from 0.5
to 1.0
when the star becomes active:
lib/scoreboard.dart
import 'package:flutter/material.dart';
class Scoreboard extends StatelessWidget {
final int score;
final int totalQuestions;
const Scoreboard({
super.key,
required this.score,
required this.totalQuestions,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (var i = 0; i < totalQuestions; i++)
AnimatedStar( // NEW
isActive: score > i, // NEW
) // NEW
],
),
);
}
}
class AnimatedStar extends StatelessWidget { // Add from here...
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: Icon(
Icons.star,
size: 50,
color: isActive ? _activatedColor : _deactivatedColor,
),
);
}
} // To here.
Now, when the user answers a question correctly, the AnimatedStar
widget updates its size using an implicit animation. The Icon
's color
is not animated here, only the scale
, which is done by the AnimatedScale
widget.
Use a Tween to interpolate between two values
Notice that the color of the AnimatedStar
widget changes immediately after the isActive
field changes to true.
To achieve an animated color effect, you might try to use an AnimatedContainer
widget (which is another subclass of ImplicitlyAnimatedWidget
), because it can automatically animate all of its attributes, including the color. Unfortunately, our widget needs to display an icon, not a container.
You might also try AnimatedIcon
, which implements transition effects between the shapes of the icons. But there isn't a default implementation of a star icon in the AnimatedIcons
class.
Instead, we'll use another subclass of ImplicitlyAnimatedWidget
called TweenAnimationBuilder
, which takes a Tween
as a parameter. A tween is a class that takes two values (begin
and end
) and calculates the in-between values, so that an animation can display them. In this example, we'll use a ColorTween
, which satisfies the Tween<Color>
interface required to build our animation effect.
Select the Icon
widget and use the "Wrap with Builder" quick action in your IDE, change the name to TweenAnimationBuilder
. Then provide the duration and ColorTween
.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
duration: _duration,
child: TweenAnimationBuilder( // Add from here...
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) { // To here.
return Icon(
Icons.star,
size: 50,
color: value, // Modify from here...
);
}, // To here.
),
);
}
}
Now, hot-reload the app to see the new animation.
Notice that the end
value of our ColorTween
changes based on the value of the isActive
parameter. This is because TweenAnimationBuilder
re-runs its animation whenever the Tween.end
value changes. When this happens, the new animation runs from the current animation value to the new end value, which allows you to change the color at any time (even while the animation is running) and display a smooth animation effect with the correct in-between values.
Apply a Curve
Both of these animation effects run at a constant rate, but animations are often more visually interesting and informative when they speed up or slow down.
A Curve
applies an easing function, which defines the rate of change of a parameter over time. Flutter ships with a collection of pre-built easing curves in the Curves
class, such as easeIn
or easeOut
.
These diagrams (available on the Curves
API documentation page) give a clue to how curves work. Curves convert an input value between 0.0 and 1.0 (displayed on the x axis) to an output value between 0.0 and 1.0 (displayed on the y axis). These diagrams also show a preview of what various animation effects look like when they use an easing curve.
Create a new field in AnimatedStar called _curve
and pass it as a parameter to the AnimatedScale
and TweenAnimationBuilder
widgets.
lib/scoreboard.dart
class AnimatedStar extends StatelessWidget {
final bool isActive;
final Duration _duration = const Duration(milliseconds: 1000);
final Color _deactivatedColor = Colors.grey.shade400;
final Color _activatedColor = Colors.yellow.shade700;
final Curve _curve = Curves.elasticOut; // NEW
AnimatedStar({super.key, required this.isActive});
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1.0 : 0.5,
curve: _curve, // NEW
duration: _duration,
child: TweenAnimationBuilder(
curve: _curve, // NEW
duration: _duration,
tween: ColorTween(
begin: _deactivatedColor,
end: isActive ? _activatedColor : _deactivatedColor,
),
builder: (context, value, child) {
return Icon(
Icons.star,
size: 50,
color: value,
);
},
),
);
}
}
In this example, the elasticOut
curve provides an exaggerated spring effect that starts with a spring motion and balances out towards the end.
Hot reload the app to see this curve applied to AnimatedSize
and TweenAnimationBuilder
.
Use DevTools to enable slow animations
To debug any animation effect, Flutter DevTools provides a way to slow down all the animations in your app, so you can see the animation more clearly.
To open DevTools, make sure the app is running in debug mode, and open the Widget Inspector by selecting it in the Debug toolbar in VSCode or by selecting the Open Flutter DevTools button in the Debug tool window in IntelliJ / Android Studio.
Once the widget inspector is open, click the Slow animations button in the toolbar.
5. Use explicit animation effects
Like implicit animations, explicit animations are pre-built animation effects, but instead of taking a target value, they take an Animation
object as a parameter. This makes them useful in situations where the animation is already defined by a navigation transition, AnimatedSwitcher
, or AnimationController
, for example.
Use an explicit animation effect
To get started with an explicit animation effect, wrap the Card
widget with an AnimatedSwitcher
.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
), // NEW
);
}
}
AnimatedSwitcher
uses a cross-fade effect by default, but you can override this using the transitionBuilder
parameter. The transition builder provides the child widget that was passed to the AnimatedSwitcher
, and an Animation
object. This is a great opportunity to use an explicit animation.
For this codelab, the first explicit animation we'll use is SlideTransition
, which takes an Animation<Offset>
that defines the start and end offset that the incoming and outgoing widgets will move between.
Tweens have a helper function, animate()
, that converts any Animation
into another Animation
with the tween applied. This means that a Tween<Offset>
can be used to convert the Animation<double>
provided by the AnimatedSwitcher
into an Animation<Offset>
, to be provided to the SlideTransition
widget.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
transitionBuilder: (child, animation) { // Add from here...
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
return SlideTransition(position: offsetAnimation, child: child);
}, // To here.
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Note that this uses Tween.animate
to apply a Curve
to the Animation
, and then to convert it from a Tween<double>
that ranges from 0.0 to 1.0, to a Tween<Offset>
that transitions from -0.1 to 0.0 on the x-axis.
Alternatively, the Animation class has a drive()
function that takes any Tween
(or Animatable
) and converts it into a new Animation
. This allows tweens to be "chained", making the resulting code more concise:
lib/question_screen.dart
transitionBuilder: (child, animation) {
var offsetAnimation = animation
.drive(CurveTween(curve: Curves.easeInCubic))
.drive(Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero));
return SlideTransition(position: offsetAnimation, child: child);
},
Another advantage to using explicit animations is that they can be easily composed together. Add another explicit animation, FadeTransition that uses the same curved animation by wrapping the SlideTransition widget.
lib/question_screen.dart
return AnimatedSwitcher(
transitionBuilder: (child, animation) {
final curveAnimation =
CurveTween(curve: Curves.easeInCubic).animate(animation);
final offsetAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset.zero)
.animate(curveAnimation);
final fadeInAnimation = curveAnimation; // NEW
return FadeTransition( // NEW
opacity: fadeInAnimation, // NEW
child: SlideTransition(position: offsetAnimation, child: child), // NEW
); // NEW
},
Customize the layoutBuilder
You might notice a small problem with the AnimationSwitcher. When a QuestionCard switches to a new question, it lays it out in the center of the available space while the animation is running, but when the animation is stopped, the widget snaps to the top of the screen. This causes a janky animation because the final position of the question card doesn't match the position while the animation is running.
To fix this, the AnimatedSwitcher also has a layoutBuilder parameter, which can be used to define the layout. Use this function to configure the layout builder to align the card to the top of the screen:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: <Widget>[
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
This code is a modified version of the defaultLayoutBuilder from the AnimatedSwitcher class, but uses Alignment.topCenter
instead of Alignment.center
.
Summary
- Explicit animations are animation effects that take an Animation object (in contrast to ImplicitlyAnimatedWidgets, which take a target value and duration)
- The Animation class represents a running animation, but does not define a specific effect.
- Use Tween().animate or Animation.drive() to apply Tweens and Curves (using CurveTween) to an animation.
- Use AnimatedSwitcher;s layoutBuilder parameter to adjust how it lays out its children.
6. Control the state of an animation
So far, every animation has been run automatically by the framework. Implicit animations run automatically, and explicit animation effects require an Animation to work correctly. In this section, you'll learn how to create your own Animation objects using an AnimationController, and use a TweenSequence to combine Tweens together.
Run an animation using an AnimationController
To create an animation using an AnimationController, you'll need to follow these steps:
- Create a StatefulWidget
- Use the SingleTickerProviderStateMixin mixin in your State class to provide a Ticker to your AnimationController
- Initialize the AnimationController in the initState lifecycle method, providing the current State object to the
vsync
(TickerProvider) parameter. - Make sure your widget re-builds whenever the AnimationController notifies its listeners, either by using AnimatedBuilder or by calling listen() and setState manually.
Create a new file, flip_effect.dart and copy-paste the following code:
lib/flip_effect.dart
import 'dart:math' as math;
import 'package:flutter/widgets.dart';
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: widget.duration);
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
}
@override
void didUpdateWidget(covariant CardFlipEffect oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.child.key != oldWidget.child.key) {
_handleChildChanged(widget.child, oldWidget.child);
}
}
void _handleChildChanged(Widget newChild, Widget previousChild) {
_previousChild = previousChild;
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationController.value * math.pi),
child: _animationController.isAnimating
? _animationController.value < 0.5
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
}
This class sets up an AnimationController and re-runs the animation whenever the framework calls didUpdateWidget to notify it that the widget configuration has changed, and there might be a new child widget.
The AnimatedBuilder ensures that the widget tree is re-built whenever the AnimationController notifies its listeners, and the Transform widget is used to apply a 3D rotation effect to simulate a card being flipped over.
To use this widget, wrap each answer card in with a CardFlipEffect widget. Make sure to provide a key
to the Card widget:
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect( // NEW
duration: const Duration(milliseconds: 300), // NEW
child: Card.filled( // NEW
key: ValueKey(answers[index]), // NEW
color: color,
elevation: 2,
margin: EdgeInsets.all(8),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTapped(index),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: Text(
answers.length > index ? answers[index] : '',
style: Theme.of(context).textTheme.titleMedium,
overflow: TextOverflow.clip,
),
),
),
),
), // NEW
);
}),
);
}
Now hot-reload the app to see the answer cards flip over using the CardFlipEffect widget.
You might notice that this class looks a lot like an explicit animation effect. In fact, it's often a good idea to extend the AnimatedWidget class directly to implement your own version. Unfortunately, since this class needs to store the previous widget in its State, it needs to use a StatefulWidget. To learn more about creating your own explicit animation effects, see the API documentation for AnimatedWidget.
Add a delay using TweenSequence
In this section, you'll add a delay to the CardFlipEffect widget so that each card flips over one at a time. To get started, add a new field called delayAmount
.
lib/flip_effect.dart
class CardFlipEffect extends StatefulWidget {
final Widget child;
final Duration duration;
final double delayAmount; // NEW
const CardFlipEffect({
super.key,
required this.child,
required this.duration,
required this.delayAmount, // NEW
});
@override
State<CardFlipEffect> createState() => _CardFlipEffectState();
}
Then add the delayAmount
to the AnswerCards
build method.
lib/question_screen.dart
@override
Widget build(BuildContext context) {
return GridView.count(
shrinkWrap: true,
crossAxisCount: 2,
childAspectRatio: 5 / 2,
children: List.generate(answers.length, (index) {
var color = Theme.of(context).colorScheme.primaryContainer;
if (correctAnswer == index) {
color = Theme.of(context).colorScheme.tertiaryContainer;
}
return CardFlipEffect(
delayAmount: index.toDouble() / 2, // NEW
duration: const Duration(milliseconds: 300),
child: Card.filled(
key: ValueKey(answers[index]),
Then in _CardFlipEffectState
, create a new Animation that applies the delay using a TweenSequence
. Notice that this does not use any utilities from the dart:async
library, like Future.delayed
. This is because the delay is part of the animation and not something the widget explicitly controls when it uses the AnimationController. This makes the animation effect easier to debug when enabling slow animations in DevTools, since it uses the same TickerProvider.
To use a TweenSequence
, create two TweenSequenceItem
s, one containing a ConstantTween
that keeps the animation at 0 for a relative duration and a regular Tween
that goes from 0.0
to 1.0
.
lib/flip_effect.dart
class _CardFlipEffectState extends State<CardFlipEffect>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
Widget? _previousChild;
late final Animation<double> _animationWithDelay; // NEW
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this, duration: widget.duration * (widget.delayAmount + 1));
_animationController.addListener(() {
if (_animationController.value == 1) {
_animationController.reset();
}
});
_animationWithDelay = TweenSequence<double>([ // NEW
if (widget.delayAmount > 0) // NEW
TweenSequenceItem( // NEW
tween: ConstantTween<double>(0.0), // NEW
weight: widget.delayAmount, // NEW
), // NEW
TweenSequenceItem( // NEW
tween: Tween(begin: 0.0, end: 1.0), // NEW
weight: 1.0, // NEW
), // NEW
]).animate(_animationController); // NEW
}
Finally, replace the AnimationController's animation with the new delayed animation in the build method.
lib/flip_effect.dart
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationWithDelay, // Modify this line
builder: (context, child) {
return Transform(
alignment: Alignment.center,
transform: Matrix4.identity()
..rotateX(_animationWithDelay.value * math.pi), // And this line
child: _animationController.isAnimating
? _animationWithDelay.value < 0.5 // And this one.
? _previousChild
: Transform.flip(flipY: true, child: child)
: child,
);
},
child: widget.child,
);
}
Now hot reload the app and watch the cards flip one by one. For a challenge try experimenting with changing the perspective of the 3D effect provided by the Transform
widget..
7. Use custom navigation transitions
So far, we've seen how to customize effects on a single screen, but another way to use animations is to use them to transition between screens. In this section, you'll learn how to apply animation effects to screen transitions using built-in animation effects and fancy pre-built animation effects provided by the official animations package on pub.dev
Animate a navigation transition
The PageRouteBuilder
class is a Route
that allows you to customize the transition animation. It allows you to override its transitionBuilder
callback, which provides two Animation objects, representing the incoming and outgoing animation that is run by the Navigator.
To customize the transition animation, replace the MaterialPageRoute
with a PageRouteBuilder
and to customize the transition animation when the user navigates from the HomeScreen
to the QuestionScreen
. Use a FadeTransition (an explicitly animated widget) to make the new screen fade in on top of the previous screen.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder( // NEW
pageBuilder: (context, animation, secondaryAnimation) { // NEW
return QuestionScreen(); // NEW
}, // NEW
transitionsBuilder: // NEW
(context, animation, secondaryAnimation, child) { // NEW
return FadeTransition( // NEW
opacity: animation, // NEW
child: child, // NEW
); // NEW
}, // NEW
), // NEW
);
},
child: Text('New Game'),
),
The animations package provides fancy pre-built animation effects, like FadeThroughTransition. Import the animations package and replace the FadeTransition with the FadeThroughTransition widget:
lib/home_screen.dart
import 'package;animations/animations.dart';
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return const QuestionScreen();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
},
),
);
},
child: Text('New Game'),
),
Customize the predictive back animation
Predictive back is a new Android feature that allows the user to peek behind the current route or app to see what's behind it before navigating. The peek animation is driven by the location of the user's finger as they drag back across the screen.
Flutter supports system predictive back by enabling the feature at the system level when Flutter has no routes to pop on its navigation stack, or in other words, when a back would exit the app. This animation is handled by the system and not by Flutter itself.
Flutter also supports predictive back when navigating between routes within a Flutter app. A special PageTransitionsBuilder called PredictiveBackPageTransitionsBuilder
listens for system predictive back gestures and drives its page transition with the gesture's progress.
Predictive back is only supported in Android U and above, but Flutter will gracefully fall back to the original back gesture behavior and ZoomPageTransitionBuilder. See our blog post for more, including a section about how to set it up in your own app.
In the ThemeData configuration for your app, configure the PageTransitionsTheme to use PredictiveBack on Android, and the fade-through transition effect from the animations package on other platforms:
lib/main.dart
import 'package:animations/animations.dart'; // NEW
import 'package:flutter/material.dart';
import 'home_screen.dart';
void main() {
runApp(MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
pageTransitionsTheme: PageTransitionsTheme(
builders: {
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(), // NEW
TargetPlatform.iOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.macOS: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.windows: FadeThroughPageTransitionsBuilder(), // NEW
TargetPlatform.linux: FadeThroughPageTransitionsBuilder(), // NEW
},
),
),
home: HomeScreen(),
);
}
}
Now you can change the Navigator.push() call back to a MaterialPageRoute.
lib/home_screen.dart
ElevatedButton(
onPressed: () {
// Show the question screen to start the game
Navigator.push(
context,
MaterialPageRoute(builder: (context) { // NEW
return const QuestionScreen(); // NEW
}), // NEW
);
},
child: Text('New Game'),
),
Use FadeThroughTransition to change the current question
The AnimatedSwitcher widget only provides one Animation in its builder callback. To address this, the animations
package provides a PageTransitionSwitcher.
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.question,
super.key,
});
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher( // NEW
layoutBuilder: (entries) { // NEW
return Stack( // NEW
alignment: Alignment.topCenter, // NEW
children: entries, // NEW
); // NEW
}, // NEW
transitionBuilder: (child, animation, secondaryAnimation) { // NEW
return FadeThroughTransition( // NEW
animation: animation, // NEW
secondaryAnimation: secondaryAnimation, // NEW
child: child, // NEW
); // NEW
}, // NEW
duration: const Duration(milliseconds: 300),
child: Card(
key: ValueKey(question),
elevation: 4,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
),
);
}
}
Use OpenContainer
The OpenContainer widget from the animations
package provides a container transform animation effect that expands to create a visual connection between two widgets.
The widget returned by closedBuilder
is displayed initially, and expands to the widget returned by openBuilder
when the container is tapped or when the openContainer
callback is called.
To connect the openContainer
callback to the view-model, add a new pass the viewModel into the QuestionCard widget and store a callback that will be used to show the "Game Over" screen:
lib/question_screen.dart
class QuestionScreen extends StatefulWidget {
const QuestionScreen({super.key});
@override
State<QuestionScreen> createState() => _QuestionScreenState();
}
class _QuestionScreenState extends State<QuestionScreen> {
late final QuizViewModel viewModel =
QuizViewModel(onGameOver: _handleGameOver);
VoidCallback? _showGameOverScreen; // NEW
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: viewModel,
builder: (context, child) {
return Scaffold(
appBar: AppBar(
actions: [
TextButton(
onPressed:
viewModel.hasNextQuestion && viewModel.didAnswerQuestion
? () {
viewModel.getNextQuestion();
}
: null,
child: const Text('Next'),
)
],
),
body: Center(
child: Column(
children: [
QuestionCard( // NEW
onChangeOpenContainer: _handleChangeOpenContainer, // NEW
question: viewModel.currentQuestion?.question, // NEW
viewModel: viewModel, // NEW
), // NEW
Spacer(),
AnswerCards(
onTapped: (index) {
viewModel.checkAnswer(index);
},
answers: viewModel.currentQuestion?.possibleAnswers ?? [],
correctAnswer: viewModel.didAnswerQuestion
? viewModel.currentQuestion?.correctAnswer
: null,
),
StatusBar(viewModel: viewModel),
],
),
),
);
},
);
}
void _handleChangeOpenContainer(VoidCallback openContainer) { // NEW
_showGameOverScreen = openContainer; // NEW
} // NEW
void _handleGameOver() { // NEW
if (_showGameOverScreen != null) { // NEW
_showGameOverScreen!(); // NEW
} // NEW
} // NEW
}
Add a new widget, GameOverScreen:
lib/question_screen.dart
class GameOverScreen extends StatelessWidget {
final QuizViewModel viewModel;
const GameOverScreen({required this.viewModel, super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Scoreboard(
score: viewModel.score,
totalQuestions: viewModel.totalQuestions,
),
Text(
'You Win!',
style: Theme.of(context).textTheme.displayLarge,
),
Text(
'Score: ${viewModel.score} / ${viewModel.totalQuestions}',
style: Theme.of(context).textTheme.displaySmall,
),
ElevatedButton(
child: Text('OK'),
onPressed: () {
Navigator.popUntil(context, (route) => route.isFirst);
},
),
],
),
),
);
}
}
In the QuestionCard widget, replace the Card with an OpenContainer widget from the animations package, adding two new fields for the viewModel and open container callback:
lib/question_screen.dart
class QuestionCard extends StatelessWidget {
final String? question;
const QuestionCard({
required this.onChangeOpenContainer,
required this.question,
required this.viewModel,
super.key,
});
final ValueChanged<VoidCallback> onChangeOpenContainer;
final QuizViewModel viewModel;
static const _backgroundColor = Color(0xfff2f3fa);
@override
Widget build(BuildContext context) {
return PageTransitionSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, animation, secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: OpenContainer( // NEW
key: ValueKey(question), // NEW
tappable: false, // NEW
closedColor: _backgroundColor, // NEW
closedShape: const RoundedRectangleBorder( // NEW
borderRadius: BorderRadius.all(Radius.circular(12.0)), // NEW
), // NEW
closedElevation: 4, // NEW
closedBuilder: (context, openContainer) { // NEW
onChangeOpenContainer(openContainer); // NEW
return ColoredBox( // NEW
color: _backgroundColor, // NEW
child: Padding( // NEW
padding: const EdgeInsets.all(16.0), // NEW
child: Text(
question ?? '',
style: Theme.of(context).textTheme.displaySmall,
),
),
);
},
openBuilder: (context, closeContainer) { // NEW
return GameOverScreen(viewModel: viewModel); // NEW
}, // NEW
),
);
}
}
8. Congratulations
Congratulations, you've successfully added animation effects to a Flutter app, and learned about the core components of Flutter's animation system. Specifically, you learned:
- How to use an ImplicitlyAnimatedWidget
- How to use an ExplicitlyAnimatedWidget
- How to apply Curves and Tweens to an animation
- How to use pre-built transition widgets such as AnimatedSwitcher or PageRouteBuilder
- How to use fancy pre-built animation effects from the
animations
package, such as FadeThroughTransition and OpenContainer - How to customize the default transition animation, including adding support for Predictive Back on Android.
What's next?
Check out some of these codelabs:
- Building an animated responsive app layout with Material 3
- Building Beautiful Transitions with Material Motion for Flutter
- Take your Flutter app from boring to beautiful
Or download the animations sample app, which showcases various animation techniques
Further reading
You can find more animations resources on flutter.dev:
- Introduction to animations
- Animations tutorial (tutorial)
- Implicit animations (tutorial)
- Animate the properties of a container (cookbook)
- Fade a widget in and out (cookbook)
- Hero animations
- Animate a page route transition (cookbook)
- Animate a widget using a physics simulation (cookbook)
- Staggered animations
- Animation and motion widgets (Widget catalog)
Or check out these articles on Medium:
- Animation deep dive
- Custom implicit animations in Flutter
- Animation management with Flutter and Flux / Redux
- How to Choose Which Flutter Animation Widget is Right for You?
- Directional animations with built-in explicit animations
- Flutter animation basics with implicit animations
- When should I use AnimatedBuilder or AnimatedWidget?