美文网首页
Flutter 动画详解系列——显式动画

Flutter 动画详解系列——显式动画

作者: FY_Chao | 来源:发表于2020-10-14 14:35 被阅读0次

    Flutter 动画详解系列——隐式动画中我们介绍了Flutter中的隐式动画,隐式动画创建简单、灵活性差,而显式动画相对来说更加灵活。接下来让我们来看看 Flutter 中的显式动画。

    Flutter内置显示动画FooTransition(Foo代表动画需要改变的属性值)

    先来看一张图,下面列出了部分Flutter中内置的显式动画Widget:


    flutter_transition.png

    我们可以通过直接使用上述的Widget,来创建显示动画,让我们以RotationTransition为例来看看显式动画如何创建。

    RotationTransition Demo

    // RotationTransition 构造器
    RotationTransition({
      Widget child,
      Alignment alignment,
      Animation<double> turns,
    })
    

    child 指定哪个widget用来做旋转动画,alignment是坐标系原点的对齐方式,我们想要有银河星系的旋转效果使用center作为坐标系原点。turns:在API文档中定义是Animation抽象类型,turns是显示动画有别于隐式动画的的重要参数。我们使用隐式动画Widget AnimatedContainer 更改transform也能达到图片旋转的效果,但是隐式动画只会执行一次便会停止。借助于显式动画,我们不仅可以控制动画时间长,并可以让动画一直重复执行。

    创建AnimationController

    AnimationController 顾名思义,是用来控制动画,AnimationController 继承自抽象类Animation<double>,AnimationController 可以监听动画的执行、还能提供一些动画的控制操作。

    class _TimeMachineState extends State<TimeMachine> {
      AnimationController _animationController;
     
      @override
      void initState() {
        super.initState();
        
        _animationController = AnimationController(
          // ...
        );
      }
      
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    }
    

    AnimationController 需要传递两个必要参数,duration 即我们动画的执行时间,默认 AnimationController 在给定的时间段内会线性的生成从0.0到1.0的数字。而具体值的数量和粒度取决于之前的duration参数。设置 vsync 时用来防止屏幕外动画(动画的UI不在当前屏幕时)消耗不必要的资源。 通过将SingleTickerProviderStateMixin添加到类定义中,可以将stateful对象作为vsync的值。

    class _TimeMachineState extends State<TimeMachine> 
    with SingleTickerProviderStateMixin {
      AnimationController _animationController;
     
      @override
      void initState() {
        super.initState();
        
        _animationController = AnimationController(
          duration: Duration(seconds: 15),
          vsync: this,
        );
      }
      
      @override
      void dispose() {
        _animationController.dispose();
        super.dispose();
      }
    }
    

    当我们创建完AnimationController后,不要忘记开启动画:

    _animationController = AnimationController(
      duration: Duration(seconds: 15),
      vsync: this,
    )..repeat();
    

    最后,我们回到上面 RotationTransition 银河旋转动画效果的代码部分,补充完turns参数。

    RotationTransition(
      child: GalaxyFitz(),
      alignment: Alignment.center,
      turns: _animationController,
    )
    

    同时,我们也可以通过_animationController来对动画进行控制,如下我们添加了一个100的正方形手势,来暂停、开启动画效果。

    class TimeStopper extends StatelessWidget {
      final AnimationController controller;
     
      const TimeStopper({Key key, this.controller}) : super(key: key);
     
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTap: () {
            if (controller.isAnimating) {
              controller.stop();
            } else {
              controller.repeat();
            }
          },
          child: Container(color: Colors.red,width: 100,height: 100,),
        );
      }
    }
    

    小结

    相对于隐式动画,显式动画需要一个动画的控制器,我们知道了在Flutter中存在内置的显示动画效果FooTransition。我们可以通过设置 duration 、和 AnimationController 来创建一个重复循环的动画效果,并且我们可以通过 AnimationController 来控制动画。那么当系统自带的显式动画Widget无法满足我们时,我们就需要创建自定义的显式动画啦。

    使用AnimatedBuilder和AnimatedWidget创建自定义显式动画

    为了更加具体的了解AnimatedBuilder和AnimatedWidget,我们编写一个外星飞船的动画demo,动画效果是飞船的传输光线。

    首先自定义一个Path剪切器,形成光束的形态。

    import 'package:flutter/material.dart';
    
    void main() => runApp(MyApp());
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            home: Scaffold(
          body: MyHomePage(),
        ));
      }
    }
    
    class MyHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: AlignmentDirectional.center,
          children: <Widget>[
            ClipPath(
              clipper: const BeamClipper(),
              child: Container(
                height: 1000,
                decoration: BoxDecoration(
                  gradient: RadialGradient(
                    radius: 1.5,
                    colors: [
                      Colors.yellow,
                      Colors.transparent,
                    ],
                  ),
                ),
              ),
            ),
          ],
        );
      }
    }
    
    class BeamClipper extends CustomClipper<Path> {
      const BeamClipper();
    
      @override
      getClip(Size size) {
        return Path()
          ..lineTo(size.width / 2, size.height / 2)
          ..lineTo(size.width, size.height)
          ..lineTo(0, size.height)
          ..lineTo(size.width / 2, size.height / 2)
          ..close();
      }
    
      /// Return false always because we always clip the same area.
      @override
      bool shouldReclip(CustomClipper oldClipper) => false;
    }
    
    截屏2020-10-14 下午2.08.37.png

    接下来就可通过AnimatedBuilder和 AnimatedWidget 来实现动画效果了。

    AnimatedBuilder

    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      final Image starsBackground = Image.asset(
        'assets/milky-way.jpg',
      );
      final Image ufo = Image.asset('assets/ufo.png');
      AnimationController _animation;
    
      @override
      void initState() {
        super.initState();
        _animation = AnimationController(
          duration: const Duration(seconds: 5),
          vsync: this,
        )..repeat();
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: AlignmentDirectional.center,
          children: <Widget>[
            starsBackground,
            AnimatedBuilder(
              animation: _animation,
              builder: (_, __) {
                return ClipPath(
                  clipper: const BeamClipper(),
                  child: Container(
                    height: 1000,
                    decoration: BoxDecoration(
                      gradient: RadialGradient(
                        radius: 1.5,
                        colors: [
                          Colors.yellow,
                          Colors.transparent,
                        ],
                        stops: [0, _animation.value],
                      ),
                    ),
                  ),
                );
              },
            ),
            ufo,
          ],
        );
      }
    
      @override
      void dispose() {
        _animation.dispose();
        super.dispose();
      }
    }
    
    class BeamClipper extends CustomClipper<Path> {
      const BeamClipper();
    
      @override
      getClip(Size size) {
        return Path()
          ..lineTo(size.width / 2, size.height / 2)
          ..lineTo(size.width, size.height)
          ..lineTo(0, size.height)
          ..lineTo(size.width / 2, size.height / 2)
          ..close();
      }
    
      /// Return false always because we always clip the same area.
      @override
      bool shouldReclip(CustomClipper oldClipper) => false;
    }
    

    跟之前Flutter 系统内置的显式动画一样,我们需要创建一个动画控制器、混入 SingleTickerProviderStateMixin 防止屏幕外动画。

    之前在介绍 TweenAnimationBuilder 文章中提到过,在设置 child 参数的时候,如果在动画过程中 child 不会变动,我们可以提前构建它,然后将其传递给AnimatedBuilder。在这个例子中,BeamClipper 可以提供一个常量的构造函数生成常量对象,用于优化动画的性能。

    AnimatedWidget

    使用 AnimatedBuilder 代码构建出来的动画效果,可能代码有点臃肿,难以阅读。此时我们可以尝试创建一个 AnimatedWidget 的子类,来完成同样的动画效果。与系统的内置显式动画效果命名一致,推荐FooTransition的命名原则。这里我们将其命名为BeamTransition

    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage>
        with SingleTickerProviderStateMixin {
      final Image starsBackground = Image.asset(
        'assets/milky-way.jpg',
      );
      final Image ufo = Image.asset('assets/ufo.png');
      AnimationController _animation;
    
      @override
      void initState() {
        super.initState();
        _animation = AnimationController(
          duration: const Duration(seconds: 5),
          vsync: this,
        )..repeat();
      }
    
      @override
      Widget build(BuildContext context) {
        return Stack(
          alignment: AlignmentDirectional.center,
          children: <Widget>[
            starsBackground,
            BeamTransition(animation: _animation),
            ufo,
          ],
        );
      }
    
      @override
      void dispose() {
        _animation.dispose();
        super.dispose();
      }
    }
    
    class BeamTransition extends AnimatedWidget {
      BeamTransition({Key key, Animation<double> animation})
          : super(key: key, listenable: animation);
      @override
      Widget build(BuildContext context) {
        final Animation<double> animation = listenable;
        return ClipPath(
          clipper: const BeamClipper(),
          child: Container(
            height: 1000,
            decoration: BoxDecoration(
              gradient: RadialGradient(
                radius: 1.5,
                colors: [
                  Colors.yellow,
                  Colors.transparent,
                ],
                stops: [0, animation.value],
              ),
            ),
          ),
        );
      }
    }
    

    我们将上面AnimatedBuilder中build的方法替换了下。将创建动画的代码抽取到了BeamTransition中。注意在BeamTransition中 child 中不变部分的我们使用常量值。

    总结

    到此,Flutter 中显式动画的介绍也告一段落了,显式动画能够循环的执行下去、并且通过动画控制器我们可以轻松的控制动画。在自定义显式动画时,比较推荐使用 AnimatedWidget 单独的抽取成一个独立的Widget。当然如果动画足够简单,使用 AnimatedBuilder 不会造成代码阅读的困难的话,也可以是用AnimatedBuilder。

    相关文章

      网友评论

          本文标题:Flutter 动画详解系列——显式动画

          本文链接:https://www.haomeiwen.com/subject/aazbpktx.html