Flutter 基础 | 动画框架分析及其中的设计模式

Posted 普通网友

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter 基础 | 动画框架分析及其中的设计模式相关的知识,希望对你有一定的参考价值。

作者:唐子玄

在阅读 Flutter 动画源码时收获颇多,深深地被它的设计框架及代码实现方式所折服~~

若不关心源码,想直接上手动画实战代码可以跳到动画实例解析

动画源码解析

动画值序列 Tween

动画是为了缓解值的“跳变”,跳变体验不好。

比如弹窗通常是由小变大(scale),由浅变深(alpha),为了让弹窗不那么突兀,就得生成 scale 和 alpha 的值序列,让弹窗一点一点变大,一点一点显现出来。

在 Flutter 中生成动画值序列的工作是由Tween完成的。

Tween 继承自Animatable:

class Tween<T extends Object?> extends Animatable<T> 

// 可动画的对象
abstract class Animatable<T> 
  // 根据动画进度 t 生成对应动画值 T(动画进度为[0,1])
  T transform(double t);

Animatable 是一个抽象类,其中最重要的方法是T transform(double t),它定义了一个生成动画值的算法,即根据给定动画进度生成单个动画值。

所以Animatable是一个将动画进度转换为动画值的对象。

Tween 重写了 transform():

class Tween<T extends Object?> extends Animatable<T> 
  // 动画值序列起点
  T? begin;
  // 动画值序列终点
  T? end;

  @override
  T transform(double t) 
    if (t == 0.0)
      return begin as T; // 若动画进度为0,直接返回起点值
    if (t == 1.0)
      return end as T; // 若动画进度为1,直接返回终点值
    return lerp(t); // 否则,返回lerp(t)
  

Tween 在 Animatable 基础上新增了2个成员变量表示动画值序列的起点和终点,对应动画进度的 0 和 1。除了这两个极值之外的其他动画值通过lerp()方法生成:

class Tween<T extends Object?> extends Animatable<T> 
  @protected
  T lerp(double t) 
    ...
    return (begin as dynamic) + ((end as dynamic) - (begin as dynamic)) * t as T;
  

lerp 是“线性插值”的意思。在直角坐标系中,动画值是动画进度 t 的函数,而且这是一段斜线,它的斜率和离原点的偏移量由 begin 和 end 决定。

所以Tween是一个将动画进度转换为给定区间动画值序列的对象,称之为补间

大部分 Tween 的子类通过重写lerp()方法实现不同的线性插值,比如ReverseTween:

class ReverseTween<T extends Object?> extends Tween<T> 
  ReverseTween(this.parent)
    : assert(parent != null),
      super(begin: parent.end, end: parent.begin);

  final Tween<T> parent;

  @override
  T lerp(double t) => parent.lerp(1.0 - t);

ReverseTween 表示反向补间,所以它的 lerp() 委托给了另一个补间对象并传入反向的动画进度1.0-t

再比如ColorTween

class ColorTween extends Tween<Color?> 
  ColorTween( Color? begin, Color? end ) : super(begin: begin, end: end);

  @override
  Color? lerp(double t) => Color.lerp(begin, end, t);

ColorTween 表示颜色补间,它把插值算法委托给了Color.lerp(),该方法内部分别对A,R,G,B实现了插值。

这就是典型的模板方法模式,即在父类实现算法框架,并将算法的某些步骤抽象为方法,子类通过重写该方法来替换掉算法的某个步骤。

模板方法的目的是复用算法框架,在当前场景的算法框架即是将动画进度转换成动画值的算法:

class Tween<T extends Object?> extends Animatable<T> 
  // transform 这个算法框架在父类 Tween 得以固定,
  // 子类通过重写 lerp() 实现各种不同的转换
  @override
  T transform(double t) 
    if (t == 0.0)
      return begin as T; 
    if (t == 1.0)
      return end as T; 
    return lerp(t); 
  

关于模板方法模式更详细的介绍可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

并不是所有的插值都是线性的,有些插值是一条曲线,CurveTween就是用来表达曲线插值的补间:

