Flutter 动画原理

151 阅读3分钟

渲染

现代操作系统GUI渲染流程API:

classDiagram

class Window {
  <<abstract>> 
  +scheduleFrame() 
  +setOnFrame(FrameCallback onFrame)
  +render(Frame frame)
}

系统提供的核心API,其目的是希望上次这样来调用:

  1. scheduleFrame() 通过此函数来向系统申请调度帧。
  2. setOnFrame(FrameCallback onFrame) 系统将在下一帧渲染之前通过此回调来告知调用者
  3. render() 调用者再通过此方法告知系统执行接下来的上屏逻辑

为何要这么设计?一个render()方法不行么?

Flutter Framework中的渲染API

PlatformDispatcher

classDiagram

class PlatformDispatcher {
  scheduleFrame()
  setOnBeginFrame(FrameCallback onBeginFrame)
  setOnDrawFrame(VoidCallback? onDrawFrame)
}

class FlutterView {
    <<abstract>>
    getPlatformDispatcher() PlatformDispatcher
    render(Scene scene)
}

FlutterView --> PlatformDispatcher

除了将其拆分成两个类实现,基本跟计算机渲染流程保持一致。值得提的是其将更新帧数据的回调跟绘制回调的区分开。

思考点:这样做的好处是什么?

图形渲染管线(Graphic pipeline)

framework层渲染时序:

sequenceDiagram

participant framework as 框架
participant engine as  引擎

framework ->> engine: 1. scheduleFrame()
engine ->> framework: 2. onBeginFrame(Duration duration)
engine ->> framework: 3. onDrawFrame()
framework ->> engine: 4. render(Scene scene)
  • 为了是整体能够垂直同步,采用申请系统回调的机制来保证何时调用
  • 为了节省资源,只在需要的时候才回调
  • 单次渲染就是在render

完整的图形渲染管线流程 完整的图形渲染管线流程

  • 绿色: framework层
  • Layer Tree: framework层渲染产物
  • 蓝色: engine层

framework层渲染流程图 framework rendering pipeline.png

flutter framework层的渲染分为2个阶段:

  • 短暂回调阶段 => Animate
  • 持续回调阶段 => Build、Layout、Paint

值得一提的是: 两个阶段间隙,flutter 官方引入了三个空闲阶段。组成目前的五个阶段, 这样的做法除了保持程序稳定之外,能在此间隙加 hook 回调,我们平常熟悉的 SchedulerBinding.instance.addPostFrameCallback()就是在持续回调之后的一个 hook

graph LR
idle(idle)
transientCallbacks((transientCallbacks))
midFrameMicrotasks(midFrameMicrotasks)
persistentCallbacks((persistentCallbacks))
postFrameCallbacks(postFrameCallbacks)

idle --> transientCallbacks
%% note left of transientCallbacks: Animate
transientCallbacks --> midFrameMicrotasks

midFrameMicrotasks -.-> persistentCallbacks
%% note right of persistentCallbacks: Build、Layout、Paint

persistentCallbacks --> postFrameCallbacks
postFrameCallbacks --> idle

动画

截止目前,已经清楚了整个GUI的图形渲染管线流程. 我们来深入framework层来看,Animation如何实现。在这之前我们要清楚一个概念。

动画,就是通过一定规则映射到时间轴上的一系列静态画面集合.

详细流程

  1. 开始动画,申请调度一帧
  2. onBeginFrame 回调,此处计算最新状态
  3. onDrawFrame 回调,此处根据状态重新渲染画面
  4. 如果动画未结束(取消、时间到),就再次申请调度一帧。
  5. 结束
sequenceDiagram

participant c as Client
participant g as GPU

c ->> c: start()
c ->> g: scheduleFrame()
loop 
  g ->> c: onBeginFrame(timeStamp)
        c ->> c: 根据当前timeStamp,计算最新状态
        g ->> c: onDrawFrame()
        c ->> c: 根据最新状态渲染画面
        alt 当动画未结束时 
          c ->> g: scheduleFrame()
        end
end 

易用性

Flutter 官方为了方便在 Flutter App 中使用动画做了不少易用性方面的工作。方便不同业务场景使用不同的组件来实现动画:

  • 有简单的针对单个组件的动画组件:AnimatedRotationAnimatedScale
  • 有需要更加精确控制其过程的过渡动画:RotationTransitionScaleTransition
  • 如果有更加复杂场景的控制,还可以使用低级API: CustomPaint
  • 对于代码难以描述的还有三方动画框架来解决:FlareLottie

官方也很贴心整理出一张图,来帮助我们选择何种动画:

我在此基础上做了精简:

graph TD
	A((开始))
	B{是否复杂动画?}
  C{是否文本动画?}
	D{是否文本样式动画?}
  E{是否有办法通过代码描述}
  F{是否需要精确控制}
  G{是否对性能敏感}

	

  R1[隐式动画]
  R2[显式动画]
  R3[低级自定义动画]
  R4[三方动画框架]

	A --> B{动画是否复杂}
	B -->|不复杂| C
  C -->|不是| F
  F -->|需要| G
  G -->|不敏感| R2
  F -->|不需要| R1  
  C -->|是| D

	D -->|是| R1
  D -->|不是| R4

	B -->|复杂| E
  E -->|无法| R4
  E -->|可以| R3
  G -->|敏感| R3

三方动画框架

为了加深大家对动画框架的理解,我借助rive创建了一个如下的动画,他可以导出成动画文件,到assert资源中,通过动画文件载入: 20221111113945.gif

实现细节

image.png

  1. AnimationController 描述了动画与时间轴关联的关系。提供了启动、停止等API以供上层调用
  2. Ticker 封装了时间,内部通过与底层的 SchedulerBinding 调度器的 scheduleFrameCallback 来实现对时间的感知
  3. CurvedAnimation 可以支持包装另一个 Animation<double> (通常是 AnimationController )来实现变速。
  4. Animatable<T> 可动对象中最核心的是 evaluate 方法,其声明是:T evaluate(Animation<double> animation),含义为:根据目前动画执行到的进度,估计出当前可动对象的当前值,比如:ColorTween, 颜色可变动对象, 其evaluate 代表了在某个特定进度(由 Animation<double> animation 决定)下该颜色可变动对象的颜色。

CurvedAnimation

贝塞尔曲线

总结

对于动画,感性上将其理解为一系列静态画面的集合即可。最简单粗暴的方式莫过于像GIF这种了。他本身就包含了这些静态画面集合。除此之外基本都是实时计算渲染出来的。