Flutter动画实战

发布于:2022-07-27 ⋅ 阅读:(208) ⋅ 点赞:(0)

一、动画

原理

Flutter动画基于补间动画,在页面刷新时,会生成一个插值,随后根据插值不断更新RenderObject的状态,实现动画播放。

Flutter动画类型

  1. 隐式动画:使用自带的Animation控件直接显示动画,无法对动画的状态做完整的监听和控制;
  2. 显式动画:自定义Controller、Tween、Curve,可以完整监听和控制动画播放;

隐式动画

如AnimatedContainer、AnimateAlign:

Color _color = Colors.blue;

@override
Widget build(BuildContext context) {
  return AnimatedContainer(
    duration: const Duration(seconds: 1),
    width: 200,
    height: 200,
    color: _color,
  );
}

setState(() {
  _color = Colors.yellow;
});

隐式动画实际上是flutter对显式动画的一种封装,通常是ImplicitlyAnimatedWidget的子类,在内部还是利用显式动画的那一套工具实现的;

abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> 
  extends State<T> with SingleTickerProviderStateMixin<T> {

  /// The animation controller driving this widget's implicit animations.
  @protected
  AnimationController get controller => _controller;
  late final AnimationController _controller = AnimationController(
    duration: widget.duration,
    debugLabel: kDebugMode ? widget.toStringShort() : null,
    vsync: this,
  );

  /// The animation driving this widget's implicit animations.
  Animation<double> get animation => _animation;
  late Animation<double> _animation = _createCurve();
  
  ...  
}

显式动画

  • 幕后主导:Ticker
  • 三大件:AnimationController、Tween、Curve
  • UI:AnimatedWidget(SlideTransition、ScaleTransition等)、AnimatedBuilder、自定义StatefulWidget

Ticker

帧定时器,负责驱动动画的运行,执行start方法后,每一帧渲染前(一个vsync信号)都会回调一次传入的函数。

Ticker ticker = Ticker((elapsed) => print('hello'));

看上去十分简单,就是每帧回调一次,但用法却非常复杂,需要手动start、手动dispose等很多情况。

直接操作Ticker比较复杂,因此创建动画控件时直接使用SingleTickerProviderStateMixin或TickerProviderStateMixin混入到动画控件的state中即可。

AnimationController

和它的名字一致,主要作用是控制和驱动动画的播放、停止等操作。

创建AnimationController时需要传入一个TickerProvider,在Ticker进行逐帧回调时,不断生成0.0 ~ 1.0之间的double值(插值)[相当于每帧刷新一次插值]。

AnimationController的播放控制能力也是通过修改当前插值实现的,如:

  • forward():实际上调用_animateToInternal(upperBound),即将value增加到1.0;
  • reverse():实际上调用_animateToInternal(lowerBound),即将value减小到0.0;
  • repeat():反复将插值从最小值到最大值之间变换;
  • reset():将值调回到0.0。

Tween<T>

主要作用就是转换AnimationController的value

原理上来说,利用AnimationController生成的value就可以实现动画效果了,但是AnimationController的值只能是double类型,如果动画需要实现控件的color、rect、alignment的变换,仅靠一个double值很难实现想要的效果。

给定一个begin和end值,Tween可以根据lerp方法,将AnimationController的value做一些转换。

Tween有很多子类,如ColorTween、RectTween、AlignmentTween等,基本上满足实现动画的需要。若需要将自定义Obj类型变换,有两种方法:

  • 继承Tween<Obj>,重写lerp方法,处理T类的变换函数;
  • T类重写+、-、*操作符,然后直接使用Tween<Obj>;
@protected
  T lerp(double t) {
    ...
    assert(() {
      ...
      try {
        // ignore: avoid_dynamic_calls
        result = (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t;
        result as T;
        return true;
      } on NoSuchMethodError {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('Cannot lerp between "$begin" and "$end".'),
          ErrorDescription(
            'The type ${begin.runtimeType} might not fully implement `+`, `-`, and/or `*`. ',
          ),
          ...
        ]);
      } on TypeError {
        ...
    }());
    // ignore: avoid_dynamic_calls
    return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
  }

这也就是为什么直接使用Tween时只能指定double或Offset类型,因为Tween类lerp方法是通过T的+、-、*运算符直接计算的,如果T没有重载这几个操作符就会导致报错。(Offset内部是重载了的)

