TouchGFX按键控制光圈移动:从GPIO到动画的完整实现

对于复杂交互的前端页面架构的思考(干货) 项目背景effevo致力于为用户提供最方便的云端任务管理工具,任务管理过程中主要涉及的就是数据的CRUD,并能够将这种数据变化实时的反馈出来;有些地方(比如甘特图)需要比较复杂的dom操作和交互;有些页面(比如首页)有SEO的要求。由于系统的这种复杂性决定不可能由单一的技术来实现整体的系统。 本文主要对Effevo 前端的整体架构做了一个简单的介绍,给大家提供一种前端架构的解决方案。其中也还存在着 阅读详情

最近在折腾TouchGFX的时候,做了一个很典型的入门级小应用:界面设计里放一个光圈,用板子上的物理按键控制它在屏幕上移动。听起来挺简单,但真正做下来发现,它其实把TouchGFX里最常用的几个基础能力全部串起来了——控件布局、事件分发、按键注入、动画调度、坐标换算。而且这类“焦点移动”的交互在真实产品里非常常见,比如遥控器切台时的选中框、洗衣机面板上参数项的高亮圈、甚至是菜单里的光标移动,都是同一个套路。这篇文章就把我做这个“按键控制光圈移动”项目的完整过程记录下来,从需求拆解到环境搭建、界面制作、按键逻辑,再到调试踩坑,给想从“跑通示例”过渡到“自己写逻辑”的朋友一个能直接参考的路径。

1. 动手之前:先把“光圈移动”这件事想清楚

1.1 核心需求拆解:这不是一个“画圆”的活

先别急着开软件,把题目拆开看。“TouchGFX 简单界面设计_按键控制光圈移动”里面其实包含四层任务:

第一层,界面设计。需要在TouchGFX Designer里搭出一个屏幕布局,背景、控件、排版,让画面看起来像个正经的UI,不是随便堆几个框。

第二层,光圈。所谓“光圈”,在GUI里本质上就是一个高亮指示器。做的人通常有两种选择:要么用Circle控件直接绘制一个空心圆环,要么用一张PNG贴图当光圈素材。两者的做法我后面都会讲到,它们踩的坑还不一样。

第三层,按键控制。这里的按键不是触摸屏,而是物理按键,也就是GPIO输入。TouchGFX本身专注GUI渲染,物理按键的读取要靠STM32的HAL库去完成,然后再把按键事件“喂”给GUI层,这一步是很多新手卡住的地方。

第四层,光圈移动。移动不是“瞬间跳过去”,而是要做一个视觉上流畅的动画。你在界面上看到的是一个圆圈平滑地从一个目标点滑到另一个目标点,这背后要么用TouchGFX自带的动画机制,要么自己写一个简单的插值逻辑。

把这几层拆出来之后,整个项目的脉络就清楚了: 读GPIO -> 转换成按键事件 -> 传给GUI -> GUI里更新当前选中项 -> 控制光圈做位移动画

1.2 技术选型:为什么要选TouchGFX

很多朋友看到“界面设计”和“让控件动起来”的第一反应可能是LVGL、emWin,或者干脆自己在STM32上裸写2D库。我这次选TouchGFX,原因很实在:

一是TouchGFX和STM32的契合度非常高。它本身就是ST在推广HMI方案时的重头产品,在F4、F7、H7这些带屏幕控制器的芯片上有硬件加速,跑起来帧率稳定,动画不容易掉链子。

二是TouchGFX Designer这个可视化工具确实好用。拖拖拽拽就能完成界面布局,交互可以以“Interactions”的方式配置,生成C++代码,比纯手写布局效率高一大截。

三是MVP架构对写业务逻辑挺友好。TouchGFX把屏幕分成Model、View、Presenter三层,界面展示和业务处理分开,虽然学习要花一点时间,但一旦理顺了,在整个项目里找代码、改逻辑都非常清晰。

这里我也把主流方案放在一起对比一下,方便不同偏好的朋友参考:

对比项 TouchGFX LVGL emWin
主要硬件生态 STM32生态最顺 各种MCU通吃,开源免费 老牌商用GUI,授权模式成熟
开发方式 Designer可视化拖拽 + C++代码 纯代码为主,配合SquareLine Studio AppWizard + 纯C代码
动画能力 内置多种动画,帧率稳定,硬件加速好 有动画API,自由度高 有动画支持,偏传统
学习曲线 Designer易上手,MVP模式需要适应 轻量灵活,但UI调试不如拖拽直观 文档多,上手成本中等
资源占用 需要一定Flash/RAM,建议带LTDC和SDRAM 占用低,适合资源紧张的MCU 占用低,优化空间大

如果你手头只有一个小资源MCU、不需要复杂动画,LVGL或者emWin也很香。但如果你用的是ST芯片,手头又有带屏的评估板,TouchGFX确实是一条阻力最小的路。

1.3 项目最终的效果定义

