Flutter和Dart系列十二:动画(Animation)

一个App中如果能有优秀的动画效果,能让App看起来显得更加高大上。此篇我们就来介绍一下Flutter中Animation体系。

  1. 我们先来一个简单的例子,来实现透明度渐变动画:

    class FadeInDemo extends StatefulWidget {
      @override
      State createState() {
        return _FadeInDemoState();
      }
    }
    
    class _FadeInDemoState extends State {
      double opacity = 0.0;
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            MaterialButton(
              child: Text(
                "Click Me",
                style: TextStyle(color: Colors.blueAccent),
              ),
              onPressed: () => setState(() {
                opacity = 1;
              }),
            ),
            AnimatedOpacity(
                duration: const Duration(seconds: 2),
                opacity: opacity,
                child: Text('Flutter Animation Demo01')
                )
          ],
        );
      }
    }

    这里我们借助于AnimatedOpacity来实现渐变:我们通过点击按钮来触发动画效果,如下图所示。

    我们来总结一下实现步骤:

    • 使用AnimatedOpacity来包裹需要实现透明度渐变动画的Widget,并指定duration和opacity参数。这俩参数也好理解:duration自然是动画时间,opacity表示透明度(取值范围为0~1,0表示透明)
    • 触发动画:通过setState()方法,我们可以直接指定opacity的最终值(为1,即完全显示)。因为所谓的动画,肯定是有起始状态和结束状态,然后在指定的动画时间内慢慢发生变化。

    2.使用AnimatedContainer来实现其他属性变化的动画.

        class AnimatedContainerDemo extends StatefulWidget {
          @override
          State createState() {
            return _AnimatedContainerDemoState();
          }
        }
    
        class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> {
          Color color;
          double borderRadius;
          double margin;
    
          double randomBorderRadius() {
            return Random().nextDouble() * 64;
          }
    
          double randomMargin() {
            return Random().nextDouble() * 64;
          }
    
          Color randomColor() {
            return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF));
          }
    
          void change() {
            setState(() {
              initState();
            });
          }
    
          @override
          void initState() {
            color = randomColor();
            borderRadius = randomBorderRadius();
            margin = randomMargin();
          }
    
          @override
          Widget build(BuildContext context) {
            return Scaffold(
              body: Center(
                child: Column(
                  children: <Widget>[
                    SizedBox(
                      width: 128,
                      height: 128,
                      child: AnimatedContainer(
                        curve: Curves.easeInOutBack,
                        duration: const Duration(milliseconds: 400),
                        margin: EdgeInsets.all(margin),
                        decoration: BoxDecoration(
                          color: color,
                          borderRadius: BorderRadius.circular(borderRadius),
                        ),
                      ),
                    ),
                    MaterialButton(
                      color: Theme.of(context).primaryColor,
                      child: Text(
                        'change',
                        style: TextStyle(color: Colors.white),
                      ),
                      onPressed: () => change(),
                    )
                  ],
                ),
              ),
            );
          }
        }

    运行效果如下:

    我们这次同时修改了margin、color以及borderRadius三个属性。AnimatedContainer的使用思路和AnimatedOpacity类似:

    • 包裹子widget,指定duration及相关属性参数
    • 在setState方法中指定属性的动画终止状态

实际上我们刚刚介绍的两种实现方式被称之为隐式动画(implicit animation),可以理解成对于Animation子系统进行了一层封装,方便我们开发者使用。下面我们正式来介绍Animation子系统的重要组成类:

3.Animation类:通过这个类,我们可以知道当前的动画值(animation.value)以及当前的状态(通过设置对应的监听listener),但是对于屏幕上如何显示、widget如何渲染,它是不知道的,换句话说也不是它所关心的,这点从软件设计上耦合性也更低。其次,从代码角度上,它是一个抽象类:

4.AnimationController类。

从类本身上看,它是继承自Animation类的,并且泛型类型为double。从作用上来看,我们可以通过AnimationController来指定动画时长,以及它提供的forward()、reverse()方法来触发动画的执行。

5.Curved­Animation类:同样继承自Animation类,并且泛型类型为double。它主要用来描述非线性变化的动画,有点类似Android中的属性动画的插值器。

6.Tween类.

从类的层次结构上,它有所不同,不再是继承自Animation,而是继承自Animatable。它主要用来指定起始状态和终止状态的。

好,我们已经对这四个类有了一定的了解,下面我们就从实例来看看他们是如何结合在一起使用的。

7.实例一:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {

 // 注释1:这里已经出现了我们前面提到的Animation和AnimationController类
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    // 注释2:
    // 在构造一个AnimationController对象时,我们需要传递两个参数
    // vsync:主要为了防止一些超出屏幕之外的动画而导致的不必要的资源消耗。我们这里就传递
    //        this,除此之外,我们还需要使用with关键字来接入SingleTickerProviderStateMixin类型
    // duration:指定动画时长
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));

    // 注释3: 通过Tween对象来指定起始值和终止值,并且通过animate方法返回一个Animation对象,
    //             并且设置了监听,最后在监听回调中调用setState(),从而刷新页面
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {});
      });

     // 注释4: 启动动画 
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        // 注释5:通过Animation对象类获取对应的变化值
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
  // 注释6:对controller解注册
    controller.dispose();
    super.dispose();
  }
}

