阅读 514

Flutter训练营(八)-Flutter动画开发

这是我参与更文挑战的第11天,活动详情查看: 更文挑战

Flutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件,同时也是当下最流行的跨端解决方案。

前言

精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。Flutter的动画支持可以轻松实现各种动画类型。许多widget,特别是Material Design widgets, 都带有在其设计规范中定义的标准动画效果,但也可以自定义这些效果。

image.png

一、动画的重要性

精心设计的动画会让用户界面感觉更直观、流畅,能改善用户体验。Flutter的动画支持可以轻松实现各种动画类型。许多widget,特别是Material Design widgets, 都带有在其设计规范中定义的标准动画效果,但也可以自定义这些效果。

动画在各个平台的实现原理都基本相同,是在一段时间内一系列连续变化画面的帧构成的。在 Flutter 中,动画的过程又被量化成一段值区间,我们可以利用这些值设置控件的各个属性来实现动画,其内部由四个关键的部分来实现这一过程。

1.1 插值器(Tweens)

tweens 可为动画提供起始值和结束值。默认情况下,Flutter 中的动画将任何给定时刻的值映射到介于 0.0 和 1.0 之间的 double 值。我们可以使用以下 Tween 将其间值的范围定义为从 -200.0变为 0.0:

tween = Tween<double>(begin: -200, end: 0);
复制代码

我们也可以将值设置为相应需要改变的对象值,比如将起始值设置为红色,结束值设置为蓝色,那么 tweens 产生的动画便是由红渐渐的变成蓝色。如下:

colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
复制代码

1.2 动画曲线(Animation Curves)

Curves 用来调整动画过程中随时间的变化率,默认情况下,动画以均匀的线性模型变化。读者可以通过自定义继承 Curves 的类来定义动画的变化率,比如设置为加速、减速或者先加速后减速等曲线模型。Flutter 内部也提供了一系列实现相应变化率的 Curves 对象:

  • linear

  • decelerate

  • ease

  • easeIn

  • easeOut

  • easeInOut

  • fastOutSlowIn

  • bounceIn

  • bounceOut

  • bounceInOut

  • elasticIn

  • elasticOut

  • elasticInOut

1.3 Ticker providers

Flutter 中的动画以屏幕频繁的重绘而实现,即每秒 60 帧。Ticker 可以被应用在 Flutter 每个对象中,当对象实现了 Ticker 的功能后,每次动画帧改变便会通知该对象。这里,开发者们不需要为对象手动实现 Ticker,flutter 提供了 TickerProvider 类可以帮助我们快速实现该功能。例如,在有状态控件下使用动画时,通常需要在 State 对象下混入 TickerProviderStateMixin。

class _MyAnimationState extends State<MyAnimation> 
    with TickerProviderStateMixin {

}
复制代码

1.4 动画控制器(AnimationController)

Flutter 中动画的实现还有一个非常重要的类 AnimationController,即动画控制器。很明显,我们用它来控制动画,即动画的启动、暂停等。其接受两个参数,第一个是 vsync,为 Ticker 对象,其作用是当接受到来自 tweens 和 curves 的新值后通知对应对象,第二个 duration 参数为动画持续的时长。

// 混入 SingleTickerProviderStateMixin 使对象实现 Ticker 功能
class _AnimatedContainerState extends State<AnimatedContainer>
        with SingleTickerProviderStateMixin {
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    // 创建 AnimationController 动画
    _controller = AnimationController(
      // 传入 Ticker 对象
      vsync: this,
      // 传入 动画持续时间
      duration: new Duration(milliseconds: 1000),
    );
    startAnimation();
  }

  Future<void> startAnimation() async {
    // 调用 AnimationController 的 forward 方法启动动画
    await _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _controller.value;
      child: //...
    );
  }
}
复制代码

AnimationController 继承自 Animation,具有一系列控制动画的方法,如可用 forward() 方法来启动动画,可用 repeat() 方法使动画重复执行,也可以通过其 value 属性得到当前值。

二、动画类型

在Flutter中动画分为两类:基于tween或基于p2物理的。

2.1 补间(Tween)动画

在补间动画中,定义了开始点和结束点、时间线以及定义转换时间和速度的曲线。然后由框架计算如何从开始点过渡到结束点。