再把这个项目要做成的效果描述清楚,避免做着做着跑偏:

屏幕尺寸我以480x272为例(这在很多4.3寸屏上很常见)。屏幕上放了4个目标点,分布在2x2的网格上,每个目标点可以理解成一个“槽位”。初始状态下,光圈停留在第一个目标点上。按下物理按键(上/下/左/右),光圈就朝对应方向移动一格。移动到边界时,再按相同方向则保持不动,光圈不会跑到屏幕外面去。

视觉上,光圈是一个带有明显颜色的圆环,目标点是四个灰色圆形或者方块,光圈移动时是平滑滑过去的,不是“瞬移”。这就够了,既覆盖了核心知识点,也不会把项目做复杂。

2. 环境准备:把TouchGFX开发链路跑起来

2.1 软件和硬件清单

我先列一下我自己用的环境,你可以根据手头东西对照着来:

  • 硬件:STM32F429-DISCO开发板,板载4.3寸480x272屏。这块板是TouchGFX官方示例经常用的老面孔,资料多,出了任何问题都能搜到方案。如果你用F746G-DISCO或者自己画的板子,后面讲的原理都是一样的。
  • 软件:STM32CubeMX(用来生成MCU初始化代码)、TouchGFX Designer(界面设计工具)、STM32CubeIDE或者IAR/Keil(编译烧录)。我这边用STM32CubeIDE,因为和CubeMX、TouchGFX Designer配合起来不需要来回切工程格式。

物理按键的问题说一下:F429-DISCO板载的用户按键只有一个,而我需要四个方向键,所以我在面包板上外接了三个按键,分别接到几个空闲GPIO上。如果你手上是别的板子,按键不够的话可以外接,或者临时把其中两个方向先做成“移动下一个”,只用一个按键来演示。

2.2 两种方式创建TouchGFX工程

创建工程无非两条路,我建议按你自己的情况选:

第一条路,用CubeMX创建。在CubeMX里选好自己的MCU型号,配置好时钟、LCD接口、SDRAM(如果用外部显存的话),然后在“Middleware and Software Packs”里勾选TouchGFX,生成一个带TouchGFX依赖的工程,再用TouchGFX Designer打开这个工程的 .touchgfx 文件。这条路适合从零开始做产品、硬件方案比较确定的情况。

第二条路,直接用TouchGFX Designer新建Demo工程。Designer里自带很多模板,比如“Empty Application”,直接基于这个模板改就行。生成的工程代码是完整的,编译烧录就能看到界面,代码里的Screen结构、MVP框架都是现成的。我看很多教程都喜欢用这条路,因为省去配置底层硬件的步骤,能更快把注意力放在界面逻辑上。

我这次用的是第二条路,因为目的是研究界面和按键逻辑,底层驱动不是重点。如果你用的是自己的板子,需要注意LCD的初始化代码和TouchGFX的HAL层要和你板子匹配,不然屏幕上就是白屏或者花屏。

2.3 TouchGFX工程里的目录结构:哪些代码能改

TouchGFX生成的工程初看会有点懵,目录比普通STM32工程多不少。但只要记住一个原则,就清楚了: generated目录下的东西不要手改,手写的业务逻辑都放在gui目录下

以4.x版本为例,核心目录大致是这样:

  • generated/ :TouchGFX Designer自动生成的所有界面代码,包括每个Screen的基类、字体、图片缓存映射。这里面的代码在每次Designer重新生成之后会被覆盖。
  • gui/ :你的主战场。每个Screen下面会有 ScreenView ScreenPresenter ScreenViewBase 这些文件。其中 ViewBase 是生成代码, View 是你可以自由写逻辑的类。设计器里新增控件后,对应的成员变量会生成到ViewBase里,你在View里只要直接使用这些控件就行。
  • target/ :平台相关的HAL层代码,比如触摸驱动、显示缓冲管理。如果你的板子显示器需要特殊时序,改这里的代码。

刚开始最容易犯的错误就是在ViewBase里手动添加成员变量或者改已有逻辑,结果Designer一保存就把你的改动冲掉了。更稳妥的做法是,在Designer里把所有控件都放好,然后切到IDE里,在 Screen1View.hpp 和自己的 Screen1View.cpp 里写扩展逻辑。这样重新生成也不会丢。

3. 界面制作:在Designer里把“光圈”安好家

3.1 创建空应用并设置背景

打开TouchGFX Designer,新建一个“Empty Application”,模板选择不带复杂示例的空白屏。分辨率我设置成480x272,屏幕背景我建议用深色,比如深灰或者纯黑。为什么?因为光圈是亮色的圆环,深色背景下对比度高,移动效果看得非常清楚,调试起来也直观。

具体操作:在右上角的Screen属性面板里找到 Canvas 配置,调整背景色。这里多说一句,Designer里的背景色和实际运行可能略有色差,但大体是准的,不影响开发。

