4、屏幕与导航:Screen概念、Screen生命周期、Screen切换动画、导航管理器
屏幕与导航,是嵌入式GUI里最基础也最容易踩坑的部分。我刚开始用TouchGFX做项目时,以为屏幕就是简单的页面跳转,结果在切换动画上栽了好几次跟头。今天咱们就把这块彻底捋清楚。
4.1 Screen到底是什么?
在TouchGFX里,Screen不是简单的“一页UI”。它是一整套资源的集合:
- 一个View类(负责界面绘制和交互)
- 一个Presenter类(负责逻辑和数据处理)
- 一个Model类(共享数据模型)
- 对应的UI设计文件(.touchgfx文件生成的代码)
说白了,每个Screen就是一个独立的功能模块。你想想看,一个温控器界面,温度显示、设置按钮、历史曲线,这些都可以放在同一个Screen里。但如果你要做菜单和子菜单的切换,那就得用多个Screen。
核心要点:Screen = View + Presenter + Model + UI资源。这四个部分缺一不可。
4.2 Screen生命周期——你必须要懂的四个阶段
每个Screen从创建到销毁,会经历四个关键阶段。我在项目中见过不少同事因为没搞懂生命周期,导致内存泄漏或者界面闪烁。
| 阶段 | 触发时机 | 典型操作 |
|---|---|---|
| setupScreen() | Screen即将显示时 | 初始化控件、加载数据、设置初始状态 |
| tearDownScreen() | Screen即将销毁时 | 释放资源、取消定时器、保存状态 |
| handleTickEvent() | 每个tick周期(通常16ms) | 周期性更新、动画驱动、数据轮询 |
| handleKeyEvent() | 物理按键触发时 | 按键响应、焦点切换 |
嗯,这里要注意:setupScreen()和tearDownScreen()是成对出现的。如果你在setupScreen()里new了对象,一定要在tearDownScreen()里delete掉。我曾经在一个项目里忘了释放动态分配的纹理,结果跑了三天后系统直接崩了。
我的习惯:在tearDownScreen()里统一做清理,不管是不是动态分配的,把指针置空、定时器停掉、回调解注册,一步到位。
4.3 Screen切换动画——别让用户觉得卡顿
切换动画是提升用户体验的关键。TouchGFX提供了几种内置的切换方式,但很多人不知道它们背后的原理。
4.3.1 内置切换类型
- SlideTransition:左右滑动切换,适合菜单导航
- FadeTransition:淡入淡出,适合弹窗或提示
- WipeTransition:擦除效果,适合进度展示
- NoTransition:无动画,适合调试或快速切换
代码示例——如何设置一个滑动切换:
// 在Application类中设置切换动画
ScreenTransition slideTransition = SlideTransition(SlideTransition::LEFT,
transitionDuration);
application().switchScreen(&nextScreen, slideTransition);
这里有个坑:transitionDuration的单位是毫秒,我建议设置在200-400ms之间。太短了用户看不清,太长了用户觉得卡。我之前做过一个300ms的滑动切换,测试反馈说“很丝滑”。
4.3.2 自定义动画——进阶玩法
内置动画不够用怎么办?你可以自己写。TouchGFX的动画底层是基于EasingEquations的,说白了就是缓动函数。
// 自定义缓动动画示例
class CustomAnimation : public AnimationListener {
public:
void animationEnded() override {
// 动画结束后的处理
application().switchScreen(&nextScreen, NoTransition());
}
void startCustomAnimation() {
// 创建一个缓动动画,持续500ms
EasingEquations::backEaseOut animation(500);
animation.setListener(this);
animation.start();
}
};
避坑指南:我曾经在自定义动画里直接操作了已经被销毁的Screen的控件,结果触发了HardFault。记住:动画回调里一定要检查Screen是否还活着。
4.4 导航管理器——别让屏幕跳转变成一团乱麻
当你的项目有十几个Screen时,手动管理跳转逻辑会变得非常痛苦。导航管理器就是来解决这个问题的。
4.4.1 什么是导航管理器?
导航管理器是一个全局的单例,负责维护Screen的堆栈。你可以把它想象成一个“历史记录”:
- pushScreen():跳转到新Screen,旧Screen入栈
- popScreen():返回上一个Screen,当前Screen出栈
- replaceScreen():替换当前Screen,不改变堆栈深度
- clearStack():清空所有历史,回到根Screen
4.4.2 实际项目中的用法
我习惯在FrontendApplication里封装导航方法:
// FrontendApplication.hpp
class FrontendApplication : public touchgfx::Application {
public:
void gotoMainScreen();
void gotoSettingScreen();
void gotoAboutScreen();
void goBack();
private:
void navigateTo(Screen& screen, ScreenTransition transition);
};
// FrontendApplication.cpp
void FrontendApplication::gotoMainScreen() {
MainScreenView* view = new MainScreenView();
MainScreenPresenter* presenter = new MainScreenPresenter(*view);
navigateTo(*presenter->getScreen(), SlideTransition(SlideTransition::RIGHT, 300));
}
你想想看,这样做的好处是什么?所有跳转逻辑集中在一个地方,后期要改动画或者加参数,只需要改一个文件。
4.4.3 堆栈管理的最佳实践
| 场景 | 推荐操作 | 原因 |
|---|---|---|
| 菜单→子菜单 | pushScreen() | 用户期望能返回 |
| 登录→主页 | replaceScreen() | 登录后不应返回登录页 |
| 设置→关于 | pushScreen() | 保持导航层级 |
| 退出到主菜单 | clearStack() + pushScreen() | 避免堆栈过深 |
我的经验:堆栈深度不要超过5层。超过5层后,内存占用会明显增加,而且用户也很难记住自己在哪里。如果发现堆栈太深,考虑用replaceScreen()或者重新设计导航结构。
4.5 本章知识体系总览
下面这张图把Screen和导航的核心关系梳理清楚了,建议你保存下来对照着看。
4.6 实战中的几个关键点
最后,分享几个我在实际项目中总结的经验:
- Screen复用:如果多个页面布局相似,考虑用同一个Screen配合参数切换,而不是创建多个Screen。我做过一个仪表盘项目,三个子页面共用同一个Screen,只切换数据源,内存节省了40%。
- 动画性能:在低端MCU上,避免同时做多个复杂动画。我建议一次只做一个Screen级别的切换动画,其他控件动画等切换完成后再启动。
- 调试技巧:在
setupScreen()和tearDownScreen()里加日志输出,可以帮你快速定位生命周期相关的问题。我习惯用touchgfx_printf()打印当前Screen的地址和状态。 - 内存管理:每个Screen的View和Presenter都是动态分配的,记得在
tearDownScreen()里释放。我见过一个项目因为忘记释放,运行两天后内存耗尽。
重要提醒:不要在handleTickEvent()里做耗时操作,比如文件读写或者网络请求。这个函数每16ms调用一次,阻塞了就会导致界面卡顿。我曾经犯过这个错,结果动画一卡一卡的,用户反馈说“像幻灯片”。
好了,屏幕与导航这块的核心内容就这些。记住:搞懂生命周期,用好导航管理器,选对切换动画,你的嵌入式GUI项目就能稳如泰山。