跳至主要內容

Flutter 动画分析之 AnimatedWidget&ImplicitlyAnimatedWidget

JI,XIAOYONG...大约 12 分钟

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

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

简单来说,Flutter 有一些内置的动画,在要写动画的时候,可以依次考虑(实现程度由易到难):


本文将对 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;

  
  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 中,根据传入的 DurationCurve,创建并持有了 AnimationControllerAnimation<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> {
  
  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;

  
  void forEachTween(TweenVisitor<dynamic> visitor) {
    // 创建所需要的 Tween
    _padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween?;
  }

  // 当 AnimationController 监听的 Ticker 时执行 setState 触发 rebuilt
  
  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;

  
  void initState() {
    // 内部持有使用者传入的 Tween
    _currentTween = widget.tween;
    _currentTween!.begin ??= _currentTween!.end;
    super.initState();
    if (_currentTween!.begin != _currentTween!.end) {
      // 如果 Tween 有效则开始动画
      controller.forward();
    }
  }

  
  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>?;
  }

  
  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
  
  Widget build(BuildContext context);
}

AnimatedWidget.listenable通常是 AnimationController,当然也可以是其他实现 Listenable 的类(including ChangeNotifier and ValueNotifier)。

_AnimatedState

AnimatedWidget 的主要逻辑在对应的_AnimatedState 中:

class _AnimatedState extends State<AnimatedWidget> {
  
  void initState() {
    super.initState();
    // 监听 listenable,调用 setState 从而触发 rebuilt
    widget.listenable.addListener(_handleChange);
  }

  
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 如果 listenable 改变了则重写添加 listener
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  
  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
  
  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;

  
  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;

  
  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.cnopen in new window

ImplicitlyAnimatedWidgetopen in new window

AnimatedWidgetopen in new window

文章标题:《Flutter 动画分析之 AnimatedWidget&ImplicitlyAnimatedWidget》
本文作者: JI,XIAOYONG
发布时间: 2022/08/22 08:58:45 UTC+8
更新时间: 2023/12/30 16:17:02 UTC+8
written by human, not by AI
本文地址: https://jixiaoyong.github.io/blog/posts/47c43abe.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。转载请注明出处!
你认为这篇文章怎么样?
  • 0
  • 0
  • 0
  • 0
  • 0
  • 0
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8