4. TouchGFX核心概念

做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 生命周期流转图

为了让你看得更清楚,我画了张图。这张图我每次带新人时都会拿出来讲一遍。

Screen / View 生命周期流转 Screen 创建 setupScreen() 运行中(Tick循环) tearDownScreen() Screen 销毁 注意 不要在 setupScreen 里做 耗时操作,会影响首帧 显示速度。

4.2 Model与ModelListener:数据同步的“桥梁”

你想想看,如果后台数据变了,界面怎么知道?难道要View自己轮询?那代码就乱套了。

TouchGFX用了一个很优雅的设计:Model 和 ModelListener。

  • Model:全局唯一的“数据仓库”。它不关心谁在显示,只负责存数据和发通知。
  • ModelListener:一个接口(抽象类)。每个View如果想接收数据变化通知,就必须实现这个接口。

4.2.1 数据是怎么“推”到界面的?

流程其实很简单,就三步:

  1. 数据源变了(比如串口收到新数据、定时器超时)。
  2. Model 更新数据,然后调用 modelListener->xxxChanged()。
  3. 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到来,框架会做三件事:

  1. 调用所有活动View的 handleTickEvent()。
  2. 更新所有正在运行的动画(比如 MoveAnimator、FadeAnimator)。
  3. 触发一次屏幕刷新(如果画面有变化)。

你想想看,这就是一个完整的“感知-思考-反应”循环。

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的动画框架就通了。

TouchGFX 动画核心关系图 Model ModelListener Screen View Tick 驱动 动画控件 通知数据变化 回调更新UI 生命周期管理 驱动 handleTickEvent 驱动动画更新 一句话总结 Model 存数据 -> ModelListener 通知 -> View 响应 -> Tick 驱动动画 -> 屏幕刷新 整个链条,缺一环动画就跑不起来。

嗯,这一章的内容就到这儿。你把这几个概念理清楚,后面写动画就会顺手很多。