For the convenience of presentation, this book stipulates that the components that perform transition animation when the Widget property changes are collectively referred to as "animation transition components", and the most obvious feature of an animation transition component is that it will manage itself 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 AnimationControllerobject to customize these attribute values. However, in this way, users must manually manage AnimationController, which will increase the complexity of use. Therefore, if it can also be AnimationControllerpackaged, the ease of use of the animation component will be greatly improved.
We want to implement one AnimatedDecoratedBox, which decorationcan perform a transition animation in the process from the old state to the new state when the property changes. Based on what we have learned before, we have implemented a AnimatedDecoratedBox1component:
class AnimatedDecoratedBox1 extends StatefulWidget {
AnimatedDecoratedBox1({
Key key,
@required this.decoration,
this.child,
this.curve = Curves.linear,
@required this.duration,
this.reverseDuration,
});
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration reverseDuration;
@override
_AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}
class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
with SingleTickerProviderStateMixin {
@protected
AnimationController get controller => _controller;
AnimationController _controller;
Animation<double> get animation => _animation;
Animation<double> _animation;
DecorationTween _tween;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child){
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
_tween = DecorationTween(begin: widget.decoration);
_updateCurve();
}
void _updateCurve() {
if (widget.curve != null)
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
else
_animation = _controller;
}
@override
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();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Let's use it AnimatedDecoratedBox1to achieve the effect of the background color transitioning from blue to red after the button is clicked:
Color _decorationColor = Colors.blue;
var duration = Duration(seconds: 1);
...//省略无关代码
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
The effect before clicking is shown in Figure 9-8, and a frame of the transition process is captured after clicking as shown in Figure 9-9: 