2.2 基于物理的动画

在基于物理的动画中,运动被模拟为与真实世界的行为相似。例如,当你掷球时,它在何处落地,取决于抛球速度有多快、球有多重、距离地面有多远。类似地,将连接在弹簧上的球落下(并弹起)与连接到绳子上的球放下的方式也是不同。

三、Flutter动画库

在为widget添加动画之前,先让我们认识下动画的几个朋友: Animation:是Flutter动画库中的一个核心类,它生成指导动画的值;

CurvedAnimation:Animation的一个子类,将过程抽象为一个非线性曲线;

AnimationController:Animation的一个子类,用来管理Animation;

Tween:在正在执行动画的对象所使用的数据范围之间生成值。例如,Tween可生成从红到蓝之间的色值,或者从0到255;

3.1 Animation

在Flutter中,Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,它拥有其当前值和状态(完成或停止)。其中一个比较常用的Animation类是Animation。 Flutter中的Animation对象是一个在一段时间内依次生成一个区间之间值的类。Animation对象的输出可以是线性的、曲线的、一个步进函数或者任何其他可以设计的映射。根据Animation对象的控制方式,动画可以反向运行,甚至可以在中间切换方向。

  • Animation还可以生成除double之外的其他类型值,如:AnimationAnimation

  • Animation对象有状态。可以通过访问其value属性获取动画的当前值;

  • Animation对象本身和UI渲染没有任何关系;

3.2 CurvedAnimation

CurvedAnimation将动画过程定义为一个非线性曲线。

final CurvedAnimation curve =
   new CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curves 类定义了许多常用的曲线,也可以创建自己的,例如:
class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}
复制代码

3.3 AnimationController

AnimationController是一个特殊的Animation对象,在屏幕刷新的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内会线性的生成从0.0到1.0的数字。例如,下面代码创建一个Animation对象:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);
复制代码

AnimationController派生自Animation,因此可以在需要Animation对象的任何地方使用。但是,AnimationController具有控制动画的其他方法:

  • forward():启动动画;

  • reverse({double from}):倒放动画;

  • reset():重置动画,将其设置到动画的开始位置;

  • stop({ bool canceled = true }):停止动画;

当创建一个AnimationController时,需要传递一个vsync参数,存在vsync时会防止屏幕外动画消耗不必要的资源,可以将stateful对象作为vsync的值。

在某些情况下,值(position,值动画的当前值)可能会超出AnimationController的0.0-1.0的范围。例如,fling()函数允许您提供速度(velocity)、力量(force)、position(通过Force对象)。位置(position)可以是任何东西,因此可以在0.0到1.0范围之外。CurvedAnimation生成的值也可以超出0.0到1.0的范围。根据选择的曲线,CurvedAnimation的输出可以具有比输入更大的范围。例如,Curves.elasticIn等弹性曲线会生成大于或小于默认范围的值。

3.4 Tween

默认情况下,AnimationController对象的范围从0.0到1.0。如果您需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。例如,以下示例,Tween生成从-200.0到0.0的值:

final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
复制代码

Tween是一个无状态(stateless)对象,需要begin和end值。Tween的唯一职责就是定义从输入范围到输出范围的映射。输入范围通常为0.0到1.0,但这不是必须的。

Tween继承自Animatable,而不是继承自Animation。Animatable与Animation相似,不是必须输出double值。例如,ColorTween指定两种颜色之间的过渡。

final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);
复制代码

Tween对象不存储任何状态。相反,它提供了evaluate(Animationanimation)方法将映射函数应用于动画当前值。Animation对象的当前值可以通过value()方法取到。evaluate函数还执行一些其它处理,例如分别确保在动画值为0.0和1.0时返回开始和结束状态。 要使用Tween对象,可调用它的animate()方法,传入一个控制器对象。例如,以下代码在500毫秒内生成从0到255的整数值。

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
// 注意animate()返回的是一个Animation,而不是一个Animatable。
复制代码

以下示例构建了一个控制器、一条曲线和一个Tween:

final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);
复制代码

四、为widget添加动画

