Flutter 动画分析之 AnimatedWidget&ImplicitlyAnimatedWidget

本文讨论的 Flutter 动画主要限定在:随着每一帧的变化,修改 Flutter Widget 的大小、颜色、位置等属性,使之看起来从一种状态渐变为另外一种状态 这一范围。

在之前的文章中,我们将 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 中的动画

简单来说,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 动画时,可以按照以下顺序选择实现方式:

  1. AnimatedFoo,选择内置的隐式动画,以实现当 Padding、Alignment 等属性变化时自动渐变到新值的动画效果。
  2. TweenAnimationBuilder/继承 ImplicitlyAnimatedWidget,当上一步无法满足需求时,可以考虑进一步自定义实现隐式动画。
  3. FooTransition,如果不止要展示动画,还希望能够控制动画开始、结束,就使用内置的显式动画结合自己创建的 AnimationController 实现动画。
  4. AnimationBuilder/AnimatedWidget,如果没有满足条件的内置显式动画,可以使用自定义实现显式动画。
  5. CustomPainter,如果上述方法仍然无法满足动画需求,可以考虑使用 CustomPainter 自己绘制动画。

参考资料

动画效果介绍 flutter.cn

ImplicitlyAnimatedWidget

AnimatedWidget

有想法?欢迎通过邮件讨论。

目录