Flutter动画简化类

  • 概述

    在Flutter的使用过程中,我们总是会调用addListener方法添加每一帧的回调监听,并且动画都是体现在UI变化上的,所以在监听中我们通常要调用setState方法重新刷新UI,从而使得控件对应属性重新引用animation.value的值,这块逻辑通常都是必须的,因为你基本上不可能应用动画但是不让他刷新UI,这样动画就失去了意义。

    另外,动画的构建过程也是大同小异,比如,我们都至少需要一个AnimationController,都可能需要Tween和Curve,都需要duration等。

    Flutter框架中有这样的类帮我们封装了这部分逻辑,我们来看一下它们的源码。

  • ImplicitlyAnimatedWidget

    凡是需要封装刷新逻辑的,通常需要继承这个类,为什么要继承呢?我这里用的是“通常”,而不是必须,其实如果要追究的话,所有的东西其实你都可以自己封装,之所以继承这个类是因为方便,不要“重复造轮子”,我们只是需要知其所以然。

    先看一下一些经常使用的子类:

    [TweenAnimationBuilder], which animates any property expressed by
    a [Tween] to a specified target value.
    [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.
    

    这个类有一些基本的属性:

    const ImplicitlyAnimatedWidget({
      Key? key,
      this.curve = Curves.linear,
      required this.duration,
      this.onEnd,
    })
    

    最重要的还是它的createState方法限制了State类型必须是ImplicitlyAnimatedWidgetState:

    @override
    ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState();
    

    作为规范,ImplicitlyAnimatedWidgetState要求Widget必须是ImplicitlyAnimatedWidget类型。

    我们来看ImplicitlyAnimatedWidgetState中封装了那些内容。

  • ImplicitlyAnimatedWidgetState

    @protected
    AnimationController get controller => _controller;
    late final AnimationController _controller = AnimationController(
      duration: widget.duration,
      debugLabel: kDebugMode ? widget.toStringShort() : null,
      vsync: this,
    );
    
    /// The animation driving this widget's implicit animations.
    Animation<double> get animation => _animation;
    late Animation<double> _animation = _createCurve();
    
    CurvedAnimation _createCurve() {
      return CurvedAnimation(parent: _controller, curve: widget.curve);
    }
    

    首先我们看到,它内部持有一个AnimationController还有一个CurvedAnimation,可能有人会问,那这样岂不是强制把AnimationController绑定在CurvedAnimation上了吗,如果我不需要Curve,我想要直接使用AnimationController或者把绑定在Tween上呢?其实很简单就能说通,即时你不使用Curve,系统的屏幕刷新频率是固定的,在你不使用任何包装类的情况下它默认速度其实就是线性变化的,这和你指定Curve.linear是完全一样的效果,而通过继承ImplicitlyAnimatedWidget,它默认指定的curve属性就是Curve.linear,这也是ImplicitlyAnimatedWidget的另一个作用,就是初始化一些必须的值。

    其次ImplicitlyAnimatedWidgetState依赖了SingleTickerProviderStateMixin,所以我们自己的State也省去了vsync这一步。

    接下来我们看initState方法:

    @override
    void initState() {
      super.initState();
      _controller.addStatusListener((AnimationStatus status) {
        switch (status) {
          case AnimationStatus.completed:
            widget.onEnd?.call();
            break;
          case AnimationStatus.dismissed:
          case AnimationStatus.forward:
          case AnimationStatus.reverse:
        }
      });
      _constructTweens();
      didUpdateTweens();
    }
    

    可以看到,initState中添加了一个StatusListener,用来处理动画结束时的回调,ImplicitlyAnimatedWidget构造中只提供了动画结束时的回调接口设置入口,所以默认只能监听动画结束,但是ImplicitlyAnimatedWidgetState提供了controller方法来获取_controller,所以有需要的话你完全可以设置其他的。

    接着会调用_constructTweens方法:

    bool _constructTweens() {
      bool shouldStartAnimation = false;
      forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
        if (targetValue != null) {
          tween ??= constructor(targetValue);
          if (_shouldAnimateTween(tween, targetValue))
            shouldStartAnimation = true;
        } else {
          tween = null;
        }
        return tween;
      });
      return shouldStartAnimation;
    }
    

    这个方法稍微有些深度,我们来看forEachTween方法:

    @protected
    void forEachTween(TweenVisitor<dynamic> visitor);
    

    这个方法在哪实现的呢?我们上面说到,ImplicitlyAnimatedWidgetState有很多子类,我们以_AnimatedPaddingState为例看看它的forEachTween方法:

    @override
    void forEachTween(TweenVisitor<dynamic> visitor) {
      _padding = visitor(_padding, widget.padding, (dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween?;
    }
    

    这个_padding是 _AnimatedPaddingState要用来应用到Widget的变化属性值,显然它是根据visitor函数生成的,而这里的visitor我们知道是在调用 _constructTweens方法中调用forEachTween时传入的,回到 _constructTweens方法,tween就是这里的 _padding,targetValue就是这里的widget.padding,constructor就是(dynamic value) => EdgeInsetsGeometryTween(begin: value as EdgeInsetsGeometry)) as EdgeInsetsGeometryTween?。所以我们可以整理一下逻辑:外面State要变化的属性是需要自定义的,因此它的Tween的构造方法也是需要自定义的,目标属性值是它的widget指定的,但是创建逻辑是由ImplicitlyAnimatedWidgetState封装好的。由此我们也可以知道,forEachTween方法是用来创建变化属性的Tween的。

    didUpdateTweens方法是用来更新Tween的,没有默认实现,只是提供了一个可以在构造完Tween 之后做一些事情的接口,你可以按照需求实现自己的逻辑。

    接下来看一下didUpdateWidget方法:

    @override
    void didUpdateWidget(T oldWidget) {
      super.didUpdateWidget(oldWidget);
      if (widget.curve != oldWidget.curve) {
        (_animation as CurvedAnimation).dispose();
        _animation = _createCurve();
      }
      _controller.duration = widget.duration;
      if (_constructTweens()) {
        forEachTween((Tween<dynamic>? tween, dynamic targetValue, TweenConstructor<dynamic> constructor) {
          _updateTween(tween, targetValue);
          return tween;
        });
        _controller
          ..value = 0.0
          ..forward();
        didUpdateTweens();
      }
    }
    

    这个方法只有在widget.canUpdate方法返回true的时候(也即是runtimeType或Widget.key有变化的时候)才会被系统调用,所以在这个方法里做一些对动画配置可能发生变化的更新操作。

    前面的就不说了,说一下 _constructTweens的返回值,前面可以看到 _constructTweens的返回值是shouldStartAnimation,它是否能返回true是根据 _shouldAnimateTween方法决定的:

    bool _shouldAnimateTween(Tween<dynamic> tween, dynamic targetValue) {
      return targetValue != (tween.end ?? tween.begin);
    }
    

    可见,只要当前动画进度值不是在两个端点就会返回true。

    再看 _updateTween方法:

    void _updateTween(Tween<dynamic>? tween, dynamic targetValue) {
      if (tween == null)
        return;
      tween
        ..begin = tween.evaluate(_animation)
        ..end = targetValue;
    }
    

    这个方法会使Widget配置发生变化后还能从之前动画执行的当前进度值继续变化。而这里也会调用 _controller的forward方法使动画继续。

    所以,结合_shouldAnimateTween方法,整体就是动画当前进度值正在进行中的时候才会调用forward使动画继续,如果在起点或者终点的话是不会自动开启的,同样在端点的时候Tween也不需要根据动画的进度值去设置起点属性值,这也能看出框架设计者的细节之处。

    当然,ImplicitlyAnimatedWidgetState也封装了动画的释放工作:

    @override
    void dispose() {
      (_animation as CurvedAnimation).dispose();
      _controller.dispose();
      super.dispose();
    }
    
  • AnimatedWidgetBaseState

    现在还有一个点没有提到,就是刷新UI的逻辑,它就在ImplicitlyAnimatedWidgetState的另一个子类AnimatedWidgetBaseState中。

    abstract class AnimatedWidgetBaseState<T extends ImplicitlyAnimatedWidget> extends ImplicitlyAnimatedWidgetState<T> {
      @override
      void initState() {
        super.initState();
        controller.addListener(_handleAnimationChanged);
      }
    
      void _handleAnimationChanged() {
        setState(() { /* The animation ticked. Rebuild with new animation value */ });
      }
    }
    

    这个类非常简单,不言而喻,不再赘述。

  • AnimatedWidget

    有人说我想要完全自定义的AnimationController和Tween,只是Listener调用State部分封装就好了,那么有没有这样的封装类呢?答案是有的,他就是AnimatedWidget。

    AnimatedWidget的State是_AnimatedState:

    @override
    void initState() {
      super.initState();
      widget.listenable.addListener(_handleChange);
    }
    
    @override
    void didUpdateWidget(AnimatedWidget oldWidget) {
      super.didUpdateWidget(oldWidget);
      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.
      });
    }
    

    可以看到只是把刷新UI回调这一步给封装了。

  • AnimatedBuilder

    在上面的例子中,调用setState方法都会引起整个组件树的构建,出于性能考虑,有了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;
    
      final Widget? child;
    
      @override
      Widget build(BuildContext context) {
        return builder(context, child);
      }
    }
    

    可以看到,AnimatedBuilder是继承自AnimatedWidget的,所以它也可以自动刷新,只不过多了个builder函数参数,build方法会返回它的返回值,这就把构建范围缩小到了应用动画的组件范围,调用setState时就不会调用更上层组件的build方法了,极大的节省了渲染效率。

  • 总结

    经过源码的阅读,我们知道了Flutter是怎样通过封装简化我们的动画使用步骤的,继承自ImplicitlyAnimatedWidget保证了我们的State必须是ImplicitlyAnimatedWidgetState类型的,为什么不是AnimatedWidgetBaseState呢?我想是为了灵活性,因为ImplicitlyAnimatedWidgetState里封装的逻辑都是必须要有的通用逻辑,而刷新UI可能不需要(虽然我想不出应用场景...),当然我们如果想要应用动画通常继承AnimatedWidgetBaseState就好。另外,ImplicitlyAnimatedWidget也会保证ImplicitlyAnimatedWidgetState中一些必须初始化的属性一定有值,比如curve。

    总之,ImplicitlyAnimatedWidgetState中涵盖了AniamtionController、CurveAnimation、Tween所有的可能需要的动画配置,我们只需要传递对应的参数就好;而AnimatedWidgetBaseState中封装了动画执行过程中刷新UI的逻辑。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容

  • 概述 动画API认识 动画案例练习 其它动画补充 一、动画API认识 动画实际上是我们通过某些方式(某种对象,An...
    IIronMan阅读 360评论 1 3
  • 对于一个前端的App来说,添加适当的动画,可以给用户更好的体验和视觉效果。所以无论是原生的iOS或Android,...
    5e4c664cb3ba阅读 1,510评论 0 7
  • 该文已授权公众号 「码个蛋」,转载请指明出处 在 Flutter 中,自带手势监听的目前为止好像只有按钮部件和一些...
    Kuky_xs阅读 1,701评论 2 3
  • 对动画系统而言,为了实现动画,它需要做三件事儿:1.确定画面变化的规律;2.根据这个规律,设定动画周期,启动动画;...
    Cat_uncle阅读 1,048评论 0 0
  • 本文翻译自原文地址:Animation deep dive[https://medium.com/flutter/...
    whqfor阅读 974评论 0 1