背景色设置好之后,从左侧控件面板里拖一个 Box 放到屏幕上,把它铺满整个界面,这个Box就是背景板。为什么要用Box而不是直接用Screen背景?因为Box可以作为一个普通控件参与图层管理,后面如果想在背景上叠加半透明遮罩或者渐变效果,操作空间更大。

3.2 画光圈:Circle控件的参数陷阱

点击左侧控件栏中的Circle,然后在画布上拖出一个圆形。右侧属性栏会有几个和圆相关的参数:

  • Radius(半径):这个直接决定光圈圆环的大小。我这边设置成60。
  • Center X / Center Y(圆心坐标):这两个是圆心在父容器坐标系里的位置,不是控件的左上角。这里有一个我最初踩过的坑:Circle控件在布局系统里其实还是用一个矩形来定位的,这个矩形的左上角坐标和圆心坐标之间相差一个半径。也就是说,如果你想让圆心在屏幕坐标(120, 80)处,圆的左上角坐标应该是(120-60, 80-60),也就是(60, 20)。后面做移动动画的时候,移动的是控件的左上角,不是圆心,坐标换算很容易搞错。
  • Line Color / Line Width:圆环的线条颜色和粗细。光圈要有“光”的质感,我会选一个明亮的青色或者橙黄色,线宽设置在6左右。
  • Fill Color / Fill Alpha:填充颜色和透明度。做光圈的话,一般填充是透明的,也就是把Fill Alpha调成0,只保留外圈线条,这样看起来就是一个空心圆环。

这一步做完,你已经有了一个能显示的光圈。不过先别急着往下走,先在Designer里把它移动到屏幕中间偏左的位置,确认数据显示正常。

3.3 布置目标点:先确定“光圈要去哪儿”

目标点是光圈移动的锚点,我建议用圆形或者方形Box简单表示即可,优先级不高,重点是让位置关系清楚。

我放了4个目标点,坐标如下:

目标点索引 位置(左上角坐标) 用途说明
0 (120, 80) 左上角目标点,光圈的初始位置
1 (360, 80) 右上角目标点
2 (120, 200) 左下角目标点
3 (360, 200) 右下角目标点

这4个点组成了2x2的网格,水平间距240像素,垂直间距120像素。你完全可以根据自己的屏幕重新设计网格间距,但网格逻辑建议保持不变,因为代码里“行号/列号 -> 屏幕坐标”的换算,依赖这样一个规律。

为了让光圈和目标点之间视觉上“套得准”,要注意圆环半径和目标点外沿的对应关系。我的目标点设计成半径40的实心圆,光圈半径60,这样光圈能正好把这些目标点包在里面,移动完成后视觉上是“光圈框住了目标点”,而不是重叠得乱七八糟。

3.4 图层顺序与命名规范

在Designer右下角的图层列表里,控件是有叠放顺序的。位于列表上方的控件,绘制时在更前面的层级。如果光圈和目标点叠在一起,你要保证光圈在目标点上面,否则视觉上就像目标点把光圈压住了。

我习惯把所有值得在代码里引用的控件都重命名:

  • 光圈: circleFocus
  • 四个目标点: target0 target1 target2 target3 (如果这一版只是示意,你也可以不引用它们)

命名这件事看着琐碎,但在写代码的时候特别重要。你如果在代码里看到 circleFocus 就知道这是焦点光圈,看到 circle3 这种名字还得回去查是哪个控件。这在TouchGFX里影响不大,但在稍微复杂一点的界面上,命名混乱会直接拖慢你的开发速度。

还需要强调一点:每次在Designer里添加完控件,点击右上角的“Generate Code”或者保存,Designer会把更新后的代码同步到工程里。然后切到IDE里编译,从ViewBase里就能看到新增的控件成员变量了。

4. 让光圈动起来:按键输入与动画实现的完整链路

4.1 TouchGFX里的两套输入:触摸和物理按键

TouchGFX框架默认处理的是触摸事件。你在Designer里给控件加点击交互,设计器和生成代码都会围绕触摸去构建。但物理按键是另一条路,它不会自动被当作触摸处理,需要你自己从GPIO读取,然后“注入”到事件系统里。

TouchGFX的事件分发机制大致是这样的:

  • 触摸屏驱动在后台触发触摸事件,HAL层把事件交给 Application 的事件队列,再由事件分发器派发给当前Screen的View。
  • 物理按键更底层,它不经过触摸屏驱动,需要你自己写一小段代码读取GPIO状态,然后调用 Application::getInstance()->handleKeyEvent(key) 这样的接口,把按键值派发给当前View的 handleKeyEvent 回调。

你可以把物理按键理解成“半外部输入源”:网络里传数据到CPU的GPIO,我读到GPIO电平变化,再转换成一张“事件票”递给GUI框架,之后所有GUI逻辑就都按你设定的规则运行了。

这样说吧,TouchGFX的 handleKeyEvent(uint8_t key) 是一个virtual函数,每个Screen的View都可以重写它,按键按下去之后,框架就会把对应的key值传给当前处于激活状态的View。你要做的,就是在这个回调里写“收到这个键该干嘛”的逻辑。

