Flutter 动画分析之 AnimatedWidget&ImplicitlyAnimatedWidget
本文讨论的 Flutter 动画主要限定在:随着每一帧的变化,修改 Flutter Widget 的大小、颜色、位置等属性,使之看起来从一种状态渐变为另外一种状态 这一范围。
在之前的文章中,我们将 Flutter 中动画的实现分为 底层实现 和 封装好的 Widget 两大部分,目前已经分析了底层实现的部分:
而 Flutter 封装好的动画 Widget 主要分为两大类:
ImplicitlyAnimatedWidget 隐式动画,关于动画的开始、停止等都封装在 Widget 内部,只要 Widget 前后传入的值不同 便可以自动从 old 渐变到 new,内置的这些类主要以 AnimatedFoo 命名。
AnimatedWidget,显式动画,需要使用者自己创建 Animation(一般是 AnimationController)并通过其 主动管理动画,此类 Widget 主要是监听 AnimationController 的值并刷新 Widget 的内容。
此类 Widget 主要有三种使用方式:
- 继承 AnimatedWidget
- 使用 AnimatedBuilder
- 使用各种内置的 AnimatedWidget 子类,一般以 FooTransition 命名。
对于 Flutter 中这些与动画有关的类如何选择,Flutter 官方给了一张图:
简单来说,Flutter 有一些内置的动画,在要写动画的时候,可以依次考虑(实现程度由易到难):
- AnimatedFoo 参考文章,设置新的状态,这些控件会自动从之前的状态切换到新状态
- TweenAnimationBuilder 参考文章,将任意属性在 Tween 指定的范围变化,和上面的 AnimatedFoo 都是属于Implicitly animated widgets(隐式动画,由系统控件控制动画)。
- FooTranslation
- AnimatedBuilder / AnimatedWidget
- CustomPainter
本文将对 Flutter 内置封装好的动画相关的 Widget 的实现和用法进行简单分析。
源码分析
按照上述分析,Flutter 中的动画 Widget 可以大体分为 隐式动画 和 显式动画 两种。
ImplicitlyAnimatedWidget
ImplicitlyAnimatedWidgets (and their subclasses) automatically animate changes in their properties whenever they change.
隐式动画内部持有 AnimationController 以管理动画,默认没有动画,当使用不同的值重新构建 Widget 的时候,会执行动画,使用者只能设置 Duration 和 Curve,如果想要更深入的控制动画(比如暂停动画)则应该使用 AnimatedWidget。
ImplicitlyAnimatedWidget 主要分为 2 大类:
- TweenAnimationBuilder, which animates any property expressed by a Tween to a specified target value.
- AnimatedFoo
- AnimatedAlign, which is an implicitly animated version of Align.
- AnimatedContainer, which is an implicitly animated version of Container.
- AnimatedDefaultTextStyle, which is an implicitly animated version of DefaultTextStyle.
- AnimatedScale, which is an implicitly animated version of Transform.scale.
- AnimatedRotation, which is an implicitly animated version of Transform.rotate.
- AnimatedSlide, which implicitly animates the position of a widget relative to its normal position.
- AnimatedOpacity, which is an implicitly animated version of Opacity.
- AnimatedPadding, which is an implicitly animated version of Padding.
- AnimatedPhysicalModel, which is an implicitly animated version of PhysicalModel.
- AnimatedPositioned, which is an implicitly animated version of Positioned.
- AnimatedPositionedDirectional, which is an implicitly animated version of PositionedDirectional.
- AnimatedTheme, which is an implicitly animated version of Theme.
- AnimatedCrossFade, which cross-fades between two given children and animates itself between their sizes.
- AnimatedSize, which automatically transitions its size over a given duration.
- AnimatedSwitcher, which fades from one widget to another.
我们简单分析一下 ImplicitlyAnimatedWidget 和 TweenAnimationBuilder:
abstract class ImplicitlyAnimatedWidget extends StatefulWidget {
const ImplicitlyAnimatedWidget({
Key? key,
this.curve = Curves.linear,
required this.duration,
this.onEnd,
}) : super(key: key);
/// The curve to apply when animating the parameters of this container.
final Curve curve;
/// The duration over which to animate the parameters of this container.
final Duration duration;
/// Called every time an animation completes.
///
/// This can be useful to trigger additional actions (e.g. another animation)
/// at the end of the current animation.
final VoidCallback? onEnd;
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState();
}
ImplicitlyAnimatedWidget.createState()
必须返回 ImplicitlyAnimatedWidgetState 或者 AnimatedWidgetBaseState 及其子类。
ImplicitlyAnimatedWidget 作为 StatefulWidget,它的主要逻辑在 ImplicitlyAnimatedWidgetState 中:
abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> {
// 隐式动画内部维护着自己的 AnimationController
AnimationController get controller => _controller;
late final AnimationController _controller = AnimationController(
duration: widget.duration,
debugLabel: kDebugMode ? widget.toStringShort() : null,
vsync: this,
);
// 驱动此隐式动画的 animation
Animation<double> get animation => _animation;
late Animation<double> _animation = _createCurve();
CurvedAnimation _createCurve() {
return CurvedAnimation(parent: _controller, curve: widget.curve);
}
}
ImplicitlyAnimatedWidgetState 中,根据传入的 Duration 和 Curve,创建并持有了 AnimationController 和 Animation<double> 用于驱动隐式动画。
initState
在ImplicitlyAnimatedWidgetState.initState
方法中:
- 监听
_controller
的状态,当AnimationStatus.completed
时回调ImplicitlyAnimatedWidget.onEnd
方法; - 此外还调用了
_constructTweens()
遍历 Tween,并调用由子类实现的forEachTween()
方法(子类在此方法内部,获取到对应的 Tween,比如 Padding,从而在监听到_controller
变化并触发 rebuilt 时使用Animatable.evaluate()
方法获取并显示最新的属性,实现动画效果); - 最后还调用了
didUpdateTweens()
方法通知子类 Tweens 发生变化了。
_constructTweens
_constructTweens()
方法会创建一个 TweenVisitor<dynamic>并传给子类 forEachTween()
方法,子类可以使用其获取对应的 Tween 对象。
_constructTweens()
则在此过程中,使用_shouldAnimateTween()
得知了子类中是否有 Tween 可以开始动画——shouldStartAnimation
。
bool _constructTweens() {
bool shouldStartAnimation = false;
// forEachTween 方法由子类实现
forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
if (targetValue != null) {
tween ??= constructor(targetValue);
// 判断 targetValue 是否不等于 Tween.end
if (_shouldAnimateTween(tween, targetValue))
shouldStartAnimation = true;
} else {
tween = null;
}
return tween;
});
return shouldStartAnimation;
}
bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
return targetValue != (tween.end ?? tween.begin);
}
didUpdateWidget
当 ImplicitlyAnimatedWidget 被重新创建时,会调用 ImplicitlyAnimatedWidgetState.didUpdateWidget 方法。
在此方法中,除了检查并更新 Curve、Duration、Tween 之外,最重要的是使用 AnimationController.forward() 开启了动画。也就是说——“ImplicitlyAnimatedWidget 第一次插入 Widget Tree 时没有动画,当再次被更新时会触发动画”。
void didUpdateWidget(T oldWidget) {
super.didUpdateWidget(oldWidget);
// 如果 Curve 变化则创建新的 CurveAnimation
if (widget.curve != oldWidget.curve) {
(_animation as CurvedAnimation).dispose();
_animation = _createCurve();
}
// 更新 duration
_controller.duration = widget.duration;
if (_constructTweens()) {
// 如果 Tween 可以开始,则更新其 begin 和 end 值
forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
_updateTween(tween, targetValue);
return tween;
});
// 当 Widget 更新后,开始动画
_controller
..value = 0.0
..forward();
didUpdateTweens();
}
}
forEachTween
子类必须实现此方法,使用传入的 TweenVisitor 创建自己对应的 Tween。
void forEachTween(TweenVisitor<dynamic> visitor);
didUpdateTweens
当 Tween 变化时会调用此方法通知子类,子类(可选)可以实现此方法。
到目前为止,我们的 AnimationController 已经控制动画开始执行,但是因为没有监听 AnimationController.value 的变化,所以还不能自动触发 ImplicitlyAnimatedWidgetState.build() 方法。
为了实现动画效果,子类可以选择自己主动监听 AnimationController;或者,继承 AnimatedWidgetBaseState:
abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends ImplicitlyAnimatedWidgetState<T> {
@override
void initState() {
super.initState();
// 注意此处监听了 AnimationController 的变化并自动触发 setState
controller.addListener(_handleAnimationChanged);
}
void _handleAnimationChanged() {
setState(() { /* The animation ticked. Rebuild with new animation value */ });
}
}
ImplicitlyAnimatedWidget 的子类主要实现 AnimatedWidgetBaseState/ImplicitlyAnimatedWidgetState 的forEachTween()
和build()
方法即可。前者用于生成 Widget 所需的 Tween;后者则使用生成的 Tween<T>的evaluate(animation)
方法计算对应的属性并展示。
以 AnimatedPadding 为例,它继承自 ImplicitlyAnimatedWidget,创建的_AnimatedPaddingState 继承自 AnimatedWidgetBaseState<AnimatedPadding>:
class _AnimatedPaddingState extends AnimatedWidgetBaseState<AnimatedPadding> {
EdgeInsetsGeometryTween? _padding;
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
// 创建所需要的 Tween
_padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween?;
}
// 当 AnimationController 监听的 Ticker 时执行 setState 触发 rebuilt
@override
Widget build(BuildContext context) {
return Padding(
// 使用 CurveAnimation 计算当前对应的 padding 值
padding: _padding!
.evaluate(animation)
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity),
child: widget.child,
);
}
// ...
}
上述分析是继承 ImplicitlyAnimatedWidget 实现隐式动画的常用流程,Flutter 中内置的 AnimatedFoo 动画都是类似实现。
TweenAnimationBuilder
ImplicitlyAnimatedWidget 的子类(以 AnimatedFoo 命名的一众子类)提供了常见的动画效果,但是如果有特殊的动画效果需要实现,除了直接继承 ImplicitlyAnimatedWidget 之外,还可以使用 TweenAnimationBuilder 并传入 Tween 来实现:
return TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: targetValue),
duration: const Duration(seconds: 1),
builder: (BuildContext context, double size, Widget? child) {
return IconButton( ... );
},
child: const Icon(Icons.aspect_ratio),
);
当 Widget 首次 build 的时候就会触发动画从 Tween.begin 过渡到 Tween.end;当再次提供一个有新 end 的 Tween 也可以随时触发新动画(新动画从动画当前值开始)。
需要注意:
- 传入到 TweenAnimationBuilder 中的 Tween 被其持有(可能修改),所以不应当再操作它;
- 当动画执行完毕会调用 TweenAnimationBuilder.onEnd 方法;
- 为了性能,应当将不需要每次更新的 subtree 传入到 TweenAnimationBuilder.child 中避免重绘。
和其他 ImplicitlyAnimatedWidget 的子类一样,TweenAnimationBuilder 的主要逻辑也在继承自 AnimatedWidgetBaseState 的_TweenAnimationBuilderState 中:
class _TweenAnimationBuilderState<T extends Object?> extends AnimatedWidgetBaseState<TweenAnimationBuilder<T>> {
Tween<T>? _currentTween;
@override
void initState() {
// 内部持有使用者传入的 Tween
_currentTween = widget.tween;
_currentTween!.begin ??= _currentTween!.end;
super.initState();
if (_currentTween!.begin != _currentTween!.end) {
// 如果 Tween 有效则开始动画
controller.forward();
}
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
assert(
widget.tween.end != null,
'Tween provided to TweenAnimationBuilder must have non-null Tween.end value.',
);
_currentTween = visitor(_currentTween, widget.tween.end, (dynamic value) {
assert(false);
throw StateError('Constructor will never be called because null is never provided as current tween.');
}) as Tween<T>?;
}
@override
Widget build(BuildContext context) {
// 使用者实现 builder 创建对应的 Widget
return widget.builder(context, _currentTween!.evaluate(animation), widget.child);
}
}
使用时需要注意,只有传入 TweenAnimationBuilder 的 Tween 是一个新的、并且 end 值和之前不一样的才会触发动画。如果 end 值一样则无动画、如果不是新的则 builder 的内容只会突然变化为 end 值对应状态而无动画。
AnimatedWidget
之前分析的 TweenAnimationBuilder 以及 ImplicitlyAnimatedWidget 的其他子类,基本上都只能定义动画的 Tween、Duration、Curve 等,动画的开始结束动都由这些 Widget 内部控制。
如果需要手动主动控制动画,可以选择使用 显式动画 —— AnimatedWidget 及其子类:
其同样也分为 2 大类:
- AnimatedBuilder, which is useful for complex animation use cases and a notable exception to the naming scheme of AnimatedWidget subclasses.
- FooTransition 子类
- AlignTransition, which is an animated version of Align.
- DecoratedBoxTransition, which is an animated version of DecoratedBox.
- DefaultTextStyleTransition, which is an animated version of DefaultTextStyle.
- PositionedTransition, which is an animated version of Positioned.
- RelativePositionedTransition, which is an animated version of Positioned.
- RotationTransition, which animates the rotation of a widget.
- ScaleTransition, which animates the scale of a widget.
- SizeTransition, which animates its own size.
- SlideTransition, which animates the position of a widget relative to its normal position.
- FadeTransition, which is an animated version of Opacity.
- AnimatedModalBarrier, which is an animated version of ModalBarrier.
AnimatedWidget 比 ImplicitlyAnimatedWidget 简单许多,其接受一个 Listenable 对象,在_AnimatedState 中监听其并触发 rebuilt。
abstract class AnimatedWidget extends StatefulWidget {
const AnimatedWidget({
Key? key,
required this.listenable,
}) : assert(listenable != null),
super(key: key);
// Commonly an [Animation] or a [ChangeNotifier].
final Listenable listenable;
// 提供了 build 方法供子类根据不同的状态创建 Widget
@protected
Widget build(BuildContext context);
}
AnimatedWidget.listenable
通常是 AnimationController,当然也可以是其他实现 Listenable 的类(including ChangeNotifier and ValueNotifier)。
_AnimatedState
AnimatedWidget 的主要逻辑在对应的_AnimatedState 中:
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
// 监听 listenable,调用 setState 从而触发 rebuilt
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// 如果 listenable 改变了则重写添加 listener
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
// 注意这里使用了子类实现的 Widget.build 方法创建 Widget
@override
Widget build(BuildContext context) => widget.build(context);
}
可以看到,相对于隐式动画 ImplicitlyAnimatedWidget,显示动画 AnimatedWidget 的逻辑要简单的多,只是监听传入的 Listenable 并触发 rebuilt 即可。对于动画的控制则由 Listenable(通常是 AnimationController)处理。
也就是说,显示动画 AnimatedWidget 只是替子类做了监听/移除监听 Listenable 的值变化,并触发 rebuilt 的工作,如何获取变化的值,以及展示对应的 Widget 则需要子类自己处理。
他的子类 FooTransition 实现逻辑也比较简单,只需要在在 Widget.build 根据不同的状态创建创建不同属性的 Widget 即可:
class RotationTransition extends AnimatedWidget {
const RotationTransition({
Key? key,
required Animation<double> turns,
this.alignment = Alignment.center,
this.filterQuality,
this.child,
}) : assert(turns != null),
// AnimatedWidget 会监听 turns 的值变化,自动触发 rebuilt,
// 从而调用此 build() 方法更新 Widget
super(key: key, listenable: turns);
Animation<double> get turns => listenable as Animation<double>;
final FilterQuality? filterQuality;
final Alignment alignment;
final Widget? child;
@override
Widget build(BuildContext context) {
return Transform.rotate(
// 这里根据 turns 的值计算当前的角度
angle: turns.value * math.pi * 2.0,
alignment: alignment,
filterQuality: filterQuality,
child: child,
);
}
}
AnimatedBuilder
一般来说,Flutter 内置的以 FooTransition 命名的 AnimatedWidget 的子类可以满足基本的需求,但是如果想要实现更复杂的效果,除了直接继承 AnimatedWidget 之外,还可以使用 AnimatedBuilder 实现丰富的动画:
AnimatedBuilder(
animation: _controller,
child: Container( ... ),
builder: (BuildContext context, Widget? child) {
return Transform.rotate(
angle: _controller.value * 2.0 * math.pi,
child: child,
);
},
)
而 AnimatedBuilder 的实现也比较简单:
class AnimatedBuilder extends AnimatedWidget {
/// Creates an animated builder.
///
/// The [animation] and [builder] arguments must not be null.
const AnimatedBuilder({
Key? key,
required Listenable animation,
required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
/// Called every time the animation changes value.
final TransitionBuilder builder;
/// The child widget to pass to the [builder].
///
/// 可选,如果 AnimatedBuilder 要创建的一部分内容和动画无关,为了优化性能,
/// 可以将其传给 child,并在 builder 中直接复用
final Widget? child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
总结
ImplicitlyAnimatedWidget 隐式动画,内部创建并监听 AnimationController 以维护动画,控制动画的开始和结束,用户可以通过传入 Duration、Curve、Tween 等决定动画的时长、曲线、开始和结束值等,当动画相关的属性变化时,隐式动画会自动播放,使用者不可以直接控制动画。
Flutter 内置的隐式动画为 TweenAnimationBuilder 和 AnimatedFoo。
AnimatedWidget 显式动画,接受 Listenable(通常是 AnimationController)并监听其值变化,以触发 Widget 重新 build,其子类中一般会监听 Listenable 的值并计算设置 Widget 对应的属性。使用者需要负责创建、控制 Listenable 从而控制动画播放。Flutter 内置的显式动画为 AnimatedBuilder 和 FooTransition。
在使用 Flutter 实现Widget 动画时,可以按照以下顺序选择实现方式:
- AnimatedFoo,选择内置的隐式动画,以实现当 Padding、Alignment 等属性变化时自动渐变到新值的动画效果。
- TweenAnimationBuilder/继承 ImplicitlyAnimatedWidget,当上一步无法满足需求时,可以考虑进一步自定义实现隐式动画。
- FooTransition,如果不止要展示动画,还希望能够控制动画开始、结束,就使用内置的显式动画结合自己创建的 AnimationController 实现动画。
- AnimationBuilder/AnimatedWidget,如果没有满足条件的内置显式动画,可以使用自定义实现显式动画。
- CustomPainter,如果上述方法仍然无法满足动画需求,可以考虑使用 CustomPainter 自己绘制动画。