- Install
- Set up an editor
- Test drive
- Write your first Flutter app, part 1
- Learn more
- Flutter for Android developers
- Flutter for iOS developers
- Flutter for React Native developers
- Flutter for web developers
- Flutter for Xamarin.Forms developers
- Introduction to declarative UI
- Cookbook
- Codelabs
- Tutorials
- User interface
- Introduction to widgets
- Layouts in Flutter
- Layout tutorial
- Dealing with box constraints
- Adding interactivity to your Flutter app
- Adding assets and images
- Navigation & routing
- Introduction to animations
- Animations overview
- Animations tutorial
- Hero Animations
- Staggered Animations
- Advanced UI
- Slivers
- Taps, drags, and other gestures
- Widget catalog
- Data & backend
- State management
- State management
- Start thinking declaratively
- Differentiate between ephemeral state and app state
- Simple app state management
- List of state management approaches
- JSON and serialization
- Firebase
- Accessibility & internationalization
- Accessibility
- Internationalizing Flutter apps
- Platform integration
- Writing custom platform-specific code
- Packages & plugins
- Using packages
- Developing packages & plugins
- Background processes
- Tools & techniques
- Android Studio / IntelliJ
- Visual Studio Code
- Upgrading Flutter
- Hot reload
- Code formatting
- Debugging Flutter apps
- Using OEM debuggers
- Flutter's build modes
- Testing Flutter apps
- Performance best practices
- Flutter performance profiling
- Creating flavors for Flutter
- Preparing an Android App for Release
- Preparing an iOS App for Release
- Continuous Delivery using fastlane with Flutter
- Bootstrap into Dart
- Inside Flutter
- Platform specific behaviors and adaptations
- Technical Overview
- Technical videos
- FAQ
- Flutter widget index
- Install
- Windows install
- MacOS install
- Linux install
- Set up an editor
- Write your first Flutter app, part 1
- Learn more
- Cupertino (iOS-style) widgets
- Layout widgets
- Animation and motion widgets
- Retrieve the value of a text field
- Basic widgets
- Material Components widgets
- Animate the properties of a Container
- Fade a Widget in and out
- Add a Drawer to a screen
- Displaying SnackBars
- Exporting fonts from a package
- Updating the UI based on orientation
- Using Themes to share colors and font styles
- Using custom fonts
- Working with Tabs
- Building a form with validation
- Create and style a text field
- Focus on a Text Field
- Handling changes to a text field
- Retrieve the value of a text field
- Adding Material Touch Ripples
- Handling Taps
- Implement Swipe to Dismiss
- Display images from the internet
- Fade in images with a placeholder
- Working with cached images
- Basic List
- Create a horizontal list
- Creating a Grid List
- Creating lists with different types of items
- Place a floating app bar above a list
- Working with long lists
- Report errors to a service
- Animating a Widget across screens
- Navigate to a new screen and back
- Navigate with named routes
- Pass arguments to a named route
- Return data from a screen
- Send data to a new screen
- Fetch data from the internet
- Making authenticated requests
- Parsing JSON in the background
- Working with WebSockets
- Persist data with SQLite
- Reading and Writing Files
- Storing key-value data on disk
- Play and pause a video
- Take a picture using the Camera
- An introduction to integration testing
- Performance profiling
- Scrolling
- An introduction to unit testing
- Mock dependencies using Mockito
- An introduction to widget testing
- Finding widgets
- Tapping, dragging and entering text
- Development
- Introduction to widgets
- Layout tutorial
- Dealing with box constraints
- Adding interactivity to your Flutter app
- Adding assets and images
- Navigation & routing
- Navigate to a new screen and back
- Send data to a new screen
- Return data from a screen
- Navigate with named routes
- Animating a Widget across screens
- AnimatedList
- Sample App Catalog
- Animations overview
- Animations tutorial
- Staggered Animations
- Slivers
- Taps, drags, and other gestures
- Accessibility widgets
- Assets, images, and icon widgets
- Async widgets
- Input widgets
- Interaction model widgets
- Painting and effect widgets
- Scrolling widgets
- Styling widgets
- Text widgets
- State management
- Start thinking declaratively
- Differentiate between ephemeral state and app state
- Simple app state management
- List of state management approaches
- JSON and serialization
- Accessibility
- Internationalizing Flutter apps
- Writing custom platform-specific code
- Using packages
- Fetch data from the internet
- Developing packages & plugins
- Background processes
- Android Studio / IntelliJ
- Set up an editor
- Flutter inspector
- Creating Useful Bug Reports
- Visual Studio Code
- Set up an editor
- Upgrading Flutter
- Hot reload
- Code formatting
Animations tutorial
This tutorial shows you how to build explicit animations in Flutter. After introducing some of the essential concepts, classes, and methods in the animation library, it walks you through 5 animation examples. The examples build on each other, introducing you to different aspects of the animation library.
The Flutter SDK also provides transition animations, such as FadeTransition, SizeTransition, and SlideTransition. These simple animations are triggered by setting a beginning and ending point. They are simpler to implement than explicit animations, which are described here.
Essential animation concepts and classes
The animation system in Flutter is based on typed Animation objects. Widgets can either incorporate these animations in their build functions directly by reading their current value and listening to their state changes or they can use the animations as the basis of more elaborate animations that they pass along to other widgets.
Animation<double>
In Flutter, an Animation object knows nothing about what is onscreen. An Animation
is an abstract class that understands its current value and its state (completed or dismissed). One of the more commonly used animation types is Animation<double>
.
An Animation
object sequentially generates interpolated numbers between two values over a certain duration. The output of an Animation
object may be linear, a curve, a step function, or any other mapping you can devise. Depending on how the Animation
object is controlled, it could run in reverse, or even switch directions in the middle.
Animations can also interpolate types other than double, such as Animation<Color>
or Animation<Size>
.
An Animation
object has state. Its current value is always available in the .value
member.
An Animation
object knows nothing about rendering or build()
functions.
CurvedAnimation
A CurvedAnimation defines the animation’s progress as a non-linear curve.
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation
and AnimationController
(described in the next section) are both of type Animation<double>
, so you can pass them interchangeably. The CurvedAnimation wraps the object it’s modifying—you don’t subclass AnimationController
to implement a curve.
AnimationController
AnimationController is a special Animation
object that generates a new value whenever the hardware is ready for a new frame. By default, an AnimationController
linearly produces the numbers from 0.0 to 1.0 during a given duration. For example, this code creates an Animation object, but does not start it running:
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
AnimationController
derives from Animation<double>
, so it can be used wherever an Animation
object is needed. However, the AnimationController
has additional methods to control the animation. For example, you start an animation with the .forward()
method. The generation of numbers is tied to the screen refresh, so typically 60 numbers are generated per second. After each number is generated, each Animation object calls the attached Listener objects. To create a custom display list for each child, see RepaintBoundary.
When creating an AnimationController
, you pass it a vsync
argument. The presence of vsync
prevents offscreen animations from consuming unnecessary resources. You can use your stateful object as the vsync by adding SingleTickerProviderStateMixin
to the class definition. You can see an example of this in animate1 on GitHub.
Tween
By default, the AnimationController
object ranges from 0.0 to 1.0. If you need a different range or a different data type, you can use a Tween to configure an animation to interpolate to a different range or data type. For example, the following Tween
goes from -200.0 to 0.0:
tween = Tween<double>(begin: -200, end: 0);
A Tween
is a stateless object that takes only begin
and end
. The sole job of a Tween
is to define a mapping from an input range to an output range. The input range is commonly 0.0 to 1.0, but that’s not a requirement.
A Tween
inherits from Animatable<T>
, not from Animation<T>
. An Animatable, like Animation, doesn’t have to output double. For example, ColorTween
specifies a progression between two colors.
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
A Tween
object does not store any state. Instead, it provides the evaluate(Animation<double> animation)
method that applies the mapping function to the current value of the animation. The current value of the Animation
object can be found in the .value
method. The evaluate function also performs some housekeeping, such as ensuring that begin and end are returned when the animation values are 0.0 and 1.0, respectively.
Tween.animate
To use a Tween
object, call animate()
on the Tween
, passing in the controller object. For example, the following code generates the integer values from 0 to 255 over the course of 500 ms.
AnimationController controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this); Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
The following example shows a controller, a curve, and a Tween
:
AnimationController controller = AnimationController( duration: const Duration(milliseconds: 500), vsync: this); final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut); Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
Animation notifications
An Animation object can have Listener
s and StatusListener
s, defined with addListener()
and addStatusListener()
. A Listener
is called whenever the value of the animation changes. The most common behavior of a Listener
is to call setState()
to cause a rebuild. A StatusListener
is called when an animation begins, ends, moves forward, or moves reverse, as defined by AnimationStatus
. The next section has an example of the addListener()
method, and Monitoring the progress of the animation shows an example of addStatusListener()
.
Animation examples
This section walks you through 5 animation examples. Each section provides a link to the source code for that example.
Rendering animations
So far you’ve learned how to generate a sequence of numbers over time. Nothing has been rendered to the screen. To render with an Animation
object, store the Animation
object as a member of your widget, then use its value to decide how to draw.
Consider the following app that draws the Flutter logo without animation:
import 'package:flutter/material.dart'; void main() => runApp(LogoApp()); class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } class _LogoAppState extends State<LogoApp> { @override Widget build(BuildContext context) { return Center( child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: 300, width: 300, child: FlutterLogo(), ), ); } }
App source: animate0
The following shows the same code modified to animate the logo to grow from nothing to full size. When defining an AnimationController
, you must pass in a vsync
object. The vsync
parameter is described in the AnimationController section.
The changes from the non-animated example are highlighted:
{animate0 → animate1}/lib/main.dart@@ -1,3 +1,4 @@ | |
1 | + import 'package:flutter/animation.dart'; |
1 2 | import 'package:flutter/material.dart'; |
2 3 | void main() => runApp(LogoApp()); |
@@ -6,16 +7,39 @@ | |
6 7 | _LogoAppState createState() => _LogoAppState(); |
7 8 | } |
8 | - class _LogoAppState extends State<LogoApp> { |
9 | + class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { |
10 | + Animation<double> animation; |
11 | + AnimationController controller; |
12 | + |
13 | + @override |
14 | + void initState() { |
15 | + super.initState(); |
16 | + controller = |
17 | + AnimationController(duration: const Duration(seconds: 2), vsync: this); |
18 | + animation = Tween<double>(begin: 0, end: 300).animate(controller) |
19 | + ..addListener(() { |
20 | + setState(() { |
21 | + // The state that has changed here is the animation object’s value. |
22 | + }); |
23 | + }); |
24 | + controller.forward(); |
25 | + } |
26 | + |
9 27 | @override |
10 28 | Widget build(BuildContext context) { |
11 29 | return Center( |
12 30 | child: Container( |
13 31 | margin: EdgeInsets.symmetric(vertical: 10), |
14 | - height: |
15 | - width: |
32 | + height: animation.value, |
33 | + width: animation.value, |
16 34 | child: FlutterLogo(), |
17 35 | ), |
18 36 | ); |
19 37 | } |
38 | + |
39 | + @override |
40 | + void dispose() { |
41 | + controller.dispose(); |
42 | + super.dispose(); |
43 | + } |
20 44 | } |
App source: animate1
The addListener()
function calls setState()
, so every time the Animation
generates a new number, the current frame is marked dirty, which forces build()
to be called again. In build()
, the container changes size because its height and width now use animation.value
instead of a hardcoded value. Dispose of the controller when the animation is finished to prevent memory leaks.
With these few changes, you’ve created your first animation in Flutter!
Simplifying with AnimatedWidget
The AnimatedWidget
base class allows you to separate out the core widget code from the animation code. AnimatedWidget
doesn’t need to maintain a State
object to hold the animation. Add the following AnimatedLogo
class:
class AnimatedLogo extends AnimatedWidget { AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { final Animation<double> animation = listenable; return Center( child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: animation.value, width: animation.value, child: FlutterLogo(), ), ); } }
AnimatedLogo
uses the current value of the animation
when drawing itself.
The LogoApp
still manages the AnimationController
and the Tween
, and it passes the Animation
object to AnimatedLogo
:
@@ -10,2 +27,2 @@ | |
10 27 | class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { |
11 28 | Animation<double> animation; |
@@ -13,32 +30,18 @@ | |
13 30 | @override |
14 31 | void initState() { |
15 32 | super.initState(); |
16 33 | controller = |
17 34 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
18 | - animation = Tween<double>(begin: 0, end: 300).animate(controller |
35 | + animation = Tween<double>(begin: 0, end: 300).animate(controller); |
19 | - ..addListener(() { |
20 | - setState(() { |
21 | - // The state that has changed here is the animation object’s value. |
22 | - }); |
23 | - }); |
24 36 | controller.forward(); |
25 37 | } |
26 38 | @override |
27 | - Widget build(BuildContext context) |
39 | + Widget build(BuildContext context) => AnimatedLogo(animation: animation); |
28 | - return Center( |
29 | - child: Container( |
30 | - margin: EdgeInsets.symmetric(vertical: 10), |
31 | - height: animation.value, |
32 | - width: animation.value, |
33 | - child: FlutterLogo(), |
34 | - ), |
35 | - ); |
36 | - } |
37 40 | @override |
38 41 | void dispose() { |
39 42 | controller.dispose(); |
40 43 | super.dispose(); |
41 44 | } |
App source: animate2
Monitoring the progress of the animation
It’s often helpful to know when an animation changes state, such as finishing, moving forward, or reversing. You can get notifications for this with addStatusListener()
. The following code modifies the previous example so that it listens for a state change and prints an update. The highlighted line shows the change:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = Tween<double>(begin: 0, end: 300).animate(controller) ..addStatusListener((state) => print('$state')); controller.forward(); } // ... }
Running this code produces this output:
AnimationStatus.forward
AnimationStatus.completed
Next, use addStatusListener()
to reverse the animation at the beginning or the end. This creates a “breathing” effect:
@@ -32,7 +32,15 @@ | |
32 32 | void initState() { |
33 33 | super.initState(); |
34 34 | controller = |
35 35 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
36 | - animation = Tween<double>(begin: 0, end: 300).animate(controller |
36 | + animation = Tween<double>(begin: 0, end: 300).animate(controller) |
37 | + ..addStatusListener((status) { |
38 | + if (status == AnimationStatus.completed) { |
39 | + controller.reverse(); |
40 | + } else if (status == AnimationStatus.dismissed) { |
41 | + controller.forward(); |
42 | + } |
43 | + }) |
44 | + ..addStatusListener((state) => print('$state')); |
37 45 | controller.forward(); |
38 46 | } |
App source: animate3
Refactoring with AnimatedBuilder
One problem with the code in the animate3 example, is that changing the animation required changing the widget that renders the logo. A better solution is to separate responsibilities into different classes:
- Render the logo
- Define the Animation object
- Render the transition
You can accomplish this separation with the help of the AnimatedBuilder
class. An AnimatedBuilder
is a separate class in the render tree. Like AnimatedWidget
, AnimatedBuilder
automatically listens to notifications from the Animation object, and marks the widget tree dirty as necessary, so you don’t need to call addListener()
.
The widget tree for the animate4 example looks like this:
Starting from the bottom of the widget tree, the code for rendering the logo is straightforward:
class LogoWidget extends StatelessWidget { // Leave out the height and width so it fills the animating parent Widget build(BuildContext context) => Container( margin: EdgeInsets.symmetric(vertical: 10), child: FlutterLogo(), ); }
The middle three blocks in the diagram are all created in the build()
method in GrowTransition
, shown below. The GrowTransition
widget itself is stateless and holds the set of final variables necessary to define the transition animation. The build() function creates and returns the AnimatedBuilder
, which takes the (Anonymous
builder) method and the LogoWidget object as parameters. The work of rendering the transition actually happens in the (Anonymous
builder) method, which creates a Container
of the appropriate size to force the LogoWidget
to shrink to fit.
One tricky point in the code below is that the child looks like it’s specified twice. What’s happening is that the outer reference of child is passed to AnimatedBuilder
, which passes it to the anonymous closure, which then uses that object as its child. The net result is that the AnimatedBuilder
is inserted in between the two widgets in the render tree.
class GrowTransition extends StatelessWidget { GrowTransition({this.child, this.animation}); final Widget child; final Animation<double> animation; Widget build(BuildContext context) => Center( child: AnimatedBuilder( animation: animation, builder: (context, child) => Container( height: animation.value, width: animation.value, child: child, ), child: child), ); }
Finally, the code to initialize the animation looks very similar to the animate2 example. The initState()
method creates an AnimationController
and a Tween
, then binds them with animate()
. The magic happens in the build()
method, which returns a GrowTransition
object with a LogoWidget
as a child, and an animation object to drive the transition. These are the three elements listed in the bullet points above.
@@ -27,22 +36,25 @@ | |
27 36 | class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { |
28 37 | Animation<double> animation; |
29 38 | AnimationController controller; |
30 39 | @override |
31 40 | void initState() { |
32 41 | super.initState(); |
33 42 | controller = |
34 43 | AnimationController(duration: const Duration(seconds: 2), vsync: this); |
35 44 | animation = Tween<double>(begin: 0, end: 300).animate(controller); |
36 45 | controller.forward(); |
37 46 | } |
38 47 | @override |
39 | - Widget build(BuildContext context) => |
48 | + Widget build(BuildContext context) => GrowTransition( |
49 | + child: LogoWidget(), |
50 | + animation: animation, |
51 | + ); |
40 52 | @override |
41 53 | void dispose() { |
42 54 | controller.dispose(); |
43 55 | super.dispose(); |
44 56 | } |
45 57 | } |
App source: animate4
Simultaneous animations
In this section, you’ll build on the example from monitoring the progress of the animation (animate3), which used AnimatedWidget
to animate in and out continuously. Consider the case where you want to animate in and out while the opacity animates from transparent to opaque.
Each tween manages an aspect of the animation. For example:
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); sizeAnimation = Tween<double>(begin: 0, end: 300).animate(controller); opacityAnimation = Tween<double>(begin: 0.1, end: 1).animate(controller);
You can get the size with sizeAnimation.value
and the opacity with opacityAnimation.value
, but the constructor for AnimatedWidget
only takes a single Animation
object. To solve this problem, the example creates its own Tween
objects and explicitly calculates the values.
Change AnimatedLogo
to encapsulate its own Tween
objects, and its build()
method calls Tween.evaluate()
on the parent’s animation object to calculate the required size and opacity values. The following code shows the changes with highlights:
class AnimatedLogo extends AnimatedWidget { // Make the Tweens static because they don't change. static final _opacityTween = Tween<double>(begin: 0.1, end: 1); static final _sizeTween = Tween<double>(begin: 0, end: 300); AnimatedLogo({Key key, Animation<double> animation}) : super(key: key, listenable: animation); Widget build(BuildContext context) { final Animation<double> animation = listenable; return Center( child: Opacity( opacity: _opacityTween.evaluate(animation), child: Container( margin: EdgeInsets.symmetric(vertical: 10), height: _sizeTween.evaluate(animation), width: _sizeTween.evaluate(animation), child: FlutterLogo(), ), ), ); } } class LogoApp extends StatefulWidget { _LogoAppState createState() => _LogoAppState(); } class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController(duration: const Duration(seconds: 2), vsync: this); animation = CurvedAnimation(parent: controller, curve: Curves.easeIn) ..addStatusListener((status) { if (status == AnimationStatus.completed) { controller.reverse(); } else if (status == AnimationStatus.dismissed) { controller.forward(); } }); controller.forward(); } @override Widget build(BuildContext context) => AnimatedLogo(animation: animation); @override void dispose() { controller.dispose(); super.dispose(); } }
App source: animate5
Next steps
This tutorial gives you a foundation for creating animations in Flutter using Tweens
, but there are many other classes to explore. You might investigate the specialized Tween
classes, animations specific to Material Design, ReverseAnimation
, shared element transitions (also known as Hero animations), physics simulations and fling()
methods. See the animations landing page for the latest available documents and examples.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论