class CurveTween extends Animatable<double> 
  CurveTween( required this.curve )
    : assert(curve != null);
  // 曲线
  Curve curve;

  @override
  double transform(double t) 
    if (t == 0.0 || t == 1.0) 
      assert(curve.transform(t).round() == t);
      return t;
    
    return curve.transform(t); // 由具体的曲线实现插值
  

CurveTween 重写了transform()并把它委托给对应的曲线。

动画进度 AnimationController

仅有动画值序列还不能满足做动画的需求,就好比即使知道完整的音符序列也不能演奏出美妙的曲子,因为没有节奏信息,即不知道每个音符应该在哪个时间点被激活。

在 Flutter 中动画节奏信息由AnimationController承载。

class AnimationController extends Animation<double> 

AnimationController 继承自 Animation。而 Animation 继承自Listenable

// 动画对象
abstract class Animation<T> extends Listenable implements ValueListenable<T> 

// 可监听对象
abstract class Listenable 
  // 添加监听器
  void addListener(VoidCallback listener);
  // 移除监听器
  void removeListener(VoidCallback listener);


// 可监听的值
abstract class ValueListenable<T> extends Listenable 
  // 值
  T get value;

Animation是一个可监听的值,用泛型声明,表示值的类型任意。而且它是抽象的,获取动画值会推迟到子类 AnimationController 中定义。

class AnimationController extends Animation<double> 
  @override
  double get value => _value;
  // 动画进度
  late double _value;
  // 时钟
  Ticker? _ticker;

AnimationController 把获取动画值委托给了一个私有成员_value,它的语义是动画进度。

AnimationController 中有另一个私有成员叫_ticker,它是一个时钟:

class Ticker 
  // 滴答回调
  final TickerCallback _onTick;

  // 启动时钟
  TickerFuture start() 
    ...
    scheduleTick();
  

  // 安排一次滴答
  void scheduleTick( bool rescheduling = false ) 
    ..
    // 注册下一帧的绘制并执行_tick
    _animationId = SchedulerBinding.instance!.scheduleFrameCallback(_tick,   rescheduling: rescheduling);
  

  void _tick(Duration timeStamp) 
    // 回调滴答给上层
    _onTick(timeStamp - _startTime!);
    //
    if (shouldScheduleTick)
      scheduleTick(rescheduling: true);
  

当时钟被启动后,它会注册每一帧的绘制,并通过回调传递给上层。当前 Ticker 的上层即是 AnimationController:

class AnimationController extends Animation<double> 

  AnimationController(
    ..
    required TickerProvider vsync,
  ) 
    // 构建时钟
    _ticker = vsync.createTicker(_tick);
  

  void _tick(Duration elapsed) 
    ..
    // 计算逝去的时间
    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
    // 根据时间生成新的动画值
    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
    // 通知动画监听者
    notifyListeners();
  

AnimationController 在构造方法中新建时钟实例,动画开始就启动时钟,在绘制的每一帧根据逝去的时间生成当前动画进度,最后通知动画进度监听者。(通常是Widget)

AnimationController 承载动画节奏信息。其内部有一个时钟,用于持续地监听下一帧的绘制,并通知上层,AnimationController 根据绘制的节奏再结合流逝的时间按照一定的算法计算出当前的动画进度并通知观察者(Widget)。

动画进度 -> 动画值序列

AnimationController 生成动画进度,而 Tween 根据动画进度生成动画值序列。它俩是如何结合在一起的?

答案是Animatable.animate()

abstract class Animatable<T> 
  // 根据动画进度 t 生成对应动画值 T(动画进度为[0,1])
  T transform(double t);
  // transform() 的包装方法,由 animation 提供动画进度
  T evaluate(Animation<double> animation) => transform(animation.value);
  // 将一个 Animation 转换成另一个 Animation
  Animation<T> animate(Animation<double> parent) 
    return _AnimatedEvaluation<T>(parent, this);
  

Tween 的基类 Animatable 除了能将动画进度转换成动画值之外,还提供了一个animate()方法,它的输入和输出都是 Animation,看上去像是把一个 Animation 转换成另一个 Animation,该方法的实现也是直接返回了一个_AnimatedEvaluation

