Flutter动画 3 - Animation动画组


简述


在前面的两篇博客中,我们了解了在Flutter中动画如何简单的使用动画,了解Tween相关使用方法.但是在很多场景下,我们使用的并不是单单一种动画,而是多种动画一起执行或者顺序执行,那么在应对这样的场景我们该怎么办呢? 今天,我们就聊一聊如何在Flutter中实现这种并行动画或者串行动画呢?接下来我们就一起看看这两种形式的动画是如何实现的.


并行动画


对于并行动画这种多个动画同时执行,我们需要让各个动画Animation的动画控制器AnimationController保持一致就可以了. 这个在上一篇Tween已经进行了对应说明演示.这里就直接把代码拿来了.

首先创建动画并保持动画控制器的统一性.示例如下代码所示.

    _animationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this);
    _animation = Tween<double>(begin: 0, end: 50).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });
    _colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });

然后再构建build方法中直接使用_animation和_colorAnimation的动画值就好,具体代码如下所示.

  Container(
    width: 200,
    height: 50,
    color: _colorAnimation.value,
    margin: EdgeInsets.only(top: _animation.value),
  ),


串行动画


相对于并行动画而言,串行动画写起来就比较复杂的多,串行动画的实现方案总共有三种,分别是 监听状态法, Interval时间间隔法, TweenSequence动画序列法.下面我们就分别来看看三种方法的实现以及区别.


监听状态法

状态监听法主要通过AnimationController监听动画的completed状态,然后再去执行下一个动画,如此往复,直到所有动画完成.

例如现在我们需要实现先执行组件在0.3秒钟往下偏移50个单位,然后再执行在0.6s中组件的颜色由 橘色 变为 红色.

首先,我们先声明位移动画控制器和颜色动画控制器以及位移动画和颜色动画,代码如下所示.

  AnimationController _animationController;
  Animation<double> _animation;
  AnimationController _colorAnimationController;
  Animation<Color> _colorAnimation;

然后,我们创建位移、颜色的动画控制器和动画,具体代码如下所示.

    _animationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this);
    _animation = Tween<double>(begin: 0, end: 50).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });
    _colorAnimationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
    _colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(_colorAnimationController)
      ..addListener(() {
        setState(() {});
      });

最后,我们只需要监听位移动画完成状态之后执行颜色动画即可,具体代码如下所示.

    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _colorAnimationController.forward();
      };
    });

整体Demo代码如下所示.

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation<double> _animation;
  AnimationController _colorAnimationController;
  Animation<Color> _colorAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(duration: Duration(milliseconds: 300), vsync: this);
    _animation = Tween<double>(begin: 0, end: 50).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });
    _colorAnimationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
    _colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(_colorAnimationController)
      ..addListener(() {
        setState(() {});
      });
    _animationController.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _colorAnimationController.forward();
      };
    });
  }

  void startEasyAnimation() {
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 200,
              height: 50,
              color: _colorAnimation.value,
              margin: EdgeInsets.only(top: _animation.value),
            ),
            FlatButton(
              onPressed: startEasyAnimation,
              child: Text(
                "点击执行最简单动画",
                style: TextStyle(color: Colors.black38),
              ),
            ),
          ],
        ),
      ),
    );
  }
}


Interval时间间隔法

上面的状态监听需要一个动画过程就写一个Controller,而且基本上还要每一个Controller都监听执行完成然后再去启动下一个Controller.如果一个动画过程有十几个,自己想想都是脑瓜子嗡嗡的.所以接下来我们就来介绍第二种方案 - Interval时间间隔法 .

Interval时间间隔法 的整体思路是一个动画Controller控制所有动画的执行.然后每一个动画只需要确认自己在整个动画的时间比重即可.

首先,声明一个动画Controller和多个动画.

  AnimationController _animationController;
  Animation<double> _animation;
  Animation<Color> _colorAnimation;

然后初始化AnimationController,AnimationController的动画时间(duration)要设置成所有动画的总时长,例如这里我设定为600毫秒(_animation时长:300毫秒,_colorAnimation时长:300毫秒).

    _animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);

接下来就是初始化两个Animation,Tween对象调用animate()函数不再是直接传入上面的AnimationController,而是传入一个 CurvedAnimation 对象. CurvedAnimation构建过程中需要传入两个参数一个是 parent ,用于指定AnimationController. 另外一个是 curve,用于指定动画曲线函数.我们可以使用常用的动画曲线函数,也可以自己生成,这里我们就自己生成.指定动画执行的时间区间.

  // CurvedAnimation的构建方法
  CurvedAnimation({
    required this.parent,
    required this.curve,
    this.reverseCurve,
  }) : assert(parent != null),
       assert(curve != null) {
    _updateCurveDirection(parent.status);
    parent.addStatusListener(_updateCurveDirection);
  }

由于两个动画时间长度是对分的,每一个都是300毫秒,所以 curve 参数中的值就分别是 Interval(0.0, 0.5)Interval(0.5, 1.0),两个Animation的初始化过程如下所示.

  _animation = Tween<double>(begin: 0, end: 50).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.0, 0.5),
    ),
  )..addListener(() {
      setState(() {});
  });

  _colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(
    CurvedAnimation(
      parent: _animationController,
      curve: Interval(0.5, 1.0),
    ),
  )..addListener(() {
      setState(() {});
  });