After clicking, the button background color will transition from blue to red. Figure 9-9 is a frame in the transition process, which is a bit purple. The background will turn red after the entire transition animation is over.
Although the above code achieves the desired function, the code is more complicated. After a little thought, we can find that AnimationControllerthe management and Tween update part of the code can be abstracted out. If our common logic is encapsulated into a base class, then we only need to inherit these base classes to implement animation transition components, and then customize Your own 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 ImplicitlyAnimatedWidgetabstract class, which inherits from StatefulWidget, and also provides a corresponding ImplicitlyAnimatedWidgetStateclass. AnimationControllerThe management is in the ImplicitlyAnimatedWidgetStateclass. If developers want to encapsulate animation, they only need to inherit ImplicitlyAnimatedWidgetand ImplicitlyAnimatedWidgetStateclass separately . Below we demonstrate how to implement it.
We need to implement it in two steps:
- Inheritance
ImplicitlyAnimatedWidgetclass.
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear, //动画曲线
@required Duration duration, // 正向动画执行时长
Duration reverseDuration, // 反向动画执行时长
}) : super(
key: key,
curve: curve,
duration: duration,
reverseDuration: reverseDuration,
);
final BoxDecoration decoration;
final Widget child;
@override
_AnimatedDecoratedBoxState createState() {
return _AnimatedDecoratedBoxState();
}
}
Wherein curve, duration, reverseDurationthree attributes ImplicitlyAnimatedWidgetdefined in. You can see AnimatedDecoratedBoxthat StatefulWidgetthe class is no different from the normal inherited class.
- The State class inherits from
AnimatedWidgetBaseState(the class inherits from theImplicitlyAnimatedWidgetStateclass).
class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
DecorationTween _decoration; //定义一个Tween
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(visitor) {
// 在需要更新Tween时,基类会调用此方法
_decoration = visitor(_decoration, widget.decoration,
(value) => DecorationTween(begin: value));
}
}
You can see that we have implemented buildand forEachTweentwo methods. During the execution of the animation, the buildmethod is called every frame (the calling logic is ImplicitlyAnimatedWidgetStatein), so in the buildmethod we need to construct the DecoratedBoxstate of each frame , so decorationwe have _decoration.evaluate(animation)to calculate the state of each frame . This can be calculated by, where animationis ImplicitlyAnimatedWidgetStateThe object defined in the base class is _decorationa DecorationTweentype of object that we customize. The question now is when is it assigned? To answer this question, we have to figure out when we need to _decorationassign values. We know that it _decorationis a Tween, and the main responsibility of Tween is to define the beginning and ending states of the animation. For this AnimatedDecoratedBox, decorationthe end state is the value passed to it by the user, and the initial state is uncertain. There are two situations:
AnimatedDecoratedBoxFor the first build, directly set itsdecorationvalue to the initial state, which_decorationis the valueDecorationTween(begin: decoration).AnimatedDecoratedBoxWhendecorationupdating, the initial state is_decoration.animate(animation), that_decorationis , the valueDecorationTween(begin: _decoration.animate(animation),end:decoration).
Now forEachTweenthe role is obvious, it is used to update the initial value of Tween, it will be called in the above two cases, and the developer only needs to rewrite this method, and update the effect of Tween in this method The initial state value is sufficient. And some updated logic is shielded in the visitorcallback, we only need to call it and pass it the correct parameters. The visitormethod signature is as follows:
Tween visitor(
Tween<dynamic> tween, //当前的tween,第一次调用为null
dynamic targetValue, // 终止状态
TweenConstructor<dynamic> constructor,//Tween构造器,在上述三种情况下会被调用以更新tween
);
As you can see, the encapsulation of animation transition components can be quickly realized through inheritance ImplicitlyAnimatedWidgetand ImplicitlyAnimatedWidgetStateclasses. Compared with our pure manual implementation, the code is much simplified.
If the reader still has doubts, it is recommended to check
ImplicitlyAnimatedWidgetStatethe source code and compare and understand with this sample code.
When using the animation transition component, we only need to rebuild the component after changing some property values. Therefore, to achieve the reverse transition of the state, only need to exchange the state values before and after it can be realized, which does not need to waste pen and ink. . However ImplicitlyAnimatedWidget, there is an reverseDurationattribute in the constructor to set the execution time of the reverse animation. This seems to tell the reader ImplicitlyAnimatedWidgetthat it also provides an interface for performing reverse animation. So I checked the ImplicitlyAnimatedWidgetStatesource code and found no interface for performing reverse animation. The only useful thing is that it exposes the control animation controller. So if you want to make it reverseDurationeffective, we can only get controllerit first , and then use it controller.reverse()to start the reverse animation. For example, based on the example above, we can implement a cyclic click background color change effect, requiring the animation execution time when changing from blue to red It is 400ms, and the change from red to blue is 2s. To make it reverseDurationeffective, we need to do this:
AnimatedDecoratedBox(
duration: Duration( milliseconds: 400),
decoration: BoxDecoration(color: _decorationColor),
reverseDuration: Duration(seconds: 2),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
if (_decorationColor == Colors.red) {
ImplicitlyAnimatedWidgetState _state =
context.findAncestorStateOfType<ImplicitlyAnimatedWidgetState>();
// 通过controller来启动反向动画
_state.controller.reverse().then((e) {
// 经验证必须调用setState来触发rebuild,否则状态同步会有问题
setState(() {
_decorationColor = Colors.blue;
});
});
} else {
setState(() {
_decorationColor = Colors.red;
});
}
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
The above code is actually very bad and unnecessary. It requires us to understand the ImplicitlyAnimatedWidgetStateinternal implementation and manually start the reverse animation. We can achieve the same effect through the following code:
AnimatedDecoratedBox(
duration: Duration(
milliseconds: _decorationColor == Colors.red ? 400 : 2000),
decoration: BoxDecoration(color: _decorationColor),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
setState(() {
_decorationColor = _decorationColor == Colors.blue
? Colors.red
: Colors.blue;
});
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
Isn't this code much more elegant! So now the question is, why ImplicitlyAnimatedWidgetStateprovide a reverseDurationparameter? The author carefully studied the ImplicitlyAnimatedWidgetStateimplementation and found that the only explanation is that the parameter is not for ImplicitlyAnimatedWidgetStateuse, but for subclasses! The reason is as we said before. To make it reverseDurationuseful, you must get the controllerproperty to manually start the reverse animation. ImplicitlyAnimatedWidgetStateThe controllerproperty in is a protection property, defined as follows:
@protected
AnimationController get controller => _controller;
In principle, the protection attribute should only be used in subclasses, and should not be used externally like the sample code above. In summary, we can draw two conclusions:
-
When using animated transitions assembly if necessary to perform a reverse animation scenes, a state should be used interchangeably method, and should not be acquired by
ImplicitlyAnimatedWidgetStatethecontrollerway. -
If our custom animation transition component is not used
reverseDuration, it is best not to expose this parameter. For example, weAnimatedDecoratedBoxcan removereverseDurationoptional parameters from our custom definition above , such as:
class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear,
@required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
Many animation transition components are also preset in the Flutter SDK, and most of them are implemented in the same way AnimatedDecoratedBox, as shown in Table 9-1:
| Component Name | Feature |
|---|---|
| AnimatedPadding | When the padding changes, the transition animation will be executed to the new state |
| AnimatedPositioned | Used together with Stack, when the positioning state changes, a transition animation will be executed to the new state. |
| AnimatedOpacity | Perform a transition animation to a new state when the transparency opacity changes |
| AnimatedContainer | When the Container property changes, a transition animation will be performed to the new state. |
| AnimatedDefaultTextStyle | When the font style changes, the text components that inherit the style in the child components will dynamically transition to the new style. |
Table 9-1: Flutter preset animation transition components
Let's use an example to experience the effects of these preset animation transition components:
import 'package:flutter/material.dart';
class AnimatedWidgetsTest extends StatefulWidget {
@override
_AnimatedWidgetsTestState 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 = TextStyle(color: Colors.black);
Color _decorationColor = Colors.blue;
@override
Widget build(BuildContext context) {
var duration = Duration(seconds: 5);
return SingleChildScrollView(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {
_padding = 20;
});
},
child: AnimatedPadding(
duration: duration,
padding: EdgeInsets.all(_padding),
child: Text("AnimatedPadding"),
),
),
SizedBox(
height: 50,
child: Stack(
children: <Widget>[
AnimatedPositioned(
duration: duration,
left: _left,
child: RaisedButton(
onPressed: () {
setState(() {
_left = 100;
});
},
child: Text("AnimatedPositioned"),
),
)
],
),
),
Container(
height: 100,
color: Colors.grey,
child: AnimatedAlign(
duration: duration,
alignment: _align,
child: RaisedButton(
onPressed: () {
setState(() {
_align = Alignment.center;
});
},
child: Text("AnimatedAlign"),
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
color: _color,
child: FlatButton(
onPressed: () {
setState(() {
_height = 150;
_color = Colors.blue;
});
},
child: Text(
"AnimatedContainer",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDefaultTextStyle(
child: GestureDetector(
child: Text("hello world"),
onTap: () {
setState(() {
_style = TextStyle(
color: Colors.blue,
decorationStyle: TextDecorationStyle.solid,
decorationColor: Colors.blue,
);
});
},
),
style: _style,
duration: duration,
),
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
].map((e) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: e,
);
}).toList(),
),
);
}
}
The effect after running is shown in Figure 9-10:
Readers can click on the corresponding component to view the actual operating effect.