class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> 
  // 在构造时注入父亲动画和 Animatable 对象
  _AnimatedEvaluation(this.parent, this._evaluatable);
  // 持有父亲动画
  @override
  final Animation<double> parent;
  // 持有 Animatable 对象
  final Animatable<T> _evaluatable;
  // 获取动画值:将父亲动画值转换为新动画值
  @override
  T get value => _evaluatable.evaluate(parent);

AnimatedEvaluation 继承自 Animation,并且它持有一个 Animation 实例。这就是典型的装饰者模式

关于装饰者模式的详细介绍可以点击 使用组合的设计模式 | 美颜相机中的装饰者模式 - 掘金 (juejin.cn)

装饰者模式的目的是扩展行为, AnimatedEvaluation 想扩展的行为是“获取动画值”,它把该行为委托给了一个 Animatable 对象。这就巧妙地实现了 “将父亲动画值转换为新动画值”

这里除了装饰者模式之外,还有适配器模式, AnimatedEvaluation 通过组合的方式持有 Animatable 实例并将其适配为 Animation。

Animation 是一个可以被监听的值,而 Animatable 提供了将动画值进行转换的功能。对于希望做动画的控件来说,它们希望和 Animation 对接,因为它可以被监听。但上层又希望能方便地变换动画值,所以 Animatable 被适配成了 Animation。

动画实例解析

结合一段控件做动画的代码,就能明白这样设计的妙处了:

class MyWidget extends StatefulWidget 
 @override
 _MyWidgetState createState() => _MyWidgetState();


class _MyWidgetState extends State<MyWidget> 
 @override
 Widget build(BuildContext context) 
   return Scaffold(
     appBar: AppBar(
       title: Text("Animation Demo"),
     ),
     body : Center(
       child: Container(
         width: 100,
         height: 100,
         color: Colors.red
       )
     )
   );
 

这是一个在屏幕中间固定宽高的红色矩形。来做一个让矩形变大的动画:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  // 动画控制器
  AnimationController controller; 
  // 变大动画
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   // 1.构造动画控制器
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   // 2.构造变大动画
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   // 3.开始动画
   controller.forward();
  

首先让 State 复用SingleTickerProviderStateMixin的行为:

mixin SingleTickerProviderStateMixin<T extends StatefulWidget> 
  on State<T> 
  implements TickerProvider  

SingleTickerProviderStateMixin 实现了 TickerProvider,这正是构造动画控制器必须的参数:

AnimationController(
  ...
  required TickerProvider vsync,
)

源码中以 Provider 命名的类通常都运用了抽象工厂设计模式,它将构建对象的细节抽象在一个接口中:

abstract class TickerProvider 
  @factory
  Ticker createTicker(TickerCallback onTick);

TickerProvider 想构建的对象是一个时钟 Ticker。

这样一来,动画控制器就无需关心构建时钟的细节:

AnimationController(
  ...
  required TickerProvider vsync,
) : _direction = _AnimationDirection.forward 
  _ticker = vsync.createTicker(_tick); // 构建时钟

无需关心细节意味着解耦,还有一个好处是动画控制器的上层类可以动态替换构建时钟的算法。关于工厂模式的详细介绍可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

构建完动画控制器后,就构建了 Tween 对象,并指定了动画值序列的起点和终点,紧接着就调用了animate()方法,这一步很关键:

sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);

这一步完成了动画进度到动画值序列的转换。其中 controller 承载着动画进度信息被作为参数传入。结合刚才的源码分析,controller 是一个 Animation 对象,在方法内部,它被装饰成另一个叫 _AnimatedEvaluation 的动画对象,该动画对象还持有了刚构建出来的 Tween,并把计算动画值的方法委托给了 Tween,完成了一次 Animatable 到 Animation 的适配

如此这般地华丽操作之后,调用 controller.forward() 开启动画,此时 sizeAnimation.value 就是变大动画对应的值序列。现在就要把它应用到控件的属性上:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   // 开始动画
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: Container(
           width: sizeAnimation.value, // 将动画值应用在宽度
           height: sizeAnimation.value, // 将动画值应用在高度
           color: Colors.red
         )
       )
     );
   

