美文网首页
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