4.2 物理按键读取:从GPIO到事件注入

先定义按键键值。在 Screen1View.hpp 里我建议直接定义枚举或者宏:

// 自定义按键键值,可以放在View的头文件里
#define KEY_UP    1
#define KEY_DOWN  2
#define KEY_LEFT  3
#define KEY_RIGHT 4

然后在STM32的HAL层里,主循环或者一个定时任务中,周期扫描GPIO状态。以无RTOS的场景为例,在主循环里大概是这样一小段逻辑:

// main() 主循环中,伪代码示意
while (1)
{
    uint8_t key = 0;

    if (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port, KEY_UP_Pin) == GPIO_PIN_RESET)
    {
        key = KEY_UP;
    }
    else if (HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port, KEY_DOWN_Pin) == GPIO_PIN_RESET)
    {
        key = KEY_DOWN;
    }
    else if (HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port, KEY_LEFT_Pin) == GPIO_PIN_RESET)
    {
        key = KEY_LEFT;
    }
    else if (HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port, KEY_RIGHT_Pin) == GPIO_PIN_RESET)
    {
        key = KEY_RIGHT;
    }

    if (key != 0)
    {
        // 可加简单消抖:确认电平稳定后再注入
        Application::getInstance()->handleKeyEvent(key);
    }

    // TouchGFX渲染相关
    touchgfx::HAL::getInstance()->backPorchExited();
}

注意几个细节:

第一,消抖。GPIO按键在按下和释放瞬间都会有机械抖动,如果不处理,一次按下的物理动作可能会产生多次 handleKeyEvent 。最简单的实用方案是“状态变化才触发”:记录上一次的键值,只有本次键值和上次不一样才去注入事件。更好的方案是延时20ms左右再读一次,确认电平稳定。相信我,这在按键控制里是必踩的坑,后面问题排查部分我会再提。

第二, handleKeyEvent 的调用在主循环里的位置。理想情况下,它应该在TouchGFX的帧循环入口被调用,这样事件可以及时进入GUI事件队列。放了 backPorchExited() 调用之前,主循环一般都能工作。

第三,如果项目里用了FreeRTOS或者ThreadX,我更建议在任务里维护一个按键消息队列,GUI任务去读取队列,而不是在主循环里直接灌事件。这样输入和UI解耦,后面加按键、加编码器都容易。

4.3 View里处理按键:移动光圈的“状态机”

事件注入到GUI框架之后,当前激活Screen的View会收到 handleKeyEvent 回调。我的设计是把“按键”映射成“方向”,再把“方向”映射成“当前选中格的变化”。

由于目标点是2x2网格,我维护一个索引值 currentIndex ,它可以是0到3。行列换算关系是:

row = currentIndex / 2
col = currentIndex % 2

按下“右”键时,col加1;按下“下”键时,row加1。但要注意边界:col已经等于1时再按右,就不应该再变化。这个边界判断在 handleKeyEvent 里做,或者抽一个 moveFocus(dx, dy) 方法,传入行列偏移。

// Screen1View.cpp
void Screen1View::handleKeyEvent(uint8_t key)
{
    if (key == KEY_UP)
    {
        moveFocus(0, -1);
    }
    else if (key == KEY_DOWN)
    {
        moveFocus(0, 1);
    }
    else if (key == KEY_LEFT)
    {
        moveFocus(-1, 0);
    }
    else if (key == KEY_RIGHT)
    {
        moveFocus(1, 0);
    }
    else
    {
        Screen1ViewBase::handleKeyEvent(key);
    }
}

void Screen1View::moveFocus(int dx, int dy)
{
    const int cols = 2;
    const int rows = 2;

    int row = currentIndex / cols;
    int col = currentIndex % cols;

    int newRow = row + dy;
    int newCol = col + dx;

    if (newRow < 0 || newRow >= rows || newCol < 0 || newCol >= cols)
    {
        return; // 已经到边界,不响应
    }

    currentIndex = newRow * cols + newCol;

    // 更新目标坐标,动画将在 tick 中逐帧完成
    targetX = targetPositions[currentIndex].x;
    targetY = targetPositions[currentIndex].y;
}

targetPositions 是一个预定义的坐标数组,存放每个目标点处光圈左上角应处的位置。注意要提前减去半径偏移,因为前面说过Circle控件的定位是容器左上角。

// Screen1View.cpp 中定义
static const touchgfx::Point TARGET_POSITIONS[4] = {
    { 60, 20 },   // 目标点0,圆心120,80,半径60
    { 300, 20 },  // 目标点1,圆心360,80
    { 60, 140 },  // 目标点2,圆心120,200
    { 300, 140 }  // 目标点3,圆心360,200
};

这样设计的好处是,目标点坐标改动只改一处数组,按键逻辑完全不用动。