在下面的实例中我们为一个logo添加了一个从小放大的动画:

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  AnimationStatus animationState;
  double animationValue;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    // #docregion addListener
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        // #enddocregion addListener
        setState(() {
          animationValue = animation.value;
        });
        // #docregion addListener
      })
      ..addStatusListener((AnimationStatus state) {
        setState(() {
          animationState = state;
        });
      });
    // #enddocregion addListener
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.only(top: 50),
      child: Column(
        children: <Widget>[
          GestureDetector(
            onTap: () {
              controller.reset();
              controller.forward();
            },
            child: Text('Start', textDirection: TextDirection.ltr),
          ),
          Text('State:' + animationState.toString(),
              textDirection: TextDirection.ltr),
          Text('Value:' + animationValue.toString(),
              textDirection: TextDirection.ltr),
          Container(
            height: animation.value,
            width: animation.value,
            child: FlutterLogo(),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}
复制代码

五、为动画添加监听器

有时我们需要知道动画执行的进度和状态,在Flutter中我们可以通过Animation的addListener与addStatusListener方法为动画添加监听器: addListener:动画的值发生变化时被调用;

addStatusListener:动画状态发生变化时被调用;

 @override
  void initState() {
    super.initState();
    controller =
        AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      // #enddocregion print-state
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })
      // #docregion print-state
      ..addStatusListener((state) => print('$state'));
      ..addListener(() {
        // #enddocregion addListener
        setState(() {
          // The state that has changed here is the animation object’s value.
        });
        // #docregion addListener
      });
    controller.forward();
  }
复制代码

六、AnimatedWidget

我们可以将AnimatedWidget理解为Animation的助手,使用它可以简化我们对动画的使用,在为widget添加动画的学习中我们不难发现,在不使用AnimatedWidget的情况下需要手动调用动画的addListener()并在回调中添加setState才能看到动画效果,AnimatedWidget将为我们简化这一操作。 在下面的重构示例中,LogoApp现在继承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在绘制时使用动画的当前值。LogoApp仍然管理着AnimationController和Tween。

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

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Container(
        margin: new EdgeInsets.symmetric(vertical: 10.0),
        height: animation.value,
        width: animation.value,
        child: new FlutterLogo(),
      ),
    );
  }
}

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

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

  dispose() {
    controller.dispose();
    super.dispose();
  }
}
复制代码

6.1 AnimatedBuilder

AnimatedBuilder是用于构建动画的通用widget,AnimatedBuilder对于希望将动画作为更大构建函数的一部分包含在内的更复杂的widget时非常有用,其实你可以这样理解:AnimatedBuilder是拆分动画的一个工具类,借助它我们可以将动画和widget进行分离: 在上面的实例中我们的代码存在的一个问题:更改动画需要更改显示logo的widget。更好的解决方案是将职责分离:

  • 显示logo

  • 定义Animation对象

  • 渲染过渡效果

接下来我们就借助AnimatedBuilder]()类来完成此分离。AnimatedBuilder是渲染树中的一个独立的类, 与[AnimatedWidget类似,AnimatedBuilder自动监听来自Animation对象的通知,不需要手动调用addListener()。 我们根据下图的 widget 树来创建我们的代码:

...
// #docregion LogoWidget
class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        child: FlutterLogo(),
      );
}
// #enddocregion LogoWidget

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

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
        child: AnimatedBuilder(
            animation: animation,
            builder: (context, child) => Container(
                  height: animation.value,
                  width: animation.value,
                  child: child,
                ),
            child: child),
      );
}
// #enddocregion GrowTransition

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

// #docregion print-state
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

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

  @override
  Widget build(BuildContext context) => GrowTransition(
        child: LogoWidget(),
        animation: animation,
      );

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
  // #docregion print-state
}
复制代码

Flutter动画类的源码

7.1 Animation

Animation是一个抽象的类,主要保存动画的状态和当前值。最常用的Animation类是Animation,T 有很多类型,可以通过Animation中的 value 属性获得当前动画的值。动画的监听:

  • addListener() 每一帧动画执行的监听

  • addStatusListener() 动画状态改变的监听。

AnimationController

  • AnimationController 继承Animation 负责控制动画的执行,停止等。
  • AnimationController 会在动画的每一帧,就会生成一个新的值。默认情况下,- - AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。

创建 AnimationController,则需要传入一个 vsync 参数。

