使用 TFT 显示屏进行滑动姿势






4.67/5 (2投票s)
使用TFT触摸屏和兼容Arduino的设备实现向左/右/上/下滑动
引言
与简单的触摸输入相比,手势可以提供更复杂和更具表现力的输入。通过手势,用户可以通过以特定方式“绘制”在屏幕上来指示各种操作。在许多小型物联网设备上,触摸显示屏可能是与之直接通信的唯一方式,此时手势变得极为有用。
不幸的是,关于为这些小型设备实现手势的信息实在太少了。
在本文中,我旨在向您展示如何使用连接到兼容Arduino设备的TFT触摸显示屏来实现简单的滑动手势。
必备组件
- 一个兼容Arduino的设备。我在本次演示中使用的是ESP32,但通过一些接线修改,也可以使用其他设备。
- 一个TFT显示设备。代码是为RA8875编写的,但通过最小的代码修改,也可以用于ILI9341等其他显示屏。
- Arduino IDE和相应的开发板管理器。
- 通常的跳线和原型板。
概念化这个混乱的局面
在编码方面,我们需要解决许多问题。读取触摸事件对于这些设备来说可能有点棘手——哦,没有包罗万象的框架来处理细节的编码是多么令人欣喜!我们还需要具备各种功能的计时能力。此外,我们还需要实际实现手势计算本身。
代码计时
让我们从一个简单的模式开始——无delay()
的定时器。delay()
在loop()
中使用可能会有问题,因为它会阻塞,导致在等待时其他活动会停止。有时,我们需要以固定的间隔运行某些内容,同时允许loop()
的其余部分继续运行。我们可以通过一个全局变量来存储时间戳,并结合millis()
函数来实现这一点。
#define INTERVAL 100 // <sup>1</sup>/<sub>10</sub> of a second
// set to 0 if you want it to fire
// immediately the first time
uint32_t _timeoutTS = millis();
...
void loop() {
if(millis()-_timeoutTS>INTERVAL) {
_timeoutTS = millis();
// do work
...
}
...
}
我们的做法是使“执行工作”部分的代码每秒只运行10次,而不是在每次调用loop()
时都运行。这比使用delay(100);
要好,因为后者会减慢整个loop()
例程的速度,而不仅仅是代码的这一部分。我们稍后将使用这种技术,所以请记住它,因为这就是我们解决计时问题的方法。
读取触摸事件
请注意,这可能因设备而异。请查阅您库的示例代码,了解如何读取触摸事件。一个或多或少普遍存在的问题是,设备无法像loop()
调用那样快速地处理触摸事件。SPI总线速度不如我们的CPU。为了应对这种情况,我们只在固定的时间间隔内处理触摸事件,方法如前所述。不过,我们将提供一个隐藏细节的例程。它被称为tryGetTouchEvent()
,它将填充一个tsPoint_t
结构体,并在发生触摸事件时返回true
。您的设备如何做到这一点是您自己的事情,但我将为您展示RA8875的代码。
请注意,大多数此类屏幕都需要校准。它们从触摸事件返回的点与屏幕上的点不匹配,必须进行偏移。幸运的是,我们不需要精确的物理坐标。我们只需要知道坐标之间的相对关系,而不是设备的物理坐标。我们在这里不讨论校准。
计算手势
对于任何手势计算,我们需要知道设备何时被触摸以及何时释放触摸。为此,我们在设备可接受的间隔(这里是每秒1/10)轮询触摸事件。我们跟踪设备是否被触摸以及当前是否被触摸。这一点很重要。我们需要这些边缘条件来确定何时有人将手指放在屏幕上,然后一直保持直到他们释放。基本上,我们这样做:
...
_touched = tryGetTouchEvent(&pt);
if(_touched!=_touchedOld) {
if(_touched) {
// touched event handling code
} else {
// released event handling code
}
}
_touchedOld=_touched;
这应该相当直接。我们只是检查触摸状态是否发生变化,如果发生变化,我们就确定它是触摸还是释放。
对于复杂的手势,我们需要一个数组来存储触摸显示屏时“绘制”的点,并在触摸释放时处理它们。但是,由于我们只做基本的滑动,我们可以大大简化代码。
我们只需要跟踪用户第一次触摸显示屏的点以及他们释放的点。然后,我们比较x和y值的差异。较大的差值决定了我们是垂直滑动(y差值最大)还是水平滑动(x差值最大)。
我们还需要确保他们实际滑动的距离足够长才能被识别,但这很简单。我们只需要确保x或y坐标的差值足够长。我们垂直和水平方向的阈值不同,因为屏幕不是方形的。
当我们进入代码部分时,我们将看到这一切是如何实现的。
构建这个大杂烩
连接这个“大杂烩”
请注意,这取决于硬件。思路是将显示屏连接到设备的**主SPI总线**,然后连接任何额外的引脚,如RST。在带有RA8875的ESP32上,接线如下:
RA8875 VIN ➜ ESP32 VIN (+5vdc)
RA8875 GND ➜ ESP32 GND
RA8875 3Vo ➜ N/C
RA8875 LITE ➜ N/C
RA8875 SCK ➜ ESP32 GPIO18
RA8875 MISO ➜ ESP32 GPIO19
RA8875 MOSI ➜ ESP32 GPIO23
RA8875 CS ➜ ESP32 GPIO5
RA8875 RST ➜ ESP32 GPIO15
RA8875 WAIT ➜ N/C
RA8875 INT ➜ N/C
RA8875 Y+ ➜ N/C
RA8875 Y- ➜ N/C
RA8875 X- ➜ N/C
RA8875 X+ ➜ N/C
编写这个混乱的程序
终于到了代码部分。由于我们已经探讨了这些技术,我们将通过查看下面的整个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 <math.h>
#include <Adafruit_RA8875.h>
#include <Adafruit_GFX.h>
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);
}
考虑到我们之前讨论的内容,其中大部分应该相当清楚。我们运行两个“定时器”,一个用于触摸事件,一个用于清除文本。我们在触摸释放时通过计算差值来处理触摸事件,如前所述。当我们滑动时,我们将滑动命令写入显示屏,然后启动“定时器”来清除文本。
Bug
文本未正确居中。我可能错误地使用了getTextBounds()
,或者它在我的设置中实际上不起作用。
历史
- 2020年12月4日 - 首次提交