4.4 平滑移动:两种动画方案的取舍

这是整个项目让视觉“像样”的关键一步。如果每次按键都把光圈直接 setXY 到新位置,效果是非常生硬的“瞬移”。要平滑移动,我试过两条路:

方案一:用TouchGFX的MoveAnimation机制。TouchGFX里有一个 MoveAnimator<T> 模板类,把控件包一层之后,可以直接调用 startMoveAnimation(newX, newY, duration) ,框架会在指定的毫秒数内自动做动画插值。如果以后项目里需要很多复杂的补间动画,走这条路最省事。

方案二:手写一个简单的逐帧插值,在 handleTickEvent 里做。这个方案更底层,但逻辑完全自己掌控,非常适合理解动画的本质。我这次在Demo里用的是方案二,因为代码量不大,而且不依赖额外的容器类型能避免版本差异。

手写插值的思想很朴素:每帧tick里,计算当前坐标和目标坐标的差值,如果差值大于步进速度,就向目标方向移动一个步长;如果差值很小,就直接定位到目标坐标。

// Screen1View::handleTickEvent
void Screen1View::handleTickEvent()
{
    int16_t curX = circleFocus.getX();
    int16_t curY = circleFocus.getY();

    int dx = targetX - curX;
    int dy = targetY - curY;

    if (dx == 0 && dy == 0)
    {
        return;
    }

    const int16_t moveSpeed = 6; // 每帧最多移动6像素

    int16_t stepX = dx;
    if (stepX > moveSpeed)  stepX = moveSpeed;
    if (stepX < -moveSpeed) stepX = -moveSpeed;

    int16_t stepY = dy;
    if (stepY > moveSpeed)  stepY = moveSpeed;
    if (stepY < -moveSpeed) stepY = -moveSpeed;

    circleFocus.setXY(curX + stepX, curY + stepY);
}

这样每次tick(TouchGFX一般以60fps运行,约16ms一帧)光圈会向目标靠近6个像素。当目标点之间距离240像素时,大概40帧能移动完,也就是0.6秒左右,肉眼看起来是平滑滑动,非常自然。

速度值 moveSpeed = 6 是调出来的,不是算出来的。步子太大了会像瞬移,太小了又显得拖沓。你在自己项目里可以根据目标点间距调整,一般间距100~200像素时,速度设4~8都是合理的。

4.5 初始状态与setupScreen的时机

还有一个容易忽略的小问题:刚进入界面时,光圈应该停在第一个目标点上,而不是从(0,0)一路飞过来。解决方法是把初始位置逻辑放到 setupScreen() 里。

void Screen1View::setupScreen()
{
    Screen1ViewBase::setupScreen();

    currentIndex = 0;
    circleFocus.setXY(TARGET_POSITIONS[0].x, TARGET_POSITIONS[0].y);
    targetX = TARGET_POSITIONS[0].x;
    targetY = TARGET_POSITIONS[0].y;
}

setupScreen() 在每次界面切换进入当前Screen时都会被调用,所以回到这个界面时,光圈也会自动复位到初始位置,这是一个很自然的逻辑。比如你从设置界面返回主界面,光圈的记忆位置如果想保留,那就别在这里强制复位,这个可以根据自己的需求决定。

5. 实战踩坑:按键控制与动画调试的问题速查

5.1 按键按下没反应,或者移动了好几次

这是新手最容易遇到的两个问题,一个方向是“没反应”,一个方向是“反应过头”。

没反应的原因,90%出在按键事件没有真正传到View。我调试时常做三件事:

第一,确认GPIO配置正确。用HAL库的话,检查按键引脚有没有配置成输入模式,上拉/下拉是否正确。如果你的按键接法是一端接地、一端接引脚,那引脚要配置上拉,按下时读到低电平。如果你按下时读到的还是高电平,大概率是引脚配置反了。

第二,确认键值对应关系。 handleKeyEvent 里做一次 key 值的打印或者临时点亮一个LED,看看按键按下时 key 是不是你期望的值。这一步能快速把“按键没被读到”和“事件没进View”区分开。

第三,确认 Application::getInstance()->handleKeyEvent 是否被反复调用。如果你的主循环里没有做“状态变化才触发”,那每次循环都会注入一次事件,View里收到的就是连续多次移动。加上“边沿触发”或者消抖逻辑就好了。

反应过头还有一个隐蔽原因:按键按下持续时间太长,你的循环里每次扫描都注入了一遍事件,导致光圈来回移动。推荐用“下降沿”或者“短按一次只响应一次”的思路,最简单的实现就是记录上一个按键状态,只有 当前按下了且上次没按下 时才注入事件。

5.2 动画卡顿、掉帧、不够顺滑

动画卡顿的原因主要集中在渲染负载和内存访问上。

先看渲染负载。TouchGFX的透明混合、抗锯齿这些效果会占用大量CPU周期。如果你用了大面积的半透明填充或者频繁刷新整个屏幕,低主频MCU容易撑不住。我的做法是尽量让光圈动画期间,背景层保持不变,只有光圈在移动,这样TouchGFX只需重绘局部区域,性能压力小很多。

再看内存。如果你的板子没有额外SDRAM,显示缓冲通常放在内部SRAM里,容量有限。TouchGFX推荐双缓冲来实现流畅动画,但双缓冲对RAM要求更高。如果内存不足,会出现画面撕裂或闪烁。你可以在HAL配置里检查缓冲方案,能开双缓冲就开,不能的话至少保证单缓冲的帧率稳定。

最后看 moveSpeed 。如果每帧移动的像素太多,动画会表现为“跳格”,视觉不连续。如果每帧移动像素太少,动画虽然平滑但很慢,也容易让人觉得卡。我自己的经验是在60fps下,速度在4~8像素/帧之间比较舒服。如果慢得离谱,你可以考虑增大速度值。

5.3 修改代码后重新生成,改动全没了

这是一个关于TouchGFX工作流的经典坑,几乎每个人都会遇到一次。

Screen1View.cpp 里写的代码,如果写在Designer保留的自定义代码区域内,重新生成后不会丢。但如果改动在 Screen1ViewBase.cpp Screen1ViewBase.hpp 这类 generated 目录或者 ViewBase 文件里,一旦Designer触发重新生成,改动会被冲掉。

我自己的习惯是:

  • 控件布局、坐标、属性,全部通过Designer去改。
  • 业务逻辑、按键回调、状态管理,全部写到 Screen1View Screen1Presenter 的自定义区域。
  • 绝不手动修改 generated 下的任何文件。

如果确实需要改底层HAL或者驱动,改成在 target 目录下进行,并保留修改记录。因为 target 目录虽然不是Designer直接生成的,但升级TouchGFX版本时也可能受到影响,最好固定版本后再动。

5.4 常见问题速查表

现象 可能原因 排查/解决办法
按键按下无响应 GPIO配置错误 检查输入模式和上下拉,打印键值确认
按键按下移动多次 无消抖/无边沿检测 加状态变化检测,或加20ms延时消抖
光圈移动方向反了 行/列方向和dx/dy映射搞反 检查moveFocus中dx/dy与上下左右的一致性
光圈没有套在目标点上 Circle坐标换算错误 确认控件左上角=圆心-半径,检查TARGET_POSITIONS数组
动画卡顿掉帧 内存不足或全屏重绘过多 开双缓冲,减少透明混合区域,降低动画区域复杂度
图片/文字显示花屏 LCD初始化或SDRAM配置错误 检查HAL层屏幕配置,确认显存地址正确
重新生成代码后功能丢失 改到了generated目录或ViewBase 业务逻辑迁移到自定义View文件中,重新生成后复查

5.5 把项目扩展成更实用的东西

这个“按键控制光圈移动”的Demo,做完之后只是一个起点。我基于这套逻辑继续做过几个扩展,效果都很好,顺手推荐给你:

一是在每个目标点上挂一个菜单项,比如“温度”、“风速”、“模式”、“定时”,用光圈做选中指示,按键负责切换,这就是典型的家电UI菜单交互。

二是加上“确认键”。在目标网格之外,再加一个独立的按键作为“确认”,按下后跳转到一个新Screen或者弹出一个对话框。你只需要在 handleKeyEvent 里多判断一个键值,然后调用 presenter->okPressed() 之类的方法,逻辑完全复用。

三是在光圈的移动动画里加入缓动效果,让动画更像“甩过来”的感觉。TouchGFX本身有缓动函数库,比如 cubicEaseOut ,如果你用 MoveAnimator 方式,直接传一个缓动函数;如果用tick插值,可以通过一个时间比例来改变每帧步长。视觉档次会明显提升。

四是把4个方向键换成编码器或摇杆。编码器的脉冲计数读出来是一个方向增量,本质上和按键dx/dy是一样的,只要你把旋转方向转换成对应的“左/右”事件,整个框架不用改。

一点收尾的个人体会

这个项目做完,我个人最大的体会是: TouchGFX真正的门槛不在控件和API,而在“事件流经的路径” 。触摸事件是框架替你做好的,物理按键却需要你自己从底层一路接到GUI层,中间任何一环断了,表现出的现象都是“没反应”,但排查起来却要从硬件一路查到软件。如果以后再遇到类似现象,我会先确认“事件是否真的到达了View”这个边界问题,再去动具体业务代码。另外,坐标换算这种小细节,虽然不起眼,但在做焦点移动类UI时特别容易反复出错,最好在项目一开始就统一用“圆心坐标”或“左上角坐标”中的一种,并在代码注释里写清楚。

希望这篇笔记能帮你少走几步弯路。

