2、TouchGFX触摸架构:事件驱动模型、回调函数与手势事件

好,咱们进入第二章。上一章我们把触摸校准的整个流程串了一遍,这一章得沉下来,看看 TouchGFX 到底是怎么“感知”触摸的。

说白了,就是搞明白两件事:触摸信号怎么从硬件传到你的代码里?以及你的代码怎么区分“点了一下”和“划了一下”?

我刚开始接触 TouchGFX 时,也以为触摸处理就是读个坐标,然后画个点。后来发现,它的架构设计其实挺讲究的。不理解这套机制,你写出来的触摸交互就会很“僵硬”,甚至出现奇怪的 Bug。

核心观点: TouchGFX 的触摸处理,本质是一个“事件驱动”模型。硬件中断产生原始数据,框架将其封装为“事件”,然后通过回调函数分发给你的应用代码。你不需要关心底层轮询,只需要处理事件。

2.1 事件驱动模型:别问,等通知

想象一下,你在办公室里等一个快递。

  • 轮询模式:你每隔 5 分钟就去门口看一眼,快递来了没。浪费精力,还容易错过。
  • 事件驱动模式:你坐在工位上干活,快递员到了前台,前台打电话通知你:“你的快递到了,下来拿。” 你收到通知,再去处理。

TouchGFX 用的就是第二种。它不会傻傻地一直问“你摸我了没?你摸我了没?”。相反,它让硬件(触摸控制器)在检测到触摸时,主动产生一个中断信号。TouchGFX 的底层驱动捕获这个中断,然后读取坐标数据,打包成一个“事件”,扔进框架的事件队列里。

主循环(Main Loop)每次迭代时,会去检查这个队列。如果有事件,就取出并调用对应的处理函数。没有事件,就继续渲染或休眠。

这样做的好处很明显:CPU 利用率高,响应快,代码结构清晰。 你不需要在 while(1) 里写一堆 if 判断触摸状态。

TouchGFX 事件驱动模型流程图 触摸控制器 (硬件中断产生) HAL 驱动层 (读取坐标,封装事件) 事件队列 (FIFO 存储) 应用回调 (你的代码) 💡 关键点: 1. 硬件中断是起点,驱动层负责“翻译”。 2. 事件队列解耦了硬件和业务逻辑。

我的经验: 有一次我在一个低端 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 内部有一个默认的规则:

  1. 手势事件优先于点击事件。 如果框架检测到这是一个滑动操作,它会“吃掉”对应的 ClickEvent::RELEASED,或者延迟触发 GestureEvent,确保你不会同时处理两个冲突的动作。
  2. 子控件优先于父控件。 如果你在一个容器里放了一个按钮,点击按钮时,按钮的 handleClickEvent 会先被调用。如果按钮没有处理,事件才会冒泡到容器。

这个机制在大多数情况下是合理的。但如果你有特殊需求,比如想在一个可滑动的列表里,同时支持点击某个列表项,就需要小心处理。我建议的做法是:

  • 在 handleGestureEvent 里判断滑动距离。如果距离很小(比如小于 10 像素),就认为是点击,不执行滑动逻辑。
  • 或者,在 handleClickEvent 里记录点击位置,在 handleGestureEvent 里判断是否发生了真正的滑动。如果没滑动,再执行点击逻辑。

我的习惯: 我一般会在 handleClickEvent 里只处理“按下”和“抬起”的状态变化,比如改变按钮颜色。真正的业务逻辑(比如切换页面、提交表单)放在 handleGestureEvent 或者一个单独的 onClick() 函数里。这样逻辑更清晰,也更容易调试。

2.5 小结:记住这三句话

好了,这一章的内容就这些。信息量不大,但很重要。我帮你总结一下:

  • 事件驱动模型: 别轮询,等通知。硬件中断 -> 驱动封装 -> 队列分发 -> 回调处理。
  • 回调函数: handleClickEvent 处理点击,handleGestureEvent 处理手势。别在回调里做耗时操作。
  • Click vs Gesture: 点击是“点一下”,手势是“划一下”。框架会自动识别,你只需要处理结果。