渲染
现代操作系统GUI渲染流程API:
classDiagram
class Window {
<<abstract>>
+scheduleFrame()
+setOnFrame(FrameCallback onFrame)
+render(Frame frame)
}
系统提供的核心API,其目的是希望上次这样来调用:
scheduleFrame()通过此函数来向系统申请调度帧。setOnFrame(FrameCallback onFrame)系统将在下一帧渲染之前通过此回调来告知调用者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层渲染流程图
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如何实现。在这之前我们要清楚一个概念。
动画,就是通过一定规则映射到时间轴上的一系列静态画面集合.
详细流程
- 开始动画,申请调度一帧
onBeginFrame回调,此处计算最新状态onDrawFrame回调,此处根据状态重新渲染画面- 如果动画未结束(取消、时间到),就再次申请调度一帧。
- 结束
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 中使用动画做了不少易用性方面的工作。方便不同业务场景使用不同的组件来实现动画:
- 有简单的针对单个组件的动画组件:
AnimatedRotation、AnimatedScale - 有需要更加精确控制其过程的过渡动画:
RotationTransition、ScaleTransition - 如果有更加复杂场景的控制,还可以使用低级API:
CustomPaint - 对于代码难以描述的还有三方动画框架来解决:
Flare、Lottie
官方也很贴心整理出一张图,来帮助我们选择何种动画:
我在此基础上做了精简:
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资源中,通过动画文件载入:
实现细节
AnimationController描述了动画与时间轴关联的关系。提供了启动、停止等API以供上层调用Ticker封装了时间,内部通过与底层的SchedulerBinding调度器的scheduleFrameCallback来实现对时间的感知CurvedAnimation可以支持包装另一个Animation<double>(通常是AnimationController)来实现变速。Animatable<T>可动对象中最核心的是evaluate方法,其声明是:T evaluate(Animation<double> animation),含义为:根据目前动画执行到的进度,估计出当前可动对象的当前值,比如:ColorTween, 颜色可变动对象, 其evaluate代表了在某个特定进度(由Animation<double> animation决定)下该颜色可变动对象的颜色。
CurvedAnimation
总结
对于动画,感性上将其理解为一系列静态画面的集合即可。最简单粗暴的方式莫过于像GIF这种了。他本身就包含了这些静态画面集合。除此之外基本都是实时计算渲染出来的。