Flutter动画 3 - Animation动画组

1,872 阅读7分钟


简述


在前面的两篇博客中,我们了解了在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中的 weight 属性是来设定动画执行的时间权重.也就是在整个动画过程,当前动画执行时长占总时长的比例.例如下面 第一个动画插值占的时间比例为 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动画,欢迎持续关注骚栋,有任何问题欢迎联系骚栋.