光是上面这段代码还不足以让控件动起来,因为动画控制器是一个可观察的值,但现在并没有控件观察它的变化:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
   // 监听动画值的变化并重绘控件
   controller.addListener(() 
     setState(() );
   )
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: Container(
           width: sizeAnimation.value, 
           height: sizeAnimation.value, 
           color: Colors.red
         )
       )
     );
   

这就是完整的动画代码。

封装动画代码

AnimatedWidget

Flutter 提供了一个控件,专门用于做动画,它封装了监听动画值这个操作:

abstract class AnimatedWidget extends StatefulWidget 

  const AnimatedWidget(
    Key? key,
    required this.listenable,
  ) : assert(listenable != null),
       super(key: key);

  // 可被监听的东西(其实就是AnimationController)
  final Listenable listenable;

  // 子类重载这个方法实现每一帧动画的重绘逻辑
  @protected
  Widget build(BuildContext context);

  @override
  State<AnimatedWidget> createState() => _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(() 
    );
  
  // 将构建控件委托给widget.build()
  @override
  Widget build(BuildContext context) => widget.build(context);

然后就可以将动画组件单独抽象为一个控件:

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

  @override
  Widget build(BuildContext context)
    return Center(
      child: Container(
        width: animation.value,
        height: animation.value, 
        color: Colors.red
     )
   );
  


class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : SizeTransition(animation: sizeAnimation)
     );
   

AnimatedBuilder

当页面中需要做动画的部分不太复杂的时候,也可以直接用 AnimatedBuilder 来进一步简化代码:

class AnimatedBuilder extends AnimatedWidget 
  const AnimatedBuilder(
    Key? key,
    required Listenable animation,
    required this.builder,
    this.child,
  ) : super(key: key, listenable: animation);

  final TransitionBuilder builder;

  final Widget? child;

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


typedef TransitionBuilder = Widget Function(BuildContext context, Widget? child);

AnimatedBuilder 是 AnimatedWidget 的子类,它将构建动画控件的逻辑抽象在一个函数中,而且这个函数可以动态注入,这样就不需要像 AnimatedWidget 那样通过新建类并继承来实现动画了:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin 
  AnimationController controller; 
  Animation sizeAnimation;

  @override
  void initState() 
   super.initState();
   controller =  AnimationController(vsync: this, duration: Duration(seconds: 2));
   sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(controller);
   controller.forward();
  

   @override
   Widget build(BuildContext context) 
     return Scaffold(
       appBar: AppBar(
         title: Text("Animation Demo"),
       ),
       body : Center(
         child: AnimatedBuilder(
           animation: sizeAnimation,
           builder: (context, child) 
             return  Container(
               width: sizeAnimation.value, 
               height: sizeAnimation.value, 
               color: Colors.red
             );
           
         )
       )
     );
   

这就是典型的策略模式,它的目的是动态的替换行为。在这里如何构建控件被抽象为一组策略,上层代码可以动态地替换 AnimatedBuilder 构建控件的策略,这使得它和构建的细节解构。关于策略模式的详细分析可以点击一句话总结殊途同归的设计模式:工厂模式=?策略模式=?模版方法模式 - 掘金 (juejin.cn)

总结

读完 Flutter 关于动画的源码后,第一感觉就是“它把一个复杂问题拆分的很到位!”。

动画被拆分为三个独立的部分:

  1. 动画值序列 Tween
  2. 动画进度 AnimationController
  3. 绘制动画 Widget

其中 Tween 只关心如何生成动画值序列,AnimationController 负责监听渲染节奏并以此生成动画进度,而这两个概念和绘制解耦,即它们完全不知道动画会怎样呈现在屏幕上。因为呈现交给了 Widget 来处理。

Flutter 还运用了多种设计模式来增加动画代码的弹性,包括模板方法模式、策略模式、适配器模式、抽象工厂模式、装饰者模式。

(我什么时候才能写出这么优雅的代码?)

以上是关于Flutter 基础 | 动画框架分析及其中的设计模式的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 基础 | 动画框架分析及其中的设计模式

Flutter 实际开发常用工具类(全局提示,请求封装,常用窗帘动画及布局)

深入分析 Flutter 渲染性能

flutter基础总结

基于dio库封装flutter项目的标准网络框架

Web前端主流框架—Angular的优缺点分析