4、动画核心类:Screen与View的生命周期、Model与ModelListener的数据同步、Animating与Tick机制
做TouchGFX动画,光会调API可不够。你得搞清楚,这些动画到底是在什么时机跑的?数据是怎么从后台传到界面的?为什么有时候动画卡一下,有时候又好好的?
这一章,我们就来啃这几个核心问题。说白了,就是搞懂TouchGFX的“心脏”怎么跳。
4.1 Screen与View:一对“双胞胎”的生死
刚接触TouchGFX时,我总搞不清Screen和View到底啥区别。后来踩坑多了才明白,它们就像一对双胞胎,形影不离,但分工明确。
- Screen:负责“生命周期”和“数据”。它是个抽象层,管理着这个界面的生老病死。
- View:负责“显示”和“交互”。它才是你放控件、画动画的地方。
一个Screen对应一个View。你切换界面时,其实是Screen在切换,然后它带着自己的View一起上场。
4.1.1 生命周期回调:什么时候该干什么事?
每个Screen和View都有几个关键的回调函数。我习惯把它们叫做“生命周期的四个节点”。
| 回调函数 | 触发时机 | 我一般用它做什么 |
|---|---|---|
setupScreen() |
界面刚被创建,即将显示 | 初始化变量、设置控件初始状态、启动非动画类的定时器 |
tearDownScreen() |
界面即将被销毁 | 清理资源、停止定时器、保存临时状态 |
handleTickEvent() |
每个“滴答”触发一次 | 驱动自定义动画、轮询数据、做帧同步计算 |
handleKeyEvent() |
物理按键按下时 | 处理硬件按键,比如返回、菜单 |
setupScreen() 里直接启动复杂动画。因为这时候View可能还没完全布局好。我会在 setupScreen() 里设个标志位,然后在第一个 handleTickEvent() 里再启动动画。这样能避免一些奇怪的闪烁问题。
4.1.2 生命周期流转图
为了让你看得更清楚,我画了张图。这张图我每次带新人时都会拿出来讲一遍。
4.2 Model与ModelListener:数据同步的“桥梁”
你想想看,如果后台数据变了,界面怎么知道?难道要View自己轮询?那代码就乱套了。
TouchGFX用了一个很优雅的设计:Model 和 ModelListener。
- Model:全局唯一的“数据仓库”。它不关心谁在显示,只负责存数据和发通知。
- ModelListener:一个接口(抽象类)。每个View如果想接收数据变化通知,就必须实现这个接口。
4.2.1 数据是怎么“推”到界面的?
流程其实很简单,就三步:
- 数据源变了(比如串口收到新数据、定时器超时)。
- Model 更新数据,然后调用
modelListener->xxxChanged()。 - View 收到回调,在回调里更新控件或启动动画。
核心要点: Model 不直接操作UI。它只负责“喊一嗓子”。谁在听,谁就自己处理。这叫做“观察者模式”。
4.2.2 我曾经踩过的坑:跨界面数据同步
有一次我做项目,需要在界面A切换到界面B时,把A的某个状态带过去。我直接在Model里存了个全局变量,然后在B的 setupScreen() 里去读。
结果呢?有时候能读到,有时候读不到。查了半天才发现,Model 的更新时机和 View 的创建时机没对上。
后来我学乖了:不要在 setupScreen() 里依赖另一个界面的实时数据。正确的做法是:
- 在Model里维护一个“待同步队列”。
- 界面B的
setupScreen()只做初始化,然后在第一个handleTickEvent()里从Model拉取数据。
4.3 Animating与Tick机制:动画的“心跳”
TouchGFX的动画,不是靠死循环跑的。它有一个核心的“心跳”——Tick。
4.3.1 Tick是什么?
简单说,Tick就是一个周期性的定时中断。默认情况下,TouchGFX的Tick是16ms一次(也就是约60Hz)。每次Tick到来,框架会做三件事:
- 调用所有活动View的
handleTickEvent()。 - 更新所有正在运行的动画(比如
MoveAnimator、FadeAnimator)。 - 触发一次屏幕刷新(如果画面有变化)。
你想想看,这就是一个完整的“感知-思考-反应”循环。
4.3.2 Animating状态:动画的“生命周期”
每个动画控件,内部都有一个状态机。我把它总结成四个状态:
| 状态 | 说明 | 我常用的场景 |
|---|---|---|
| Idle | 空闲,没有动画在运行 | 控件静止时 |
| Animating | 动画正在执行中 | 控件正在移动、淡入淡出时 |
| Delay | 等待延迟时间结束 | 实现“先停一下,再动”的效果 |
| Finished | 动画执行完毕 | 触发回调,做后续处理 |
handleTickEvent() 里做了太多计算,导致一次Tick处理时间超过16ms,那动画就会掉帧。我曾经在一个项目里,因为 handleTickEvent() 里做了大量的浮点数运算,结果动画一卡一卡的。后来我把运算放到后台任务里,只在Tick里取结果,问题就解决了。
4.3.3 自定义动画:自己控制每一帧
有时候,框架自带的动画满足不了需求。比如,我想做一个“弹性缓动”效果。这时候,就需要自己动手了。
我一般会在 handleTickEvent() 里这样写:
void MyView::handleTickEvent()
{
if (myAnimationRunning)
{
// 计算当前进度
currentTick++;
float progress = (float)currentTick / totalTicks;
// 应用缓动函数(比如弹性缓动)
float easedProgress = applyElasticEasing(progress);
// 更新控件位置
myButton.moveTo(startX + (endX - startX) * easedProgress,
startY + (endY - startY) * easedProgress);
// 判断是否结束
if (currentTick >= totalTicks)
{
myAnimationRunning = false;
// 触发结束回调
animationFinished();
}
}
// 别忘了调用父类
View::handleTickEvent();
}
这段代码的核心就是:用Tick计数来控制进度,用缓动函数来改变运动曲线。
4.4 本章小结:一张图看懂核心关系
最后,我把这一章的核心逻辑总结成一张图。你把这图记住,TouchGFX的动画框架就通了。
嗯,这一章的内容就到这儿。你把这几个概念理清楚,后面写动画就会顺手很多。