整体Demo代码如下所示.

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation<double> _animation;
  Animation<Color> _colorAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
    _animation = Tween<double>(begin: 0, end: 50).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.0, 0.5),
      ),
    )..addListener(() {
        setState(() {});
      });
    _colorAnimation = ColorTween(begin: Colors.orangeAccent, end: Colors.redAccent).animate(
      CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.5, 1.0),
      ),
    )..addListener(() {
        setState(() {});
      });
  }

  void startEasyAnimation() {
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 200,
              height: 50,
              color: _colorAnimation.value,
              margin: EdgeInsets.only(top: _animation.value),
            ),
            FlatButton(
              onPressed: startEasyAnimation,
              child: Text(
                "点击执行最简单动画",
                style: TextStyle(color: Colors.black38),
              ),
            ),
          ],
        ),
      ),
    );
  }
}


TweenSequence动画序列法

上面的两种方案虽然能解决动画组的问题,但是都太过于繁琐,那么有没有一种比较优雅的方案呢?这就需要使用到 TweenSequenceTweenSequenceItem 这两个类了. 其中 TweenSequence 是动画组类,TweenSequenceItem 则是用来定义每一个动画的具体实现的类.但是TweenSequenceTweenSequenceItem也不是尽善尽美的,它最大的问题就是前后变化的属性值类型必须是一致的.

下面,我们仍然以改变Margin为例, 先让视图组件往上移动50,再让视图 来看看如何使用 TweenSequenceTweenSequenceItem 来实现这个动画.

首先,我们声明一个 动画控制器AnimationController 和 动画Animation.

  AnimationController _animationController;
  Animation<double> _animation;

仍然以两者的动画总时长为600毫秒,所以我们需要实现AnimationController,如下所示.

_animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);

然后,我们通过 TweenSequenceTweenSequenceItem 这两个类对 动画Animation 进行实现.

实现两个 TweenSequenceItem, TweenSequenceItem中的 <font color=red>weight</font> 属性是来设定动画执行的时间权重.也就是在整个动画过程,当前动画执行时长占总时长的比例.例如下面 第一个动画插值占的时间比例为 50/(50 + 100). 第二个动画插值占的时间比例为 100/(50 + 100) .


    TweenSequenceItem downMarginItem = TweenSequenceItem<double>(
      tween: Tween(begin: 1.0, end: 50.0),
      weight: 50,
    );
    TweenSequenceItem upMarginItem = TweenSequenceItem<double>(
      tween: Tween(begin: 50.0, end: 100.0),
      weight: 100,
    );

然后创建一个动画插值组,把上面两个动画插值放入组中.

    TweenSequence tweenSequence = TweenSequence<double>([
      downMarginItem,
      upMarginItem,
    ]);

最后,生成动画就OK了.

    _animation = tweenSequence.animate(_animationController);
    _animation.addListener(() {
      setState(() {});
    });

当然了,上面的三步可以缩写成一步代码来实现,我只是进行了分解代码来说明每一个代码.

      // 缩写代码
    _animation = TweenSequence<double>([
      TweenSequenceItem<double>(
        tween: Tween(begin: 0.0, end: 50.0),
        weight: 50,
      ),
      TweenSequenceItem<double>(
        tween: Tween(begin: 50.0, end: 100.0),
        weight: 100,
      ),
    ]).animate(_animationController)
      ..addListener(() {
        setState(() {});
      });

整体代码如下所示.

class _FlutterAnimationWidgetState extends State<FlutterAnimationWidget> with TickerProviderStateMixin {
  AnimationController _animationController;
  Animation<double> _animation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(duration: Duration(milliseconds: 600), vsync: this);
    TweenSequenceItem downMarginItem = TweenSequenceItem<double>(
      tween: Tween(begin: 1.0, end: 50.0),
      weight: 50,
    );
    TweenSequenceItem upMarginItem = TweenSequenceItem<double>(
      tween: Tween(begin: 50.0, end: 100.0),
      weight: 100,
    );
    TweenSequence tweenSequence = TweenSequence<double>([
      downMarginItem,
      upMarginItem,
    ]);
    _animation = tweenSequence.animate(_animationController);
    _animation.addListener(() {
      setState(() {});
    });
  }

  void startEasyAnimation() {
    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Container(
              width: 200,
              height: 50,
              color: Colors.orangeAccent,
              margin: EdgeInsets.only(top: _animation.value),
            ),
            FlatButton(
              onPressed: startEasyAnimation,
              child: Text(
                "点击执行最简单动画",
                style: TextStyle(color: Colors.black38),
              ),
            ),
          ],
        ),
      ),
    );
  }
}


动画组实现总结

上面三种实现动画组基本上已经说完了,接下来我们就来对比其不同点.

特性 监听状态法 Interval时间间隔法 TweenSequence动画序列法
代码简洁度 🔅🔅 🔅🔅🔅 🔅🔅🔅🔅🔅
动画是否可交织
动画属性是否可以多变

动画是否可交织 : 动画可否交织主要是说两个动画之间是否需要上一个动画完全执行完成之后,下一个动画才能执行.

动画属性是否可以多变 : 动画属性多变是指当前动画过程中可变化的属性是否可以有多个,例如同时变化尺寸和颜色等等.


结语


OK,如何使用Flutter实现串行动画和并行动画就说道这里,下一篇就说一下在转场动画比较常见的飞入飞出动画 - Hero动画,欢迎持续关注骚栋,有任何问题欢迎联系骚栋.


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