运行效果如图所示:

这里涉及到了两个Dart语言本身的知识点:mixin和..。mixin这里推荐一篇medium上的文章:https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3;而..很简单,它就是为了方便链式调用。我们可以看下前面addListener()方法,方法本身返回的void类型,但是我们最终却返回了一个Animation类型的对象。这其中就是..起到的作用,它可以使得方法返回调用这个方法的对象本身。

8.使用AnimatedWidget来重构上面的代码:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }

        print('$status');
      });

    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedLogo(animation: animation);
  }

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


class AnimatedLogo extends AnimatedWidget {
  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final animation = listenable as Animation;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

先看效果:

大部分代码是和之前的例子是一样的,不同的是:

  • 使用AnimatedWidget,并且Animation对象作为参数传递进来
  • 省略了在addListener的回调里调用setState方法来触发页面刷新

这样写的好处:

  • 省去了需要调用setState的重复代码
  • 使得程序耦合性更低。试想一下,我们的App中有多处都需要实现Logo的resize动画,这个时候我们只需要在使用处定义Animation的描述,最后都使用这里的AnimatedLogo。这样做就使得Widget和Animation的描述进行分离。

我们可以去看一下AnimatedWidget类的源码:

abstract class AnimatedWidget extends StatefulWidget {
  /// Creates a widget that rebuilds when the given listenable changes.
  ///
  /// The [listenable] argument is required.
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  /// The [Listenable] to which this widget is listening.
  ///
  /// Commonly an [Animation] or a [ChangeNotifier].
  final Listenable listenable;

  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
  }
}

class _AnimatedState extends State<AnimatedWidget> {
  @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.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}

可以看到在initState方法里,添加了动画监听,回调的执行逻辑为_handleChange()方法,而_handleChange()的实现就是调用了setState方法,这点和我们之前在第7条中例子的写法一样。也就是说,AnimatedWidget只是做了一层封装而已。

9.使用AnimatedBuilder进一步重构上面的代码:

class LogoDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(child: LogoWidget(), animation: animation);
  }

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


class LogoWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 10),
      child: FlutterLogo(),
    );
  }
}

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: AnimatedBuilder(
          animation: animation,
          builder: (context, child) => Container(
            height: animation.value,
            width: animation.value,
            child: child,
          ),
          child: child),
    );
  }
}

我们可以在任意地方使用这里GrowTransition,代码进行进一步分离。

10.使用Transition。

Flutter还为我们提供一些封装好的Transition,方便我们实现动画效果,下面我们就以ScaleTransition为例,说明如何去使用这些Transition。

class ScaleTransitionDemo extends StatefulWidget {
  @override
  State createState() => _LogoState();
}

class _LogoState extends State with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();

    controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 2));
    animation = Tween<double>(begin: 0, end: 10).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
        child: ScaleTransition(
      scale: animation,
      child: FlutterLogo(),
    ));
  }

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

ScaleTransition在使用时需要指定两个参数:

  • scale: 就是一个Animation对象
  • child: 需要实现缩放动画的widget

最后再注意一下Tween中指定的值所表示的含义,它表示的倍数。比如我们这里end填入了10,表示动画结束状态为放大10倍。我们可以通过ScaleTransition的源码来说服大家:

class ScaleTransition extends AnimatedWidget {
  /// Creates a scale transition.
  ///
  /// The [scale] argument must not be null. The [alignment] argument defaults
  /// to [Alignment.center].
  const ScaleTransition({
    Key key,
    @required Animation<double> scale,
    this.alignment = Alignment.center,
    this.child,
  }) : assert(scale != null),
       super(key: key, listenable: scale);

  /// The animation that controls the scale of the child.
  ///
  /// If the current value of the scale animation is v, the child will be
  /// painted v times its normal size.
  Animation<double> get scale => listenable;

  /// The alignment of the origin of the coordinate system in which the scale
  /// takes place, relative to the size of the box.
  ///
  /// For example, to set the origin of the scale to bottom middle, you can use
  /// an alignment of (0.0, 1.0).
  final Alignment alignment;

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.child}
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final double scaleValue = scale.value;
    final Matrix4 transform = Matrix4.identity()
      ..scale(scaleValue, scaleValue, 1.0);
    return Transform(
      transform: transform,
      alignment: alignment,
      child: child,
    );
  }
}

可以看到,ScaleTransition也是继承自我们前面已经介绍过的AnimatedWidget,然后重点关注build()方法里,用到了Matrix4矩阵,这里的scale.value实际上就是Animation.value,而Matrix4.identity()..scale(),它的三个参数分别表示在x轴、y轴以及z轴上缩放的倍数。

与ScaleTransition类似的还有SlideTransition、RotationTransition等等,读者可以自己去尝试一下,这里就不在一一赘述了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值