用法:Tween是Animatable的子类,通过animate方法可转为Animation类,绑定到AnimationController中。

Animation sizeAnimation = Tween<double>(
  begin: 1.0,
  end: 2.0,
).animate(_animController);

Animation colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.yellow,
).animate(_animController);

Curve

定义动画值的变化速度。AnimationController的value是线性速度生成的,Curve可以改变生成速度,达到加速、减速等动画效果。

系统内置了几十种不同Curve实例,这也是最常用的控制,也可以自己基于cubic(二阶贝塞尔)创建。

总结:

  • Ticker提供逐帧的回调;
  • AnimationController默认提供了从0.0到1.0的值变化,在Ticker回调时生成一个插值,用来进行动画控制;
  • 基于AnimationController的初始值,Tween对其进行转换,Curve对动画速度进行控制。
  • Tween的值包装进Animation对象中,提供了值和状态的监听,绑定到动画组件中实现动画效果。

动画Widget

通过AnimationController、Tween、Curve三大件,可以得到一个Animation对象;

以平移动画为例:

AnimationController _animController = AnimationController(
  duration: Duration(second: 1);
  vsync: this;
);

Animation sildeAnimation = Tween<Offset>(
  begin: Offset.zero,
  end: Offset(1, 1),
).animate(
  CurvedAnimation(parent: _animController, curve: Curves.ease),
);

用法一:AnimatedWidget子类

SlideTransition(
  position: sildeAnimation,
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
);

用法二:AnimateBuilder

AnimatedBuilder(
  animation: _animController,
  builder: (context, child) {
    return FractionalTranslation(
      translation: sildeAnimation.value,
      child: Container(
        width: 200,
        height: 200,
        color: Colors.blue,
      ),
    );
  },
);

用法三:StatefulWidget(需要手动setState())

_animController.addListener(() {
  setState(() {
  });
});

FractionalTranslation(
  translation: sildeAnimation.value,
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
);

组合动画

1、顺序动画(同步)

TweenSequence按顺序执行每一个子Tween item,通过weigt参数控制每一个item的时间占比:

_offsetAnimation = TweenSequence([
  TweenSequenceItem(
    tween: Tween<Offset>(begin: Offset.zero, end: const Offset(1, 1)),
    weight: 1,
  ),
  TweenSequenceItem(
    tween: Tween<Offset>(begin: const Offset(1, 1), end: Offset.zero),
    weight: 1,
  ),
]).animate(_animController);

交织动画(异步),通过Interval控制每个子动画的起始和终止的时间;

_offsetAnimation = Tween<Offset>(
  begin: Offset.zero,
  end: const Offset(2, 1),
).animate(
  CurvedAnimation(
    parent: _animController,
    curve: const Interval(0.0, 1.0, curve: Curves.linear),
  ),
);

_colorAnimation = ColorTween(
  begin: Colors.blue,
  end: Colors.yellow,
).animate(
  CurvedAnimation(
    parent: _animController,
    curve: const Interval(0.3, 0.7, curve: Curves.linear),
  ),
);

 

二、游戏

以认单词游戏为例:动画数量多,控件数量多,状态管理复杂。

  1. 枚举动画的所有状态,分类整理成状态组,如人物动作、人物位置、地图位置等,每种状态组包含多个状态,例如人物动作中有站立、跑步、欢呼状态。
  2. 创建游戏剧本,将游戏中的每个状态按顺序保存,并使用链表结构将其串联;
  3. 创建GameController,持有游戏剧本,控制游戏剧本的走向;
  4. 将游戏的各个元素从Page中抽成子控件,如人物控件、地图控件、选择器控件等;
  5. 各个控件和GameController的通信通过Cubit实现;
  6. 控件中,通过BlocBuilder、BlocListener监听自己关心的状态,当监听游戏切换到自己的状态时,开始执行动画(或其他操作),结束后回调notifyStateFinish通知GameController,GameController会转到到下一个状态;
  7. GameController收到notifyStateFinish通知后,视为当前状态已经结束,按照游戏剧本切换到下一个状态,让Cubit去通知下一个组件,循环往复该过程;

认单词游戏属于线性游戏流程,也就是游戏的流程固定,不会受到用户的操作影响而改变游戏的走向。如果是多分支的游戏,可以将游戏剧本也做成链表结构,创建多个子剧本,GameController再根据游戏的具体走向,连接到下一个子剧本中,以实现切换分支的功能。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到