前端页面性能优化办法有哪些 启用Gzip压缩,减小HTML、CSS、JavaScript等文本文件的大小。:对非关键资源(如图片、视频)使用懒加载,仅在用户滚动到可视区域时加载。:将小的CSS或JavaScript直接嵌入HTML中,减少外部请求。:将多个CSS或JavaScript文件合并为一个,减少请求次数。:去除CSS、JavaScript中的空格、注释等,减小文件体积。:通过CDN分发静态资源,减少用户与服务器的距离,加快加载速度。:优先使用CSS动画而非JavaScript动画,提升性能。 阅读详情

相关推荐

几秒生成两个复杂前端页面,这是要抢程序员饭啊?

2月底,Anthropic发布的Claude 3.7 Sonnet(以下简称Claude 3.7)引发了AI编程领域的震动。这款被称为“全球首个混合推理模型”的AI工具,不仅在代码生成能力上实现了质的飞跃,更通过技术革新重新定义了开发者与AI的协作模式。Claude 3.5 Sonnet 编码能力就非常出色,Claude 3.7 编程能力更是强的离谱!评估 AI 模型解决实际软件开发问题能力的测试...

公众号-老炮说Java 120

复杂前端页面

复杂前端页面 <!doctype html> <#include "/jsp/common/commConstant.ftl"> <#include "/WEB-INF/jsp/common/commRSAEncryption.ftl"> <html> <head> <meta charset="utf-8"> <title>517电信钜惠</title> <link rel="stylesheet

Java修炼者的博客 525

基于数据驱动设计复杂页面

背景介绍: 最近公司启动了一个新的版本,我负责的一个的模块中有一个很复杂的新建的页面,表格里嵌套表格,三层数据,数据级联,组件较多.交互复杂, 下面是我做的一个简略图,为了保密我已将需求细节隐藏.(PS:没有table组件的墨刀,掩面苦笑,真难用) 从整个页面上分析,整体分为二部分,表单FA列表和表格TA 表单FA列表可以新增,删除,设为默认,其中一些表单项要求可以边输入边检索,选中选...

知无涯 7490

前端学院学习,复杂静态页面

前言 在做静态页面的时候会遇到诸多问题,自己现在也没能解决,比如如何能够准确判断单个盒子在页面的位置,设置多少像素才能和设计图百分之99吻合,做的过程还发现弹性盒子都没有理解好,于是又翻书,觉得最好用的还是绝对定位,和相对定位权当记录一下吧,菜鸟学习之路还好漫长还有一个问题,代码的不规范性问题,比如命名,最后命的我都头大 HTML <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &lt

qq_40891002的博客 328

前端页面优化

前端是庞大的,包括HTML、CSS、Javascript、Image、Flash等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。那么,前端优化的目的是什么 1. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验。 2. 从服务商角度而言,优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源。 总之,恰当...

yushang的博客 2811

记一复杂页面前端优化(1) - 延迟加载

刚刚做完1个复杂页面前端性能优化,这里的优化是针对这个页面具体的需求单独做的优化,所以这里不会谈那些减少http请求,合并压缩js,css,图片合并等等。因为这些是所有页面都需要做的,如果需要了解这些,可以参考我这篇文章:web高性能开发系列随笔   页面介绍:   该页面是1个记账类的页面页面如下:     页面主要有4部分组成: 1. 上部的输入

平安\平凡\平淡 1068

Web页面经常无响应前端如何定位与优化

在日常工作中,不断探索新的技术和方法,勇于尝试创新的设计理念,才能在这个快速发展的领域中保持竞争力。虚拟DOM通过对比前后两次的状态差异,仅对发生变化的部分进行实际的DOM操作,大大减少了不必要的重排和重绘。通过有效的定位和优化,可以显著提升Web应用的性能和稳定性,改善用户体验,提高用户满意度和忠诚度。希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。通过Performance面板,可以录制页面加载和交互过程中的性能数据,分析各个阶段的时间消耗。

一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身"农奴"把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟! 1837

前端页面性能优化#页面响应时间过长#前端优化方案#web性能优化#优化案例

前端性能优化是提高页面访问速度、减少资源消耗、减轻服务器负担、提升用户体验的重要手段。本篇文章将介绍几种高效的前端性能优化的方法,文章最后将附上一页面加载优化案例供大家参考,该案例采用Web Workers+requestAnimationFrame(帧率刷新)结合主线程进出栈 分布任务的手法极大提高页面响应速度。

weixin_53099292的博客 1810

前端页面初始化加载速度优化方案

前端页面初始化加载速度优化方案

前端金牌摸鱼达人 3867

前端页面性能优化(完整归纳版)

总之,通过优化资源加载、图片优化、CSS 和 JavaScript 优化、页面结构和渲染优化、响应式设计和移动优化等方面的技巧,可以显著提升前端页面的性能和用户体验。根据具体的项目需求和限制,选择适合的优化策略,并结合性能监测和分析工具进行实时调优和改进。这些是一些常见的前端页面性能优化方面,根据具体情况和项目需求,可能还有其他的优化技巧和策略可以使用。当优化前端页面性能时,可以从多个方面入手。

