Animation abstraction in Flutter
In order to make it easier for developers to create animations, different UI systems abstract animations. Flutter also abstracts animations, mainly involving Animation、Curve、Controller、Tween
these four characters. They work together to complete a complete animation. Let’s introduce them one by one. they.
1. Animation
Animation
Is an abstract class, which has nothing to do with UI rendering itself, and its main function is to save the interpolation and state of animation; one of the more commonly used Animation
classes is Animation<double>
.
Animation
Tween
An object is a class that sequentially generates values between an interval ( ) over a period of time . Animation
The value output by the object during the entire animation execution can be linear, curved, a step function or any other curved function, etc., Curve
depending on decision.
Depending on Animation
how the object is controlled, the animation can run forward (starting at the start state and ending at the end state), or in reverse, or even switch directions in between.
Animation
It is also possible to generate double
other types of values besides, such as: Animation<Color>
or Animation<Size>
. In each frame of the animation, we can get the current state value of the animation through the properties Animation
of the object .value
animation notification
We can Animation
monitor each frame of the animation and the change of the execution state Animation
through the following two methods:
addListener()
; it can be used toAnimation
add a frame listener, which will be called every frame. The most common behavior in a frame listener is called after a state changesetState()
to trigger a UI rebuild.addStatusListener()
; it canAnimation
add an "animation state change" listener; theAnimationStatus
state change listener will be called when the animation starts, ends, forward or reverse (see definition).
Here you only need to know the difference between the frame monitor and the status monitor, and we will illustrate it with an example later.
2. Curve
The animation process can be at a constant speed, uniformly accelerated, or first accelerated and then decelerated. Flutter uses Curve
(curve) to describe the animation process. We call uniform animation linear ( Curves.linear
), and non-uniform animation non-linear.
We can CurvedAnimation
specify the curve of the animation by, such as:
final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
CurvedAnimation
and AnimationController
(described below) are Animation<double>
both types. CurvedAnimation
We can wrap AnimationController
and Curve
generate a new animation object, which is how we associate the animation with the curve that the animation executes. The curve we specify for the animation is Curves.easeIn
, which means that the animation starts slower and ends faster. Curves
The class is a preset enumeration class, which defines many commonly used curves. The following are some commonly used ones:
Curves | animation process |
---|---|
linear | Uniform |
decelerate | Uniform deceleration |
ease | Start to accelerate, then slow down |
easeIn | start slow, then fast |
easeOut | start fast, then slow |
easeInOut | Start slow, then speed up, then slow down again |
In addition to the above listed, Curves
there are many other curves defined in the class, you can check Curves
the class definition yourself.
Of course we can also create our own Curve
, for example we define a sine curve:
class ShakeCurve extends Curve {
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
3. AnimationController
AnimationController
It is used to control the animation, which includes methods for starting forward()
, stopping stop()
, and playing in reverse . Every frame of the animation, a new value is generated. By default, the numbers from to (the default interval) are generated linearly over the given time period .reverse()
AnimationController
AnimationController
0.0
1.0
For example, the following code creates an Animation
object (but does not start an animation):
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
Among them, duration
it represents the duration of the animation execution, through which we can control the speed of the animation.
AnimationController
The range of generated numbers can be specified by lowerBound
and upperBound
, such as:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
lowerBound: 10.0,
upperBound: 20.0,
vsync: this
);
AnimationController
Derived from Animation<double>
, so it can Animation
be used anywhere an object is expected.
What's more, AnimationController
there are other methods to control the animation, such as forward()
methods can start forward animation, and reverse()
can start reverse animation. After the animation starts to execute, the animation frame is generated. Every time the screen is refreshed, it is an animation frame. In each frame of the animation, the current animation value ( ) will be generated according to the animation curve, and then constructed according to the current animation value Animation.value
. UI, when all animation frames are triggered sequentially, the animation value will change sequentially, so the built UI will also change sequentially, so finally we can see a completed animation. In addition, at each frame of the animation, Animation
the object will call its frame listener, and when the animation state changes (such as the end of the animation), it will call the state change listener.
Note: In some cases, animation values may be out of
AnimationController
range[0.0,1.0]
, depending on the specific curve. For example,fling()
the function can simulate a finger throwing animation according to the speed (velocity
) and force (force
) of our finger sliding (throwing out), so its animation value can be[0.0,1.0]
out of range. That is, depending on the curve chosen,CurvedAnimation
the output of the can have a larger range than the input. For example,Curves.elasticIn
isoelastic curves generate values that are larger or smaller than the default range.
Ticker
When creating one AnimationController
, you need to pass a vsync
parameter, which receives a TickerProvider
type of object whose main responsibility is to create Ticker
, defined as follows:
abstract class TickerProvider {
Ticker createTicker(TickerCallback onTick); // 通过一个回调创建一个Ticker
}
The Flutter application will bind one when it is started SchedulerBinding
, through which you SchedulerBinding
can add a callback to each screen refresh, and add a screen refresh callback through , so that it will be called every time the screen refreshes .Ticker
SchedulerBinding
TickerCallback
Using Ticker
(instead of Timer
) to drive the animation will prevent off-screen animation (when the animated UI is not on the current screen, such as when the screen is locked) from consuming unnecessary resources, because when the screen refreshes in Flutter, it will be notified to the bound, butSchedulerBinding
driven Yes, since the screen will stop refreshing after the screen is locked, it will not be triggered again.Ticker
SchedulerBinding
Ticker
Usually we will SingleTickerProviderStateMixin
mix the class into the custom State
class, and then use the current State
object as the value AnimationController
of vsync
the parameter.
4. Tween
1 Introduction
By default, AnimationController
the range of object values is [0.0,1.0]
. If we need to build UI animation values in different ranges or different data types, we can use Tween
to add mappings to generate values in different ranges or data types.
For example, as in the following example, the Tween
generated [-200.0,0.0]
value:
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
Tween
The constructor takes begin
and end
two parameters. Tween
The sole responsibility of is to define the mapping from input ranges to output ranges. The input range is usually [0.0,1.0]
, but this is not required, we can customize the required range.
Tween
Inherit from Animatable<T>
, rather than inherit from Animation<T>
, Animatable
mainly defines the mapping rules for animation values.
Let's look at an ColorTween
example of mapping an animation input range to a transition output between two color values:
final Tween colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
The object does not store any state, instead, it provides evaluate(Animation<double> animation)
methods, which can get the animation's current mapping value. Animation
The current value of the object can value()
be obtained through the method. The function also does some additional processing, such as ensuring that the start and end states are returned when the animation value is and evaluate
, respectively .0.0
1.0
2)Tween.animate
To use Tween
an object, you call its animate()
method, passing in an AnimationController
object.
For example, the following code generates integer values from to in 500
milliseconds .0
255
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
Note animate()
that one is returned Animation
, not one Animatable
.
The following example builds a controller, a curve, and a Tween
:
final 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);
Linear interpolation lerp function
The principle of animation is actually to draw different content in each frame. Generally, the start and end states are specified, and then gradually change from the start state to the end state within a period of time, and the state value of a specific frame will be based on the animation. Therefore, Flutter defines static lerp
methods (linear interpolation) for some state properties that may be animated , such as:
// a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
lerp
The calculation of generally follows: 返回值 = a + (b - a) * t
, and other lerp
classes with methods:
Size.lerp(a, b, t)
Rect.lerp(a, b, t)
Offset.lerp(a, b, t)
Decoration.lerp(a, b, t)
Tween.lerp(t) // 起始状态和终止状态在构建 Tween 的时候已经指定了
...
It should be noted that lerp
it is a linear interpolation, which means that the return value and the animation progress t
are in a linear function ( y = kx + b
) relationship, because the image of a linear function is a straight line, so it is called linear interpolation.
If we want the animation to execute according to a curve, we can t
map to , for example, to achieve a uniform acceleration effect, then t' = at²+bt+c
specify the acceleration a
and b
(in most cases, it is necessary to ensure t'
that the value range of is [0,1]
, of course, there are some cases that may be will exceed this value range, such as the spring ( bounce
) effect), and Curve
the principle of performing animation according to different curves is essentially t
realized by mapping according to different mapping formulas.
animation basic structure
In Flutter, we can implement animation in a variety of ways. The following uses a different implementation of an example of gradually zooming in on a picture to demonstrate the difference between different implementations of animation in Flutter.
1. Basic version
Let's demonstrate the most basic animation implementation method:
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute({
Key? key}) : super(key: key);
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
// 需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// 匀速 图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() => {
});
});
// 启动动画(正向执行)
controller.forward();
}
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
dispose() {
controller.dispose(); // 路由销毁时需要释放动画资源
super.dispose();
}
}
The function in the above code addListener()
is called setState()
, so every time the animation generates a new number, the current frame is marked as dirty ( dirty
), which will cause widget
the build()
method to be called again, and in build()
it, Image
the width and height of the change, because its height and The width is now used animation.value
, so it will be gradually enlarged.
It is worth noting that the controller is released (calling dispose()
the method) when the animation is complete to prevent memory leaks.
In the above example, it is not specified Curve
, so the zoom-in process is linear (uniform speed). Next, we specify one Curve
to achieve an animation process similar to a spring effect. We only need to initState
change the code in the following to the following:
initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 3), vsync: this);
// 使用弹性曲线
animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn);
// 图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() => {
});
});
// 启动动画
controller.forward();
}
running result:
2. Use AnimatedWidget to simplify
We found that the step of updating the UI through addListener()
and in the above example setState()
is actually common, and it would be cumbersome to add such a sentence to each animation. AnimatedWidget
The class encapsulates setState()
the details of the call and allows us to widget
separate out. The refactored code is as follows:
import 'package:flutter/material.dart';
class AnimatedImage extends AnimatedWidget {
const AnimatedImage({
Key? key, required Animation<double> animation,}) : super(key: key, listenable: animation);
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
}
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute1({
Key? key}) : super(key: key);
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State<ScaleAnimationRoute> with SingleTickerProviderStateMixin {
late Animation<double> animation;
late AnimationController controller;
initState() {
super.initState();
controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
// 图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
// 启动动画
controller.forward();
}
Widget build(BuildContext context) {
return AnimatedImage(animation: animation);
}
dispose() {
controller.dispose(); // 路由销毁时需要释放动画资源
super.dispose();
}
}
3. Refactoring with AnimatedBuilder
Use AnimatedWidget
can be separated from the animation widget
, and the rendering process of the animation (that is, setting the width and height) is still in AnimatedWidget
, assuming that if we add an widget
animation of transparency change, then we need to implement another one AnimatedWidget
, which is not very elegant. If we can put The rendering process is also abstracted, it will be much better, and AnimatedBuilder
it is to separate the rendering logic, build
the code in the above method can be changed to:
Widget build(BuildContext context) {
// return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("imgs/avatar.png"),
builder: (BuildContext ctx, child) {
return Center(
child: SizedBox(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
One confusing problem with the above code is that child
it looks like it is specified twice. But what's actually happening is that the external reference child
is passed to the anonymous constructor, which then uses the object as its child AnimatedBuilder
. AnimatedBuilder
The end result is that AnimatedBuilder
the returned object is inserted into widget
the tree.
Maybe you will say that this is not much different from our initial example, but it will bring three benefits:
-
There is no need to explicitly add a frame listener and then call it
setState()
, the benefit isAnimatedWidget
the same as that of . -
Better performance: Because
widget
the scope of each frame of the animation needs to be constructed is narrowed, if notbuilder
,setState()
it will be called in the context of the parent component, which will causebuild
the method of the parent component to be called again; after havingbuilder
it, it will only cause animationwidget
Self-build
recall, avoiding unnecessaryrebuild
. -
Animations can be reused by
AnimatedBuilder
encapsulating common transition effects. Let'sGrowTransition
illustrate by encapsulating one, which canwidget
realize zoom-in animation for children:
class GrowTransition extends StatelessWidget {
const GrowTransition({
Key? key, required this.animation, this.child,}) : super(key: key);
final Widget? child;
final Animation<double> animation;
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (BuildContext context, child) {
return SizedBox(
height: animation.value,
width: animation.value,
child: child,
);
},
child: child,
),
);
}
}
This way, the original example can be changed to:
...
Widget build(BuildContext context) {
return GrowTransition(
child: Image.asset("images/avatar.png"),
animation: animation,
);
}
It is in this way that Flutter encapsulates many animations, such as: FadeTransition
, ScaleTransition
, SizeTransition
etc. These preset transition classes can be reused in many cases.
animation state monitoring
As mentioned above, we can add animation state change listeners through the method Animation
. addStatusListener()
In Flutter, there are four animation states, AnimationStatus
which are defined in the enumeration class, and we will explain them one by one below:
enumeration value | meaning |
---|---|
dismissed |
Animation stops at start point |
forward |
The animation is executing in the forward direction |
reverse |
animation is being performed in reverse |
completed |
animation stops at end |
Example: Let's change the above image zoom-in example to a loop animation that zooms in first, then zooms out, and then zooms in again. To achieve this effect, we only need to monitor the change of the animation state, that is, reverse the animation at the end of the forward execution of the animation, and execute the animation forward at the end of the reverse execution of the animation. code show as below:
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
);
animation = Tween(begin: 0.0, end: 300.0).animate(controller); // 图片宽高从0变到300
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse(); // 动画执行结束时反向执行动画
} else if (status == AnimationStatus.dismissed) {
controller.forward(); // 动画恢复到初始状态时执行动画(正向)
}
});
controller.forward(); // 启动动画(正向)
}
Custom route switching animation
The Material component library provides a MaterialPageRoute
component that can use routing switching animations consistent with the platform style, such as sliding left and right on iOS, and sliding up and down on Android. Now, if we Android
also want to use the left and right switching styles on the screen, what should we do? A simple approach can be used directly CupertinoPageRoute
, such as:
Navigator.push(context, CupertinoPageRoute(
builder: (context)=>PageB(),
));
CupertinoPageRoute
It is Cupertino
an iOS-style routing switching component provided by the component library, which realizes left and right sliding switching. So how do we customize the routing switching animation? The answer is PageRouteBuilder
.
Let's take a look at how to use PageRouteBuilder
to customize the routing switching animation. For example, if we want to realize the routing transition with fade-in animation, the implementation code is as follows:
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500), // 动画时间为500毫秒
pageBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation) {
return FadeTransition( // 使用渐隐渐入过渡
opacity: animation,
child: PageB(), // 路由B
);
},
),
);
We can see pageBuilder
that there is a parameter, which is provided by the Flutter route manager, and it will be called back at each animation frame animation
when the route is switched , so we can customize the transition animation through the object.pageBuilder
animation
Whether they are MaterialPageRoute
, CupertinoPageRoute
, or PageRouteBuilder
, they all inherit from the PageRoute
class, but they PageRouteBuilder
are actually just PageRoute
a wrapper. We can directly inherit PageRoute
the class to implement custom routing. The above example can be implemented as follows:
- define a routing class
FadeRoute
class FadeRoute extends PageRoute {
FadeRoute({
required this.builder,
this.transitionDuration = const Duration(milliseconds: 300),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor,
this.barrierLabel,
this.maintainState = true,
});
final WidgetBuilder builder;
final Duration transitionDuration;
final bool opaque;
final bool barrierDismissible;
final Color barrierColor;
final String barrierLabel;
final bool maintainState;
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) => builder(context);
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}
}
- use
FadeRoute
Navigator.push(context, FadeRoute(builder: (context) {
return PageB();
}));
Although the above two methods can realize custom switching animations, they should be used first in actual use PageRouteBuilder
, so that there is no need to define a new routing class, and it will be more convenient to use.
But sometimes PageRouteBuilder
it cannot meet the requirements. For example, when applying transition animation, we need to read some properties of the current route. At this time, we can only use inheritance PageRoute
. For example, if we only want to apply when opening a new route Animation, but do not use animation when returning, then we must judge isActive
whether the current routing property is when we build the transition animation true
, the code is as follows:
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
if (isActive) {
// 当前路由被激活,是打开新路由
return FadeTransition(
opacity: animation,
child: builder(context),
);
} else {
// 是返回,则不应用过渡动画
return Padding(padding: EdgeInsets.zero);
}
}
For detailed information about routing parameters, you can refer to the API documentation by yourself, which is relatively simple and will not be repeated here.
Hero animation
Hero
It refers to the ability to "fly" between routes (pages) widget
. In simple terms, Hero
animation means that when routes are switched, there is a shared one widget
that can switch between old and new routes. Since the shared widget
position and appearance on the old and new route pages may be different, when the route is switched, it will gradually transition from the old route to the specified position in the new route, which will generate an animation Hero
.
You may have seen hero
animation many times. For example, one route displays a thumbnail list of items for sale, and selecting an entry jumps them to a new route that contains the item's details and a "Buy" button. "Flying" an image from one route to another in Flutter is called a hero animation , although the same action is sometimes called a shared element transition . Let's use an example to experience hero
animation.
example
Suppose there are two routes A and B, and their content interaction is as follows:
A: Contains a user avatar, circular, jump to route B after clicking, you can view a larger image.
B: Display the original image of the user's avatar, a rectangle.
When jumping between the two routes of AB, the user's avatar will gradually transition to the avatar of the target routing page. Next, let's look at the code first, and then analyze it.
Route A:
class HeroAnimationRouteA extends StatelessWidget {
const HeroAnimationRouteA({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Hero动画"),),
body: Container(
alignment: Alignment.topCenter,
child: InkWell(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.asset("images/avatar.png", width: 100.0,),
),
),
onTap: () {
// 打开B路由
Navigator.push(context, PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 500),
pageBuilder: (context, animation, secondaryAnimation) {
return FadeTransition(
opacity: animation,
child: const HeroAnimationRouteB(),
);
},
));
},
),
),
);
}
}
Route B:
class HeroAnimationRouteB extends StatelessWidget {
const HeroAnimationRouteB({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
//appBar: AppBar(title: const Text("原图"),),
body: Center(
child: Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: Image.asset("images/avatar.png"),
),
),
);
}
}
Effect:
We can see that to realize Hero
the animation, you only need Hero
to wrap the shared with components widget
and provide the same tag
, and the transition frames in the middle are all automatically completed by the Flutter framework. It must be noted that the shared of the front and rear routing pages must be the sameHero
tag
, and the Flutter framework uses tag
to determine widget
the corresponding relationship between the old and new routing pages.
Hero
The principle of animation is relatively simple. The Flutter framework knows the position and size of the shared elements in the old and new routing pages, so according to these two endpoints, it is enough to find the interpolation (intermediate state) during the animation execution process. Fortunately, , these things don’t need to be done by ourselves, Flutter has already done it for us, if you are interested, you can go to the source code related to Hero animation.
interlaced animation
Sometimes we may need some complex animations. These animations may consist of an animation sequence or overlapping animations. For example: there is a histogram that needs to change color while growing in height. After growing to the maximum height, we need in Translate a certain distance on the X axis. It can be found that the above scene contains a variety of animations at different stages. To achieve this effect, it is very simple to use Stagger Animation. Interweaving animation needs to pay attention to the following points:
- To create an interleaved animation, multiple animation objects ( ) are used
Animation
. - One
AnimationController
controls all animation objects. - Specify a time interval for each animation object (
Interval
)
All animations are driven by the same AnimationController
, no matter how long the animation needs to last, the value of the controller must be 0.0
between 1.0
and , and the interval ( Interval
) of each animation must be between 0.0
and 1.0
. For each property that you animate over an interval, you need to create a separate Tween
that specifies the property's start and end values. In other words, 0.0
to 1.0
represent the entire animation process, we can specify different start and end points for different animations to determine their start time and end time.
Let's look at an example to realize an animation of the growth of a histogram:
- At the beginning, the height grows from 0 to 300 pixels, and the color fades from green to red at the same time; this process takes up 60% of the entire animation time.
- After the height grows to 300, start to translate 100 pixels to the right along the X axis; this process takes up 40% of the entire animation time.
We Widget
separate out the implementation of the animation:
class StaggerAnimation extends StatelessWidget {
StaggerAnimation({
Key? key, required this.controller }): super(key: key){
var animationFirst = CurvedAnimation(
parent: controller,
curve: const Interval(0.0, 0.6, curve: Curves.ease,),//间隔,前60%的动画时间
);
var animationAfter = CurvedAnimation(
parent: controller,
curve: const Interval(0.6, 1.0, curve: Curves.ease,), //间隔,后40%的动画时间
);
// 高度动画
height = Tween<double>(begin:.0, end: 300.0,).animate(animationFirst);
// 颜色
color = ColorTween(begin:Colors.green, end:Colors.red,).animate(animationFirst);
// 边距
padding = Tween<EdgeInsets>(
begin: const EdgeInsets.only(left: .0),
end: const EdgeInsets.only(left: 100.0),).animate(animationAfter);
}
final Animation<double> controller;
late final Animation<double> height;
late final Animation<EdgeInsets> padding;
late final Animation<Color?> color;
Widget _buildAnimation(BuildContext context, Widget? child) {
return Container(
alignment: Alignment.bottomCenter,
padding:padding.value ,
child: Container(
color: color.value,
width: 50.0,
height: height.value,
),
);
}
Widget build(BuildContext context) {
return AnimatedBuilder(
builder: _buildAnimation,
animation: controller,
);
}
}
StaggerAnimation
Three animations are defined in , which are right Container
, height
, color
and padding
attribute setting animations, and then Interval
specify the start point and end point for each animation in the entire animation process by passing. Let's implement the routing to start the animation:
class StaggerRoute extends StatefulWidget {
const StaggerRoute({
Key? key}) : super(key: key);
State createState() => _StaggerRouteState();
}
class _StaggerRouteState extends State<StaggerRoute> with TickerProviderStateMixin {
late AnimationController _controller;
void initState() {
super.initState();
_controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
}
Future<void> _playAnimation() async {
try {
//先正向执行动画
await _controller.forward().orCancel;
//再反向执行动画
await _controller.reverse().orCancel;
} on TickerCanceled {
// the animation got canceled, probably because we were disposed
}
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("StaggerAnimation"),),
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _playAnimation(),
child: Center(
child: Container(
width: 300.0,
height: 300.0,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
border: Border.all(color: Colors.black.withOpacity(0.5),),
),
//调用我们定义的交织动画Widget
child: StaggerAnimation(controller: _controller),
),
),
),
);
}
void dispose() {
//路由销毁时需要释放动画资源
_controller.dispose();
super.dispose();
}
}
Execution effect:
animation toggle component
AnimatedSwitcher
In actual development, we often encounter scenarios of switching UI elements, such as tab switching and routing switching. In order to enhance the user experience, an animation is usually specified when switching to make the switching process appear smooth. Some commonly used switching components have been provided in the Flutter SDK component library, such as PageView
, , TabView
etc. However, these components cannot cover all demand scenarios. For this reason, a AnimatedSwitcher
component is provided in the Flutter SDK, which defines a general UI switching abstract.
AnimatedSwitcher
Show and hide animations can be added to its new and old child elements at the same time. That is to say, AnimatedSwitcher
when the sub-element changes, it will animate its old element and new element. Let's first look at AnimatedSwitcher
the definition of :
const AnimatedSwitcher({
Key? key,
this.child,
required this.duration, // 新child显示动画时长
this.reverseDuration,// 旧child隐藏的动画时长
this.switchInCurve = Curves.linear, // 新child显示的动画曲线
this.switchOutCurve = Curves.linear,// 旧child隐藏的动画曲线
this.transitionBuilder = AnimatedSwitcher.defaultTransitionBuilder, // 动画构建器
this.layoutBuilder = AnimatedSwitcher.defaultLayoutBuilder, //布局构建器
})
When the AnimatedSwitcher
of child
changes (type or Key
different), the old child
will execute the hide animation, and the new child
will execute the display animation. What kind of animation effect to perform is transitionBuilder
determined by the parameter, which accepts a AnimatedSwitcherTransitionBuilder
type builder
, defined as follows:
typedef AnimatedSwitcherTransitionBuilder = Widget Function(Widget child, Animation<double> animation);
The new and old bindings will be animated separately when switching builder
:AnimatedSwitcher
child
child
- For old
child
, bound animations are executed in reverse (reverse
) - For new
child
, bound animations will point forward (forward
)
In this way, the new and old animation binding is realized child
. AnimatedSwitcher
The default value is AnimatedSwitcher.defaultTransitionBuilder
:
Widget defaultTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: child,
);
}
It can be seen that the object is returned FadeTransition
, that is to say, by default, the "fading" and "fading" animations AnimatedSwitcher
will be performed on the old and new .child
example
Let's look at an example below: implement a counter, and then during each self-increment process, the old number performs zoom-out animation and hides, and the new number performs zoom-in animation display. The code is as follows:
import 'package:flutter/material.dart';
class AnimatedSwitcherCounterRoute extends StatefulWidget {
const AnimatedSwitcherCounterRoute({
Key key}) : super(key: key);
State createState() => _AnimatedSwitcherCounterRouteState();
}
class _AnimatedSwitcherCounterRouteState extends State<AnimatedSwitcherCounterRoute> {
int _count = 0;
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(child: child, scale: animation); // 执行缩放动画
},
child: Text(
'$_count',
// 显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
key: ValueKey<int>(_count),
style: Theme.of(context).textTheme.headline4,
),
),
ElevatedButton(
child: const Text('+1',),
onPressed: () {
setState(() {
_count += 1;
});
},
),
],
),
);
}
}
Run the sample code, when the "+1" button is clicked, the original number will gradually shrink until hidden, while the new number will gradually enlarge, as shown in the figure:
Note:
AnimatedSwitcher
old and new of , must not be equalchild
if they are of the same type .Key
AnimatedSwitcher implementation principle
In fact, AnimatedSwitcher
the implementation principle of is relatively simple, and we AnimatedSwitcher
can also guess based on the way of use. In order to realize the old and new child
switching animation, only two questions need to be clarified:
- When is the animation executed?
- How to
child
animate old and new?
From AnimatedSwitcher
the way of usage, we can see that when child
there is a change ( if the or type widget
of the child key
is different, it is considered to have changed), it will be re-executed build
, and then the animation will start to execute.
We can StatefulWidget
achieve this by inheriting AnimatedSwitcher
. The specific method is didUpdateWidget
to judge child
whether its old and new have changed in the callback. If there is a change, perform child
a reverse exit ( reverse
) animation for the old child
and a forward ( forward
) entry animation for the new one. The following is AnimatedSwitcher
part of the core pseudocode of the implementation:
Widget _widget;
void didUpdateWidget(AnimatedSwitcher oldWidget) {
super.didUpdateWidget(oldWidget);
// 检查新旧child是否发生变化(key和类型同时相等则返回true,认为没变化)
if (Widget.canUpdate(widget.child, oldWidget.child)) {
// child没变化,...
} else {
//child发生了变化,构建一个Stack来分别给新旧child执行动画
_widget= Stack(
alignment: Alignment.center,
children:[
//旧child应用FadeTransition
FadeTransition(
opacity: _controllerOldAnimation,
child : oldWidget.child,
),
//新child应用FadeTransition
FadeTransition(
opacity: _controllerNewAnimation,
child : widget.child,
),
]
);
// 给旧child执行反向退场动画
_controllerOldAnimation.reverse();
//给新child执行正向入场动画
_controllerNewAnimation.forward();
}
}
//build方法
Widget build(BuildContext context){
return _widget;
}
The above pseudo-code shows AnimatedSwitcher
the core logic of the implementation. Of course, AnimatedSwitcher
the real implementation is more complicated than this. It can customize the transition animation of entering and exiting the scene and the layout when performing the animation. Here, we cut out the complicated and simplify, and we can clearly see the main implementation ideas through the form of pseudo-code, and the specific implementation can refer to AnimatedSwitcher
the source code.
In addition, a component is also provided in the Flutter SDK AnimatedCrossFade
, which can also switch between two sub-elements. The switching process performs a fading animation. The AnimatedSwitcher
difference is that it AnimatedCrossFade
is for two sub-elements , AnimatedSwitcher
but between the old and new values of a sub-element switch . AnimatedCrossFade
The implementation principle is also relatively simple, AnimatedSwitcher
similar to and, so I won’t go into details, if you are interested, you can check its source code.
Advanced usage of AnimatedSwitcher
Suppose now we want to implement an animation similar to routing translation switching: the old page screen moves to the left to exit, and the new page moves to enter from the right side of the screen. If we want to use AnimatedSwitcher
it, we will soon find a problem: it can't be done! We might write the following code:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween = Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
What's wrong with the code above? We said earlier that the new will be animated forward ( ) and the old will be animated in reverse ( ) when AnimatedSwitcher
the is switched , so the real effect is that the new does pan in from the right side of the screen, but the old does from the Exit on the right side of the screen (not the left). In fact, it is also easy to understand, because without special treatment, the forward and reverse of the same animation are exactly the opposite (symmetrical).child
child
forward
child
reverse
child
child
So the question is, can it not be used AnimatedSwitcher
? The answer is of course no! Think about this problem carefully, the reason is that the same Animation
forward ( forward
) and reverse ( reverse
) are symmetrical. So if we can break this symmetry, then we can realize this function. Next, let’s encapsulate one MySlideTransition
. The SlideTransition
only difference is that the reverse execution of the animation is customized (slide out from the left to hide), the code is as follows:
class MySlideTransition extends AnimatedWidget {
const MySlideTransition({
Key? key,
required Animation<Offset> position,
this.transformHitTests = true,
required this.child,
}) : super(key: key, listenable: position);
final bool transformHitTests;
final Widget child;
Widget build(BuildContext context) {
final position = listenable as Animation<Offset>;
Offset offset = position.value;
if (position.status == AnimationStatus.reverse) {
offset = Offset(-offset.dx, offset.dy);
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
When calling, SlideTransition
replace it with MySlideTransition
:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return MySlideTransition(
child: child,
position: tween.animate(animation),
);
},
...//省略
)
Effect:
It can be seen that in this ingenious way, we have realized an animation similar to the routing entry switch. In fact, the Flutter routing switch is also AnimatedSwitcher
realized through this method.
SlideTransitionX
In the above example, we realized the animation of "left in and right in", so what if we want to realize "left in and right out", "top in and bottom out" or "bottom in and top out"? Of course, we can modify the above code separately, but in this way, each animation has to define a "Transition" separately, which is very troublesome.
The following will encapsulate a general SlideTransitionX
to achieve this "in and out animation", the code is as follows:
import 'package:flutter/widgets.dart';
/// 实现同向滑动效果,通常和[AnimatedSwitcher]一起使用
/// Animates the position of a widget relative to its normal position
/// ignoring the animation direction(always slide along one direction).
/// Typically, is used in combination with [AnimatedSwitcher].
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key? key,
required Animation<double> position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
required this.child,
}) : super(key: key, listenable: position) {
_tween = Tween(begin: const Offset(0, 1), end: Offset.zero);
// 偏移在内部处理
switch (direction) {
case AxisDirection.up:
_tween.begin = const Offset(0, 1);
break;
case AxisDirection.right:
_tween.begin = const Offset(-1, 0);
break;
case AxisDirection.down:
_tween.begin = const Offset(0, -1);
break;
case AxisDirection.left:
_tween.begin = const Offset(1, 0);
break;
}
}
final bool transformHitTests;
final Widget child;
//退场(出)方向
final AxisDirection direction;
late final Tween<Offset> _tween;
Widget build(BuildContext context) {
final position = listenable as Animation<double>;
Offset offset = _tween.evaluate(position);
//执行反向动画时 再反一下处理
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}
Now if we want to achieve various "sliding in and out animations", it is very easy, just pass direction
different direction values, for example, to achieve "top in and bottom out", then:
AnimatedSwitcher(
duration: Duration(milliseconds: 200),
transitionBuilder: (Widget child, Animation<double> animation) {
var tween=Tween<Offset>(begin: Offset(1, 0), end: Offset(0, 0))
return SlideTransitionX(
child: child,
direction: AxisDirection.down, //上入下出
position: animation,
);
},
...//省略其余代码
)
Effect:
You can try to SlideTransitionX
take direction
different values for to see the running effect.
animation transition component
We collectively refer to the components that perform transition animation when the properties of the Widget change as "animation transition components". The most obvious feature of animation transition components is that they manage themselves internally AnimationController
. We know that in order to facilitate the user to customize the animation curve, execution time, direction, etc., in the animation packaging method introduced above, the user usually needs to provide an object to customize these attribute values AnimationController
. However, in this way, users must manually manage AnimationController
, which will increase the complexity of use. Therefore, if it can also AnimationController
be packaged, it will greatly improve the ease of use of animation components.
Custom Animated Transition Components
We want to implement one AnimatedDecoratedBox
that can decoration
perform a transition animation from the old state to the new state when the property changes. Based on what we learned earlier, we implemented a AnimatedDecoratedBox1
component:
class AnimatedDecoratedBox extends StatefulWidget {
const AnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
this.curve = Curves.linear,
required this.duration,
this.reverseDuration,
}) : super(key: key);
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration? reverseDuration;
State createState() => _AnimatedDecoratedBoxState();
}
class _AnimatedDecoratedBoxState extends State<AnimatedDecoratedBox> with SingleTickerProviderStateMixin {
AnimationController get controller => _controller;
late AnimationController _controller;
Animation<double> get animation => _animation;
late Animation<double> _animation;
late DecorationTween _tween;
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
_tween = DecorationTween(begin: widget.decoration);
_updateCurve();
}
void _updateCurve() {
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
}
void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve) _updateCurve();
_controller.duration = widget.duration;
_controller.reverseDuration = widget.reverseDuration;
//正在执行过渡动画
if (widget.decoration != (_tween.end ?? _tween.begin)) {
_tween
..begin = _tween.evaluate(_animation)
..end = widget.decoration;
_controller
..value = 0.0
..forward();
}
}
void dispose() {
_controller.dispose();
super.dispose();
}
}
Let's use it AnimatedDecoratedBox
to achieve the effect that the background color transitions from blue to red after the button is clicked:
class AnimatedDecoratedBoxExample extends StatefulWidget {
const AnimatedDecoratedBoxExample({
Key? key}) : super(key: key);
State createState() => _AnimatedDecoratedBoxExampleState();
}
class _AnimatedDecoratedBoxExampleState
extends State<AnimatedDecoratedBoxExample> {
Color _decorationColor = Colors.blue;
var duration = const Duration(seconds: 1);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("自定义动画过渡组件"),
),
body: Center(
child: AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: TextButton(
onPressed: () {
setState(() {
_decorationColor = (_decorationColor == Colors.blue
? Colors.red
: Colors.blue);
});
},
child: const Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
),
),
);
}
}
Effect:
Although the above code achieves the functions we expect, the code is more complicated. After a little thought, we can find that AnimationController
the management and Tween
update part of the code can be abstracted. If we encapsulate these general logics into base classes, then to implement animation transition components, we only need to inherit these base classes and customize ourselves. Different code (such as the construction method of each frame of the animation) is enough, which will simplify the code.
In order to facilitate developers to realize the encapsulation of animation transition components, Flutter provides an ImplicitlyAnimatedWidget
abstract class, which inherits from StatefulWidget
and provides a corresponding ImplicitlyAnimatedWidgetState
class, and AnimationController
the management is in ImplicitlyAnimatedWidgetState
the class. If developers want to encapsulate animation, they only need to inherit ImplicitlyAnimatedWidget
and ImplicitlyAnimatedWidgetState
class respectively. Let's demonstrate how to achieve it.
We need to do this in two steps:
- Inheritance
ImplicitlyAnimatedWidget
class.
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
const AnimatedDecoratedBox({
Key? key,
required this.decoration,
required this.child,
Curve curve = Curves.easeIn, //动画曲线
required Duration duration, // 正向动画执行时长
}) : super(
key: key,
curve: curve,
duration: duration,
);
final BoxDecoration decoration;
final Widget child;
ImplicitlyAnimatedWidgetState createState() => _AnimatedDecoratedBoxState();
}
Three of these curve、duration、reverseDuration
properties ImplicitlyAnimatedWidget
are defined in . You can see that the class is no different from the class it AnimatedDecoratedBox
normally inherits from .StatefulWidget
- Let
State
the class inheritAnimatedWidgetBaseState
(the class inherits fromImplicitlyAnimatedWidgetState
the class).
class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
Tween<dynamic>? _decoration; //定义一个Tween
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration?.evaluate(animation),
child: widget.child,
);
}
void forEachTween(TweenVisitor<dynamic> visitor) {
// 在需要更新Tween时,基类会调用此方法
_decoration = visitor(_decoration, widget.decoration,
(value) => DecorationTween(begin: value));
}
}
You can see that we have implemented build
and forEachTween
two methods. During the execution of the animation, build
the method will be called every frame (the calling logic is ImplicitlyAnimatedWidgetState
in), so build
we need to build DecoratedBox
the state of each frame in the method, so we have to calculate decoration
the state of each frame, which we can _decoration.evaluate(animation)
calculate by , where animation
is ImplicitlyAnimatedWidgetState
The object defined in the base class _decoration
is a type of object that we customize DecorationTween
, so the question now is when is it assigned?
To answer this question, we have to figure out when we need to assign _decoration
a value. We know _decoration
that it is one Tween
, and Tween
the main responsibility is to define the start state ( begin
) and end state ( end
) of the animation. For AnimatedDecoratedBox
, decoration
the final state is the value passed to it by the user, and the initial state is uncertain. There are two situations:
AnimatedDecoratedBox
For the first timebuild
, its value is directlydecoration
set to the initial state at this time, that is,_decoration
the value isDecorationTween(begin: decoration)
.AnimatedDecoratedBox
When the update ofdecoration
, the initial state is_decoration.animate(animation)
, that is,_decoration
the value isDecorationTween(begin: _decoration.animate(animation),end:decoration)
.
Now forEachTween
the function is obvious, it is used to update Tween
the initial value, it will be called in the above two cases, and the developer only needs to rewrite this method, and update the Tween
initial state in this method value. And some update logic is shielded in visitor
the callback, we only need to call it and pass the correct parameters to it, visitor
the method signature is as follows:
Tween<T> visitor(
Tween<T> tween, //当前的tween,第一次调用为null
T targetValue, // 终止状态
TweenConstructor<T> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
It can be seen that the encapsulation of animation transition components can be quickly realized through inheritance ImplicitlyAnimatedWidget
and classes, which simplifies the code a lot compared with our pure manual implementation.ImplicitlyAnimatedWidgetState
Flutter's preset animation transition components
There are also many animation transition components preset in the Flutter SDK, and the implementation methods are AnimatedDecoratedBox
similar to most of them, as shown in the following table:
component name | Function |
---|---|
AnimatedPadding |
padding Transition animations are performed to the new state when a change occurs |
AnimatedPositioned |
Used Stack together, when the positioning state changes, the transition animation will be performed to the new state |
AnimatedOpacity |
Perform transition animation to new state when transparency opacity changes |
AnimatedAlign |
When alignment a change occurs, a transition animation is performed to the new state |
AnimatedContainer |
When Container the property changes, the transition animation will be performed to the new state |
AnimatedDefaultTextStyle |
When the font style changes, the text component that inherits the style in the child component will dynamically transition to the new style |
Let's use an example to feel the effects of these preset animation transition components:
import 'package:flutter/material.dart';
class AnimatedWidgetsTest extends StatefulWidget {
const AnimatedWidgetsTest({
Key? key}) : super(key: key);
State createState() => _AnimatedWidgetsTestState();
}
class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
double _padding = 10;
var _align = Alignment.topRight;
double _height = 100;
double _left = 0;
Color _color = Colors.red;
TextStyle _style = const TextStyle(color: Colors.black);
Color _decorationColor = Colors.blue;
Widget build(BuildContext context) {
var duration = const Duration(seconds: 1);
return SingleChildScrollView(
child: Column(
children: <Widget>[
ElevatedButton(
onPressed: () => setState(() => _padding = 20),
child: AnimatedPadding(
duration: duration,
padding: EdgeInsets.all(_padding),
child: const Text("AnimatedPadding"),
),
),
SizedBox(
height: 50,
child: Stack(
children: <Widget>[
AnimatedPositioned(
duration: duration,
left: _left,
child: ElevatedButton(
onPressed: () => setState(() => _left = 100),
child: const Text("AnimatedPositioned"),
),
)
],
),
),
Container(
height: 100,
color: Colors.grey,
child: AnimatedAlign(
duration: duration,
alignment: _align,
child: ElevatedButton(
onPressed: () => setState(() => _align = Alignment.center),
child: const Text("AnimatedAlign"),
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
color: _color,
child: TextButton(
onPressed: () {
setState(() {
_height = 150;
_color = Colors.blue;
});
},
child: const Text(
"AnimatedContainer",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDefaultTextStyle(
style: _style,
duration: duration,
child: GestureDetector(
child: const Text("hello world"),
onTap: () {
setState(() {
_style = const TextStyle(
color: Colors.blue,
decorationStyle: TextDecorationStyle.solid,
decorationColor: Colors.blue,
);
});
},
),
),
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: TextButton(
onPressed: () => setState(() => _decorationColor = Colors.red),
child: const Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
].map((e) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: e,
);
}).toList(),
),
);
}
}
running result:
Animation source code analysis
Animation
The key classes and their relationships are shown in Figure 8-5.
In Figure 8-5, Animation
it is the key class of animation, which inherits from and Listenable
holds Animation
an animation value ( value
) and an animation state ( status
) for external monitoring. External users will update the UI and other operations based on these monitoring callbacks.
AnimationController
Is Animation
the most common implementation, it holds two key fields - Ticker
and Simulation
, the former is TickerProvider
provided by the interface, used to provide the "heartbeat" to drive animation updates, mainly used for tween animation; the latter provides update rules for animation values, Implemented by concrete subclasses.
Simulation
The default implementation of is _InterpolationSimulation
that it Curve
computes values based on concrete subclasses of . In addition, Simulation
it also provides the ability to simulate various physical effects, such as SpringSimulation
the simulation of spring effects.
Generally speaking, the field AnimationController
of value
is not used directly, _AnimatedEvaluation
it will hold an Animation
object (usually an AnimationController
instance of ) and an Animatable
object, the former provides the original value of the animation, and the latter uses the animation value of the former ( value
) as a parameter for "tweening ” ( Tween
), the final value will be exposed externally through the field _AnimatedEvaluation
of the instance .value
The following is a more specific analysis from the perspective of source code.
motion tween
Tween animation is only interpolated according to a certain rule within a specified time range, and its usage process is usually shown in Listing 8-28.
// 代码清单8-28 补间动画示例
AnimationController animationController = // 见代码清单8-29
AnimationController(vsync: this, duration: Duration(milliseconds: 1000));
Animation animation = // 见代码清单8-30
Tween(begin: 0.0,end: 10.0).animate(animationController);
animationController.addListener(() {
// 通知发生,见代码清单8-40
setState(() {
newValue = animation.value });
});
animationController.forward(); // 见代码清单8-32
In the above logic, AnimationController
it is the driver that Tween
provides the interpolation model of the tween animation Animation
as the final call exit. The following is an in-depth analysis of the code.
First analyze AnimationController
the initialization logic, as shown in Listing 8-29.
// 代码清单8-29 flutter/packages/flutter/lib/src/animation/animation_controller.dart
AnimationController({
......, required TickerProvider vsync,
}) : _direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick); // _tick将在"心跳"时触发,见代码清单8-38
_internalSetValue(value ?? lowerBound); // 更新当前动画的状态
}
void _internalSetValue(double newValue) {
_value = newValue.clamp(lowerBound, upperBound);
if (_value == lowerBound) {
_status = AnimationStatus.dismissed; // 动画尚未开始
} else if (_value == upperBound) {
_status = AnimationStatus.completed; // 已经结束
} else {
_status = (_direction == _AnimationDirection.forward) ? // 动画进行中
AnimationStatus.forward : AnimationStatus.reverse;
}
}
AnimationController
In the initialization logic of , first create an Ticker
object, which is the core driver of motion tween animation, which will be analyzed in detail later. _internalSetValue
Responsible for updating the state of the current animation, so I won't go into details here.
In Code Listing 8-28, animation.value
the current value will be obtained when each frame of animation is updated, and the calculation process is shown in Code Listing 8-30.
// 代码清单8-30 flutter/packages/flutter/lib/src/animation/tween.dart
abstract class Animatable<T> {
T transform(double t); // 见代码清单8-31
T evaluate(Animation<double> animation) => transform(animation.value);
Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}
} // Animatable
class _AnimatedEvaluation<T> extends Animation<T>
with AnimationWithParentMixin<double> {
_AnimatedEvaluation(this.parent, this._evaluatable);
final Animation<double> parent; // 通常为AnimationController
final Animatable<T> _evaluatable; // 见代码清单8-31
// 代码清单8-28中获取的值
T get value => _evaluatable.evaluate(parent); // 见前面内容
}
Tween
It is Animatable
a subclass whose animate
method is implemented by the parent class and mainly returns _AnimatedEvaluation
the object, so animation.value
when calling, the essence is to call the specific interpolation model, that is, the method of Animatable
the specific subclass (such as ) , but in fact it calls the method, as an example, its The logic is shown in Listing 8-31.Tween
evaluate
evaluate
transform
Tween
// 代码清单8-31 flutter/packages/flutter/lib/src/animation/tween.dart
class Tween<T extends dynamic> extends Animatable<T> {
Tween({
this.begin, this.end,});
T lerp(double t) {
// Linear Interpolation
return begin + (end - begin) * t as T;
}
T transform(double t) {
// t 即AnimationController的值,在代码清单8-33中进行定义
if (t == 0.0) return begin as T;
if (t == 1.0) return end as T;
return lerp(t); // 线性差值的逻辑
}
}
The above logic is a typical linear interpolation process. The parameter is AnimationController.value
that the value will be updated every frame, and Tween
the linear interpolation will be completed based on this value, and the result will be _AnimatedEvaluation
the value of the object.
Next, analyze AnimationController.value
the updated driving mechanism, that is, forward
the method, as shown in Listing 8-32.
// 代码清单8-32 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture forward({
double? from }) {
// AnimationController
_direction = _AnimationDirection.forward;
if (from != null) value = from;
return _animateToInternal(upperBound);
}
TickerFuture _animateToInternal(double target, // 即upperBound
{
Duration? duration, Curve curve = Curves.linear }) {
double scale = 1.0;
if (SemanticsBinding.instance!.disableAnimations) {
...... } // SKIP 禁用动画的逻辑
Duration? simulationDuration = duration; // 第1步,计算动画的执行时长
if (simulationDuration == null) {
// 没有指定动画时长
final double range = upperBound - lowerBound; // 根据当前进度,以1s为基准计算
final double remainingFraction = range.isFinite ? (target - _value).abs() /
range : 1.0;
final Duration directionDuration = // 根据动画是顺序还是逆序来计算最终的执行时长
(_direction == _AnimationDirection.reverse && reverseDuration != null)
? reverseDuration! : this.duration!;
simulationDuration = directionDuration * remainingFraction;
} else if (target == value) {
// 动画已完成,对应第2步
simulationDuration = Duration.zero;
}
stop(); // 见代码清单8-41
if (simulationDuration == Duration.zero) {
// 第2步,动画结束,完成字段更新,并通知
if (value != target) {
_value = target.clamp(lowerBound, upperBound); // 修正值到合法的目标值
notifyListeners(); // 通知值变化,见代码清单8-40
}
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed : AnimationStatus.dismissed;
_checkStatusChanged(); // 更新动画状态,见代码清单8-40
return TickerFuture.complete();
} // if
return _startSimulation(_InterpolationSimulation( // 第3步,开始动画
_value, target, simulationDuration, curve, scale));
}
The above logic is mainly divided into 3 steps. The first step is simulationDuration
the calculation, which will be calculated based on the ratio of the remaining value to the total value, stop
and the logic will be introduced later. The second step is mainly to deal simulationDuration
with 0
the situation that the animation has ended, and at this time, the assignment of the field and the notification of the state are performed. The third step is to actually start the animation. Note that Simulation
the specific implementation class here is _InterpolationSimulation
that it is a linear interpolation simulator, which will be analyzed in detail later. The logic of the first analysis _startSimulation
is shown in Listing 8-33.
// 代码清单8-33 flutter/packages/flutter/lib/src/animation/animation_controller.dart
double get value => _value;
TickerFuture _startSimulation(Simulation simulation) {
// AnimationController
_simulation = simulation;
_lastElapsedDuration = Duration.zero; // 截止到上一帧动画,已消耗的时间
_value = simulation.x(0.0).clamp(lowerBound, upperBound);
// 起始值,x方法见代码清单8-39
final TickerFuture result = _ticker!.start(); // 开始请求"心跳",驱动动画
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.forward : AnimationStatus.reverse;
_checkStatusChanged();
return result;
}
The above logic uses _ticker!.star
the t method to start the animation and update _status
the state at the same time. start
The logic of the method is shown in Listing 8-34.
// 代码清单8-34 flutter/packages/flutter/lib/src/scheduler/ticker.dart
TickerFuture start() {
_future = TickerFuture._(); // 表示一个待完成的动画
if (shouldScheduleTick) {
scheduleTick(); } // 请求"心跳"
if (SchedulerBinding.instance!.schedulerPhase.index
> SchedulerPhase.idle.index
&& SchedulerBinding.instance!.schedulerPhase.index
< SchedulerPhase.postFrameCallbacks.index) // 在这个阶段内应修正动画开始时间
_startTime = SchedulerBinding.instance!.currentFrameTimeStamp;
return _future!;
}
The above logic is mainly to scheduleTick
initiate a "heartbeat" through the method, and there is one detail that needs attention: if a frame is currently being processed, it _startTime
will be counted from Vsync
the arrival time of the signal of the current frame. This is a small correction, namely If a frame is already being rendered when the animation starts, the animation state for the next frame should be equivalent to the state of the animation two frames later. scheduleTick
The logic of the method is shown in Listing 8-35.
// 代码清单8-35 flutter/packages/flutter/lib/src/scheduler/ticker.dart
bool get muted => _muted; // 是否为静默状态
bool _muted = false;
bool get isActive => _future != null; // 存在动画
bool get scheduled => _animationId != null; // 已经在等待"心跳"
// 调用逻辑见代码清单8-37,用于判断是否需要"心跳"
bool get shouldScheduleTick => !muted && isActive && !scheduled;
void scheduleTick({
bool rescheduling = false }) {
_animationId = SchedulerBinding.instance!.scheduleFrameCallback(
_tick, rescheduling: rescheduling); // 见代码清单8-36
}
In the above logic, the functions of several key attribute fields have been marked, and scheduleFrameCallback
the method will _tick
register the function in the callback list when the next Vsync
signal arrives, as shown in Listing 8-36.
// 代码清单8-36 flutter/packages/flutter/lib/src/scheduler/ticker.dart
int scheduleFrameCallback(FrameCallback callback, {
bool rescheduling = false }) {
scheduleFrame(); // 见代码清单5-20
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] // 处理逻辑见代码清单5-36
= _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
_transientCallbacks
callback
Its callback , that is, the method, will be processed after the Vsync signal arrives _tick
, as shown in Listing 8-37.
// 代码清单8-37 flutter/packages/flutter/lib/src/scheduler/ticker.dart
void _tick(Duration timeStamp) {
// Ticker
_animationId = null;
_startTime ??= timeStamp; // 首次"心跳"的时间戳,timeStamp是当次"心跳"的时间戳
// 注意这里的 ??= 语法表明只会记录首次"心跳"发生时的时间戳
_onTick(timeStamp - _startTime!); // 见代码清单8-38,参数为动画已经执行的时间
if (shouldScheduleTick) scheduleTick(rescheduling: true);
// 如有必要,等待下一次"心跳"
}
So far, it can be said with certainty that the so-called "heartbeat" ( tick
) is actually Vsync
a signal, _startTime
which will only be assigned once, indicating the timestamp of the start of the animation, and then call _onTick
the method with the time difference as a parameter (that is, the parameter of the previous content - _tick
function), if the logic shouldScheduleTick
After completion true
, it will continue to register Vsync
the signal to drive the generation of the next "heartbeat". The logic of the analysis method below _tick
is shown in Listing 8-38.
// 代码清单8-38 flutter/packages/flutter/lib/src/animation/animation_controller.dart
// 该方法的注册逻辑位于代码清单8-29的createTicker方法中
void _tick(Duration elapsed) {
// AnimationController
_lastElapsedDuration = elapsed; // 动画已经执行的时间
final double elapsedInSeconds = // 毫秒转秒
elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
assert(elapsedInSeconds >= 0.0);
_value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound); // 插值
if (_simulation!.isDone(elapsedInSeconds)) {
// 判断是否完成,见代码清单8-39
_status = (_direction == _AnimationDirection.forward) ? // 完成则更新状态
AnimationStatus.completed :AnimationStatus.dismissed;
stop(canceled: false); // 停止动画,见代码清单8-41
} // 否则,代码清单8-37中的shouldScheduleTick为true,将继续等待"心跳"(请求Vsync)
notifyListeners(); // 见代码清单8-40
_checkStatusChanged();
}
The above logic first calculates the duration of the animation that has been executed in seconds, then calls _simulation
the x
method to calculate the current value, and finally judges whether the animation is complete, and broadcasts _value
the changes of its own fields.
First analyze x
the method, Tween
animation defaults to _InterpolationSimulation
, as shown in Listing 8-39.
// 代码清单8-39 flutter/packages/flutter/lib/src/animation/animation_controller.dart
// _InterpolationSimulation
double x(double timeInSeconds) {
final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
if (t == 0.0) return _begin;
else if (t == 1.0) return _end;
else return _begin + (_end - _begin) * _curve.transform(t);
}
// 是否完成完全取决于动画执行的时间,注意与代码清单8-44中介绍的物理动画进行区分
bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
In the above logic, _curve
the default is Curves.linear
to return t
itself, so the return value is t
a linear function, and the coefficient is ( _end - _begin
).
Secondly, analyze _value
the notification logic after the calculation is completed, as shown in the code list 8-40.
// 代码清单8-40 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void notifyListeners() {
final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
for (final VoidCallback listener in localListeners) {
InformationCollector? collector;
try {
if (_listeners.contains(listener)) listener(); // 通知监听器
} catch (exception, stack) {
...... }
}
}
void _checkStatusChanged() {
final AnimationStatus newStatus = status;
if (_lastReportedStatus != newStatus) {
// 状态发生改变才需要通知
_lastReportedStatus = newStatus;
notifyStatusListeners(newStatus);
}
}
In the above logic, notifyListeners
the method is responsible for _value
the change of the notification, which is basically called every frame; notifyStatusListeners
it is responsible for the change of the notification state, and it will only be triggered when the animation state changes.
The logic of the final analysis isDone
. For _InterpolationSimulation
it, its logic is shown in code list 8-39, that is, it stops when the execution time exceeds the target time. This is also the characteristic of tween animation, that is, time is the determining factor of the "heartbeat" of animation, and users can only customize specific interpolation rules.
Let's continue to analyze stop
how the method stops the animation, as shown in Listing 8-41.
// 代码清单8-41 flutter/packages/flutter/lib/src/animation/animation_controller.dart
void stop({
bool canceled = false }) {
if (!isActive) return;
final TickerFuture localFuture = _future!;
_future = null;
_startTime = null;
unscheduleTick(); // 停止等待"心跳"
if (canceled) {
localFuture._cancel(this);
} else {
localFuture._complete();
}
}
The above logic is mainly to reset animation-related fields, so I won’t go into details here.
In general, developers can also drive the execution of the next frame build
through the method in the method , so as to achieve the effect of animation, but it provides a flexible, scalable, and easy-to-manage resource framework, which saves a lot of resources for developers. energy.Future+setState
Animation
Summarize:
The essence of tween animation is to register Vsync
the signal callback to the bottom layer:
-
ticker.start
After the method starts the animation, callscheduleTick
--> callSchedulerBinding.instance!.scheduleFrameCallback
--> callscheduleFrame()
to go through the drawing process, -
And in
FrameCallback
will execute_tick
the method, whichonTick
will callnotifyListeners()
the notification animation to monitor and call backvalue
the change of the current animation, which will be called basically every frame;notifyStatusListeners
it is responsible for the change of the notification state, which will only be triggered when the animation state changes.
Supplement: scheduleFrame
The logic of the method is shown in Code Listing 5-20. It will initiate a request to the Engine through the interface, requesting rendering when window.scheduleFrame
the next signal arrives.Vsync
// 代码清单5-20 flutter/packages/flutter/lib/src/scheduler/binding.dart
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) return;
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}
physical animation
Although tween animation is flexible, its time is often fixed, which is not suitable for some scenes. Take Figure 8-6 as an example. When the user drags the box away from the center point, if you want the box to return to its original position with a spring-dragged animation effect, it is difficult to realize the tween animation, because the completion time of the animation depends on the user's sliding distance. Velocity and various properties of the spring. At this point you need to use Physics (Physics) animation.
Code List 8-42 is the key implementation code for the animation effect in Figure 8-6.
// 代码清单8-42 物理动画示例
void _runAnimation(Offset pixelsPerSecond, Size size) {
// pixelsPerSecond表示拖曳手势结束、动画开始时
// 方块由于拖曳手势而在X、Y方向上因物理惯性而产生的移动速度
_animation = _controller.drive( // 触发AlignmentTween的animate方法
AlignmentTween(begin: _dragAlignment, end: Alignment.center,));
// 由6.2节可知,由于Alignment的坐标系是[0,1]形式的,因此需要除以屏幕大小,转为比例
final unitsPerSecondX = pixelsPerSecond.dx / size.width;
final unitsPerSecondY = pixelsPerSecond.dy / size.height;
final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
final unitVelocity = unitsPerSecond.distance;
const spring = SpringDescription(mass: 30, stiffness: 1, damping: 1,);
// 弹簧的各种属性
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);
// 构造一个弹簧物理模型
_controller.animateWith(simulation);
}
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;});
});
}
In the above logic, pixelsPerSecond
it is a parameter carried by the callback of the dragging gesture, indicating the current user's sliding speed, that is, the speed at which the box is pulled out. _dragAlignment
Represents the real-time position of the block, so when _runAnimation
the method starts executing, its value is exactly the real position of the animation. At this point, we know where the cube starts and ends, and the velocity at which the drag gesture ends. Next is the construction of the physical model of the sliding process, mainly the calculated SpringSimulation
parameters, unitVelocity
that is, the sliding speed of the spring in the linear direction, opposite to the direction of the spring.
The following direct analysis animateWith
method, as shown in the code list 8-43.
// 代码清单8-43 flutter/packages/flutter/lib/src/animation/animation_controller.dart
TickerFuture animateWith(Simulation simulation) {
stop(); // 见代码清单8-41
_direction = _AnimationDirection.forward;
return _startSimulation(simulation); // 见代码清单8-33
}
Since _startSimulation
the previous content of the logic has been analyzed, the method Simulation
of the x
method can be directly analyzed isDone
, as shown in the code list 8-44.
// 代码清单8-44 flutter/packages/flutter/lib/src/physics/spring_simulation.dart
class SpringSimulation extends Simulation {
final _SpringSolution _solution; // 弹簧物理模型的抽象表示
double x(double time) => _endPosition + _solution.x(time);
double dx(double time) => _solution.dx(time);
bool isDone(double time) {
return nearZero(_solution.x(time), tolerance.distance) &&
nearZero(_solution.dx(time), tolerance.velocity);
}
}
From the above logic, we can see SpringSimulation
that the main logic of the field is handed over to _solution
the field, which is created by the code in Listing 8-42 SpringDescription
, and the physical model of the spring will not be specifically analyzed here. SpringSimulation
The isDone
method is also relatively easy to understand, that is, to judge whether the current position has reached the target position. This is also a key difference between tween animation and physical animation - isDone
the realization of the method: judge whether to end according to whether the physical position meets the condition rather than the time factor.
reference: