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

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

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


  1. 动画进度 AnimationController


  1. 绘制动画 Widget


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


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

目录
相关文章
|
26天前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
105 4
|
27天前
|
缓存 监控 前端开发
优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面
本文探讨了优化 Flutter 应用启动速度的策略,涵盖理解启动过程、资源加载优化、减少初始化工作、界面布局优化、异步初始化、预加载关键数据、性能监控与分析等方面,并通过案例分析展示了具体措施和效果,强调了持续优化的重要性及未来优化方向。
54 10
|
27天前
|
开发框架 前端开发 定位技术
Flutter框架中的插件市场及开源资源的利用方法。内容涵盖插件市场的扩展功能、时间节省与质量保证
本文深入探讨了Flutter框架中的插件市场及开源资源的利用方法。内容涵盖插件市场的扩展功能、时间节省与质量保证,常见插件市场的介绍,选择合适插件的策略,以及开源资源的利用价值与注意事项。通过案例分析和对社区影响的讨论,展示了这些资源如何促进开发效率和技术进步,并展望了未来的发展趋势。
36 11
|
26天前
|
开发框架 数据安全/隐私保护 开发者
Flutter 是一款强大的跨平台移动应用开发框架,本文深入探讨了其布局与样式设计
Flutter 是一款强大的跨平台移动应用开发框架,本文深入探讨了其布局与样式设计,涵盖布局基础、常用组件、样式设计、实战应用、响应式布局及性能优化等方面,助力开发者打造精美用户界面。
41 7
|
26天前
|
开发框架 Dart 前端开发
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。
Flutter 是谷歌推出的一款高效跨平台移动应用开发框架,使用 Dart 语言,具备快速开发、跨平台支持、高性能、热重载及美观界面等特点。本文从 Flutter 简介、特点、开发环境搭建、应用架构、组件详解、路由管理、状态管理、与原生代码交互、性能优化、应用发布与部署及未来趋势等方面,全面解析 Flutter 技术,助你掌握这一前沿开发工具。
56 8
|
26天前
|
缓存 前端开发 数据安全/隐私保护
Flutter 框架提供了丰富的机制和方法来优化键盘处理和输入框体验
在移动应用开发中,Flutter 框架提供了丰富的机制和方法来优化键盘处理和输入框体验。本文深入探讨了键盘的显示与隐藏、输入框的焦点管理、键盘类型的适配、输入框高度自适应、键盘遮挡问题处理及性能优化等关键技术,结合实例分析,旨在帮助开发者提升应用的用户体验。
42 6
|
1月前
|
设计模式 移动开发 开发框架
如何学习 Flutter 框架?
学习 Flutter 需要耐心和持续的努力,通过系统的学习、实践、交流和不断跟进最新技术,你将逐渐掌握 Flutter 框架,并能够开发出高质量的移动应用。
|
1月前
|
开发框架 移动开发 Dart
Flutter 框架的缺点
以上缺点并不意味着 Flutter 框架不优秀,只是在使用过程中需要开发者根据具体的项目需求和场景,充分考虑这些因素,并采取相应的措施来克服或缓解这些问题,以充分发挥 Flutter 的优势,开发出高质量的移动应用。
|
1月前
|
IDE 开发工具 Android开发
Flutter 框架的优点
综上所述,Flutter框架以其跨平台一致性、高性能表现、丰富的组件和插件生态、热重载等诸多优点,为移动应用开发带来了全新的体验和强大的开发能力,成为了越来越多开发者的首选框架。
|
1月前
|
开发工具 UED 容器
Flutter&鸿蒙next 实现长按录音按钮及动画特效
本文介绍了如何在 Flutter 中实现一个带有动画效果的长按录音按钮。通过使用 `GestureDetector` 监听长按手势,结合 `AnimatedContainer` 和 `AnimationController` 实现按钮的动画效果,以及 `flutter_sound` 插件完成录音功能。文章详细讲解了功能需求、实现思路和代码实现,帮助读者逐步掌握这一实用功能的开发方法。
122 5