在物联网(IoT)设备中,触摸屏往往成为与设备直接交互的唯一方式,而手势识别则为这种交互提供了极大的便利。然而,对于这些小型设备来说,实现手势识别的信息并不丰富。本文旨在展示如何使用Arduino兼容设备和TFT触摸屏实现简单的滑动手势识别。
为了实现这一功能,需要以下设备和软件:
在编写代码之前,需要解决几个问题。读取触摸事件可能会有些棘手,因为没有全面的框架来处理这些细节。还需要为不同的目的实现计时功能。此外,还需要实际实现手势计算。
让从简单的模式开始——无延迟的计时器。在loop()函数中使用delay()可能会导致问题,因为它会阻塞,从而可能导致其他活动在等待时停止。有时需要在允许loop()继续运行的同时,定时运行某些操作。可以通过一个全局变量来记录时间戳,并使用millis()函数来实现这一点:
#define INTERVAL 100 // 1/10秒
uint32_t _timeoutTS = millis();
void loop() {
if (millis() - _timeoutTS > INTERVAL) {
_timeoutTS = millis();
// 执行操作
}
}
这样做的目的是让"执行操作"部分每秒只运行10次,而不是每次调用loop()时都运行。这比使用delay(100)更可取,因为后者会减慢整个loop()过程,而不仅仅是那一部分代码。将在后面使用这种技术,所以请记住,这是解决计时问题的方法。
请注意,这可能因设备而异。请查看库示例代码,了解如何读取触摸事件。一个几乎普遍存在的问题是,设备处理触摸事件的速度没有loop()调用的速度快。SPI总线的速度没有CPU快。为了解决这个问题,只使用前面概述的技术,在定时间隔内处理触摸事件。然而,要做的是提供一个隐藏细节的单一例程。它被称为tryGetTouchEvent(),它将填充一个tsPoint_t结构,并在发生触摸事件时返回true。设备是如何做到这一点的并不重要,但将向展示RA8875的代码。
请注意,大多数这些屏幕需要校准。它们从触摸事件返回的点与屏幕上的点不匹配,必须进行偏移。幸运的是,不需要精确的物理坐标。只需要知道相对于彼此的坐标,而不是物理设备。不会在这里涵盖校准。
对于任何手势计算,需要知道设备何时被触摸以及何时触摸被释放。为此,以设备能够处理的间隔轮询触摸事件——这里为1/10秒。跟踪设备是否被触摸以及它当前是否被触摸。这很重要。需要这些边缘条件来确定当有人将手指放在屏幕上时,然后继续直到他们释放它。基本上,要做的就是这样:
_touched = tryGetTouchEvent(&pt);
if (_touched != _touchedOld) {
if (_touched) {
// 触摸事件处理代码
} else {
// 释放事件处理代码
}
}
_touchedOld = _touched;
这应该相当简单。只是看看触摸状态是否发生了变化,如果是的话,确定它是触摸还是释放。
对于复杂的手势,需要保留一个数组,记录在触摸显示屏时"绘制"的点,并在触摸释放时处理它们。然而,由于正在做基本的滑动,可以走一个重要的捷径,大大简化代码。
只需要跟踪他们第一次触摸显示屏的点,以及他们释放的点。然后比较x和y值的差异。最大的差异决定了垂直滑动(y差异最大),还是水平滑动(x差异最大)。
还需要确保他们实际上滑动得足够远才能注册,但这很简单。所要做的就是确保x或y坐标的差异足够长。有垂直和水平的不同阈值,因为屏幕不是正方形的。
当看到代码时,会看到所有这些在行动中。
请注意,这是硬件特定的。想法是将显示屏连接到设备的主SPI总线,然后连接任何额外的引脚,如RST。在ESP32上使用RA8875看起来像这样:
最后,来看代码。由于已经探讨了技术,将通过查看整个tft_swipe.ino文件来了解它们在行动中的所有内容:
// touch configuration
#define TOUCH_INTERVAL 100
#define TOUCH_THRESHOLD_X 200
#define TOUCH_THRESHOLD_Y 120
// time before text indicator disappears
#define TEXT_TIMEOUT 2000
// hardware configuration
#define RA8875_CS 5
#define RA8875_RST 15
#include
#include
#include
void drawCentered(char * sz);
bool tryGetTouchEvent(tsPoint_t * point);
Adafruit_RA8875 tft = Adafruit_RA8875(RA8875_CS, RA8875_RST);
// for handling touched/release events
bool _touchedOld = false;
bool _touched = _touchedOld;
uint32_t _touchTS = 0;
// first and last points touched
tsPoint_t _touchFirst;
tsPoint_t _touchLast;
// the timestamp for making text go away
uint32_t _textTimeoutTS = 0;
void setup() {
Serial.begin(115200);
// init the display
if (!tft.begin(RA8875_800x480)) {
Serial.println(F("RA8875 Not Found!"));
while (1);
}
tft.displayOn(true);
tft.GPIOX(true);
// Enable TFT - display enable tied to GPIOX
tft.PWM1config(true, RA8875_PWM_CLK_DIV1024);
// PWM output for backlight
tft.PWM1out(255);
tft.touchEnable(true);
tft.fillScreen(RA8875_WHITE);
}
void loop() {
if (millis() - _touchTS > TOUCH_INTERVAL) {
_touchTS = millis();
// process our touch events
tsPoint_t pt;
_touched = tryGetTouchEvent(&pt);
if (_touched)
_touchLast = pt;
if (_touched != _touchedOld) {
if (_touched) {
_touchFirst = pt;
} else {
pt = _touchLast;
// compute differences
int32_t dx = pt.x-_touchFirst.x;
int32_t dy = pt.y-_touchFirst.y;
uint32_t adx=abs(dx);
uint32_t ady=abs(dy);
// swipe horizontal
if (adx > ady && adx > TOUCH_THRESHOLD_X) {
if (0 > dx) {
// swipe right to left
drawCentered("Right to left");
_textTimeoutTS=millis();
} else {
// swipe left to right
drawCentered("Left to right");
_textTimeoutTS=millis();
}
// swipe vertical
} else if (ady > adx && ady > TOUCH_THRESHOLD_Y) {
if (0 > dy) {
// swipe bottom to top
drawCentered("Bottom to top");
_textTimeoutTS=millis();
} else {
// swipe top to bottom
drawCentered("Top to bottom");
_textTimeoutTS=millis();
}
}
}
}
_touchedOld = _touched;
}
// make the text we displayed disappear, if necessary
if (_textTimeoutTS && millis() - _textTimeoutTS > TEXT_TIMEOUT) {
_textTimeoutTS = 0;
tft.fillScreen(RA8875_WHITE);
}
}
// read the TFT touch
bool tryGetTouchEvent(tsPoint_t * point) {
uint16_t x, y;
tft.touchRead(&x, &y);
delay(1);
if (tft.touched()) {
tft.touchRead(&x, &y);
point->x = x;
point->y = y;
return true;
}
return false;
}
// draws text centered on the display
// BUG: getTextBounds() doesn't seem
// to work right for computing width
void drawCentered(const char *sz) {
int16_t x, y;
uint16_t w, h;
tft.textMode();
tft.setTextWrap(false);
tft.setTextSize(1);
tft.textEnlarge(1);
tft.getTextBounds(sz, 0, 0, &x, &y, &w, &h);
tft.textTransparent(RA8875_BLACK);
tft.textSetCursor((tft.width() - w) / 2, (tft.height() - h) / 2);
tft.textWrite(sz);
}
鉴于在前面的部分已经涵盖了这些内容,大部分内容应该相当清楚。运行两个"计时器",一个用于触摸事件,一个用于清除文本。在释放时通过计算差异来处理触摸事件。当滑动时,将滑动命令写入显示屏,然后启动"计时器"来清除文本。