2、TouchGFX触摸架构:事件驱动模型、回调函数与手势事件
好,咱们进入第二章。上一章我们把触摸校准的整个流程串了一遍,这一章得沉下来,看看 TouchGFX 到底是怎么“感知”触摸的。
说白了,就是搞明白两件事:触摸信号怎么从硬件传到你的代码里?以及你的代码怎么区分“点了一下”和“划了一下”?
我刚开始接触 TouchGFX 时,也以为触摸处理就是读个坐标,然后画个点。后来发现,它的架构设计其实挺讲究的。不理解这套机制,你写出来的触摸交互就会很“僵硬”,甚至出现奇怪的 Bug。
核心观点: TouchGFX 的触摸处理,本质是一个“事件驱动”模型。硬件中断产生原始数据,框架将其封装为“事件”,然后通过回调函数分发给你的应用代码。你不需要关心底层轮询,只需要处理事件。
2.1 事件驱动模型:别问,等通知
想象一下,你在办公室里等一个快递。
- 轮询模式:你每隔 5 分钟就去门口看一眼,快递来了没。浪费精力,还容易错过。
- 事件驱动模式:你坐在工位上干活,快递员到了前台,前台打电话通知你:“你的快递到了,下来拿。” 你收到通知,再去处理。
TouchGFX 用的就是第二种。它不会傻傻地一直问“你摸我了没?你摸我了没?”。相反,它让硬件(触摸控制器)在检测到触摸时,主动产生一个中断信号。TouchGFX 的底层驱动捕获这个中断,然后读取坐标数据,打包成一个“事件”,扔进框架的事件队列里。
主循环(Main Loop)每次迭代时,会去检查这个队列。如果有事件,就取出并调用对应的处理函数。没有事件,就继续渲染或休眠。
这样做的好处很明显:CPU 利用率高,响应快,代码结构清晰。 你不需要在 while(1) 里写一堆 if 判断触摸状态。
我的经验: 有一次我在一个低端 MCU 上做项目,CPU 主频只有 72MHz。如果我用轮询方式处理触摸,主循环里全是读 I2C 的代码,界面卡得不行。改成中断+事件队列后,CPU 占用率直接降了 40%。所以,别跟硬件过不去,用事件驱动。
2.2 触摸回调函数:你的代码入口
事件驱动模型讲完了,那你的代码到底写在哪里?答案是:回调函数。
TouchGFX 在 Screen 基类里定义了几个虚函数,专门用来处理触摸事件。你只需要在自己的 Screen 子类里重写它们就行。
最常用的两个回调是:
handleClickEvent(const ClickEvent& evt):处理点击事件。handleGestureEvent(const GestureEvent& evt):处理手势事件。
框架会在主循环中,自动调用这些函数。你不需要手动调用,也不需要关心它们什么时候被调用。你只需要在函数体里写“收到事件后该做什么”。
来看一个最简单的例子:
// MyScreen.hpp
class MyScreen : public Screen {
public:
MyScreen();
virtual ~MyScreen() {}
// 重写触摸回调
virtual void handleClickEvent(const ClickEvent& evt);
virtual void handleGestureEvent(const GestureEvent& evt);
};
// MyScreen.cpp
void MyScreen::handleClickEvent(const ClickEvent& evt) {
// evt.getType() 返回 ClickEvent::RELEASED 或 ClickEvent::PRESSED
// evt.getX() 和 evt.getY() 返回触摸坐标
if (evt.getType() == ClickEvent::PRESSED) {
// 手指按下
// 比如:点亮一个 LED,或者记录起始坐标
led_on();
startX = evt.getX();
startY = evt.getY();
} else if (evt.getType() == ClickEvent::RELEASED) {
// 手指抬起
// 比如:判断是否在按钮区域内
led_off();
if (isInsideButton(evt.getX(), evt.getY())) {
buttonPressed();
}
}
}
void MyScreen::handleGestureEvent(const GestureEvent& evt) {
// evt.getType() 返回 GestureEvent::SWIPE_HORIZONTAL 等
// evt.getVelocity() 返回滑动速度
if (evt.getType() == GestureEvent::SWIPE_HORIZONTAL) {
// 水平滑动
// 比如:切换页面
if (evt.getVelocity() > 0) {
// 向右滑
switchToNextScreen();
} else {
// 向左滑
switchToPrevScreen();
}
}
}
⚠️ 注意: 回调函数是在主循环上下文中被调用的。不要在回调里做耗时操作,比如延时、大循环、文件读写。否则会导致界面卡顿,触摸响应变慢。如果必须做耗时操作,请使用 Application::getInstance()->registerTimerTask() 或消息队列。
2.3 Gesture 与 Click 事件:点一下 vs 划一下
很多新手会混淆这两个事件。我简单解释一下它们的区别:
| 特性 | ClickEvent(点击事件) | GestureEvent(手势事件) |
|---|---|---|
| 触发条件 | 手指按下或抬起 | 手指在屏幕上滑动一段距离后抬起 |
| 事件类型 | PRESSED / RELEASED |
SWIPE_HORIZONTAL / SWIPE_VERTICAL / PAN / ZOOM 等 |
| 数据内容 | 坐标 (x, y) | 方向、速度、距离 |
| 典型用途 | 按钮点击、开关切换 | 页面滑动、列表滚动、图片缩放 |
| 触发顺序 | 先 PRESSED,后 RELEASED | 在 RELEASED 之后触发(如果检测到滑动) |
你可能会问:“为什么要有 GestureEvent?直接用 ClickEvent 记录起始点和结束点,自己算滑动方向不行吗?”
当然可以。但 TouchGFX 帮你做了这件事。它内部有一个手势识别器,会根据连续多个 ClickEvent 的坐标变化,自动判断用户是在“点击”还是在“滑动”。如果是滑动,它还会计算速度和方向,然后封装成 GestureEvent 发给你。
这样做的好处是:你不需要自己写滑动识别算法,而且识别效果更稳定。 比如,用户手指轻微抖动,不会误判为滑动。
避坑指南: 我曾经在一个项目中,同时重写了 handleClickEvent 和 handleGestureEvent。结果发现,每次滑动操作,两个回调都会被调用。先触发 ClickEvent::PRESSED,然后触发 ClickEvent::RELEASED,最后触发 GestureEvent。如果你在 ClickEvent::RELEASED 里做了页面切换,又在 GestureEvent 里做了同样的操作,就会导致页面被切换两次。
解决方案: 在 ClickEvent::RELEASED 里加一个标志位,判断是否已经触发了手势。或者,干脆只用 GestureEvent 处理滑动,ClickEvent 只处理纯粹的点击。
2.4 事件处理的优先级与冲突
还有一个细节值得注意:事件处理的优先级。
TouchGFX 内部有一个默认的规则:
- 手势事件优先于点击事件。 如果框架检测到这是一个滑动操作,它会“吃掉”对应的 ClickEvent::RELEASED,或者延迟触发 GestureEvent,确保你不会同时处理两个冲突的动作。
- 子控件优先于父控件。 如果你在一个容器里放了一个按钮,点击按钮时,按钮的
handleClickEvent会先被调用。如果按钮没有处理,事件才会冒泡到容器。
这个机制在大多数情况下是合理的。但如果你有特殊需求,比如想在一个可滑动的列表里,同时支持点击某个列表项,就需要小心处理。我建议的做法是:
- 在
handleGestureEvent里判断滑动距离。如果距离很小(比如小于 10 像素),就认为是点击,不执行滑动逻辑。 - 或者,在
handleClickEvent里记录点击位置,在handleGestureEvent里判断是否发生了真正的滑动。如果没滑动,再执行点击逻辑。
我的习惯: 我一般会在 handleClickEvent 里只处理“按下”和“抬起”的状态变化,比如改变按钮颜色。真正的业务逻辑(比如切换页面、提交表单)放在 handleGestureEvent 或者一个单独的 onClick() 函数里。这样逻辑更清晰,也更容易调试。
2.5 小结:记住这三句话
好了,这一章的内容就这些。信息量不大,但很重要。我帮你总结一下:
- 事件驱动模型: 别轮询,等通知。硬件中断 -> 驱动封装 -> 队列分发 -> 回调处理。
- 回调函数:
handleClickEvent处理点击,handleGestureEvent处理手势。别在回调里做耗时操作。 - Click vs Gesture: 点击是“点一下”,手势是“划一下”。框架会自动识别,你只需要处理结果。