Tween

  • 默认情况下,AnimationController对象的范围从0.0到1.0。

  • 如果你需要不同范围或者不同的数据类型,就需要tween来配置动画以生成不同的范围或数据类型的值。

7.2 AnimatedWidget

abstract class AnimatedWidget extends StatefulWidget {
    //创建一个widget,当listenable 发生改变时重构
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);
  //声明一个Listenable ,帧动画监听
  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

  /// Subclasses typically do not override this method.
  @override
  _AnimatedState createState() => _AnimatedState();
...
}
复制代码

AnimatedWidget继承自StatefulWidget,拥有自己的状态,并且实例一个listenable用来监听帧动画,当动画方法变化时,刷新AnimatedWidget。

// __AnimatedState方法源码如下:
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.
    });
  }
//调用build()方法,重构AnimatedWidget
  @override
  Widget build(BuildContext context) => widget.build(context);
}
复制代码

当listenable触发刷新的时候,调用 setState重构AnimatedWidget,虽然到最后还是调用setState,但是刷新的对象是不同的。

7.3 CurvedAnimation

Tween动画默认为我们提供了区间内线性变化,如果我们需要曲线变化,则需要配合使用CurvedAnimation。

class HomePageState extends State<HomePage> with TickerProviderStateMixin{

  Animation animation;
  AnimationController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
    CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
      ..addListener((){
        setState(() {
        });
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      alignment: Alignment.topCenter,
      child: Container(
        margin: EdgeInsets.only(top: animation.value),
        width: 100,
        height: 100,
        child: FlutterLogo(),
      ),
    );
  }
}

// CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut); 这里我们使用的是bounceOut效果

class _BounceOutCurve extends Curve {
  const _BounceOutCurve._();

  @override
  double transformInternal(double t) {
    return _bounce(t);
  }
}

double _bounce(double t) {
  if (t < 1.0 / 2.75) {
    return 7.5625 * t * t;
  } else if (t < 2 / 2.75) {
    t -= 1.5 / 2.75;
    return 7.5625 * t * t + 0.75;
  } else if (t < 2.5 / 2.75) {
    t -= 2.25 / 2.75;
    return 7.5625 * t * t + 0.9375;
  }
  t -= 2.625 / 2.75;
  return 7.5625 * t * t + 0.984375;
}
// 可以看到bounceOut 是继承 Curve类,实现它的transformInternal方法,在transformInternal实现它的轨迹。
复制代码

7.4 使用AnimatedBuilder来重构我们的widget

上面这个demo代码主要是实现一个颜色变化的例子,假如我们现在需要实现一个大小变化的widget呢?是不是要在声明一个SizeAnimationWidget继承AnimationWidget ?显然这样做是可以的。当然我们也有更好的做法,就是使用AnimatedBuilder来重构我们的widget。

AnimatedBuilder继承自抽象的AnimationWidget ,目的为了构建通用的AnimationWidget 实现类,不用每次使用AnimationWidget 都要创建一个实现类。

// AnimatedBuilder源码
class AnimatedBuilder extends AnimatedWidget {
  /// Creates an animated builder.
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

// 使用的时候我们只需要传入animation和builder就行了
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    final CurvedAnimation curve =
    new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
    animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
    controller.forward();
  }

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

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

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

  final Widget child;
  final Animation<double> animation;

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

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: new FlutterLogo(),
    );
  }
}
复制代码

总结

通过上面的栗子,我们已经可以方便地封装出一系列控件动画了,但是这种实现方式均需要我们自己提供 Animation 对象,然后通过提供的接口方法来启动我们的动画,控件的属性由 Animation 对象提供并在动画过程中改变而达到动画的效果。为了使动画更加方便,Flutter 帮助了开发者从另一个角度以更简单的方式实现了动画效果——隐式动画组件(ImplicitlyAnimatedWidget)。 通过隐式动画组件,不需要手动实现插值器、曲线等对象,开发者甚至也不需要使用 AnimationController 来启动动画,它的实现方式更贴近对组件本身的操作,我们可以直接通过 setState() 的方法改变隐式动画组件的属性值,其内部自行为我们实现动画过程的过渡效果,即隐藏了所有动画实现的细节

文章分类
iOS