西装暴徒_12138的博客 4101

记一复杂页面前端优化

下拉列表优化 然后创建自己的输入框和弹出框。当页面只有1,2个select的时候,没发现有什么问题,但当页面出现7,8个select的时候,熏染速度明显慢了很多,可以看着1个个select变成input,主要原因还是js执行的太多了(动态生成html,绑定事件)等等。这个是影响页面渲染速度的1个大问题,所以第一个要解决的就是这个select。因原始的select外观是在是不行,而且又不能修改...

iteye_7304的博客 125

页面顿到流畅如丝,前端开发优化前后大揭秘

满心期待打开网页,却等半天不见内容,操作时还频频顿,这样的体验让人抓狂!开发者明明花了大量时间写代码,为何页面还是 “慢如蜗牛”?是技术不够先进,还是有隐藏的 “性能杀手”?从图片加载缓慢到动画掉帧,从白屏时间过长到交互延迟,这些问题究竟如何解决?其实,只要找准优化方向,就能让页面实现从 “龟速” 到 “丝滑” 的逆袭。接下来,就为你揭开前端开发优化背后的秘密!从页面顿到流畅如丝,前端开发优化涵盖了资源、代码、渲染等多个方面。

2401_82881178的博客 1511

前端长任务优化:让你的页面不再

你是否曾经遇到过这样的情况:当你在使用一个网页时,点击按钮后界面突然住不动,或者滚动页面时感觉特别不流畅?这些令人沮丧的体验很可能是由**前端长任务**引起的。在当今这个用户体验至上的时代,一个流畅的界面可以极大地提升用户对产品的好感度,而优化长任务则是实现这一目标的关键环节。

wenyang12的博客 1642

Web 页面优化:如何提高页面加载速度和用户体验

页面加载速度和用户体验优化需要从多个层面入手,包括前端、后端和网络架构等方面。通过合理的工具和技术手段,可以显著提升页面性能,从而增加用户的访问时长和转化率。未来,随着新技术(如 HTTP/3、WebAssembly)的发展,优化手段将更加多样化,为用户提供更加流畅的体验。如果你对本文内容感兴趣,欢迎留言探讨,分享你的优化经验!

一名热衷于技术的全栈开发者,专注于前端与后端的全面技术探索。在这里,我将分享我在技术领域的学习与成长,助力更多开发者的进步。 3547

《如何优化项目一》:页面缓存优化

文章目录前言叙述步入正题 :mega:代码实现:rocket: 前言叙述 在日常编写练手项目的时候,对于项目的优化并没有关注太多,关注点大部分集中在后端的代码逻辑如何去实现,而对于前端的渲染却没有过多的重视,只觉得可以显示即可,并不会对此有什么深入的要求。但是,如果我们要做的是一个供给大众使用的平台呢?如果依旧只注重于后端代码逻辑实现,而不思考前端的渲染效率,那么最终会造成什么样的后果呢?我们可以从两方面去分析: 用户方面 💁:

派大星的博客 1825

记一复杂页面前端优化(1) - 不一样的延迟加载

 刚刚做完1个复杂页面前端性能优化,这里的优化是针对这个页面具体的需求单独做的优化,所以这里不会谈那些减少http请求,合并压缩js,css,图片合并等等。因为这些是所有页面都需要做的,如果需要了解这些,可以参考我这篇文章:web高性能开发系列随笔 页面介绍:   该页面是1个记账类的页面页面如下:   页面主要有4部分组成: 1. 上...

weixin_34242658的博客 105

前端性能优化学习 04 前端页面的生命周期

前端页面的生命周期 性能问题呈现给用户的感受往往是简单而直接的:加载资源缓慢、运行过程顿或响应交互迟缓等,当把这些问题呈现到前端工程师面前时,却是另一种系统级别复杂的图景。 从域名解析、TCP 建立连接到 HTTP 的请求与响应,以及从资源请求、文件解析到关键渲染路径等,每一个环节都有可能因为设计不当、考虑不周、运行出错而产生性能不佳的体验。作为前端工程师,为了能在遇到性能问题时快速而准确地定位问题所在,并设计可行的优化方案,熟悉前端页面的生命周期是一堂必修课。 下面从一道常见的前端面试题开始,通过对此问

皮蛋很白的博客 1063

页面优化

前端是庞大的,包括HTML、CSS、Javascript、Image、Flash等等各种各样的资源。前端优化是复杂的,针对方方面面的资源都有不同的方式。那么,前端优化的目的是什么   1. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验。 2. 从服务商角度而言,优化能够减少页面请求数、或者减小请求所占带宽,能够节省可观的资源。

再给我三十分钟 6万+
上一篇: AI项目风险审计:从评估失真到成本失控的工程防范
下一篇: LINGO优化建模实战:从语法到应用,十分钟掌握数学建模利器
weixin_34168880
博客等级 码龄11年 4913粉丝 827原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值