3、UI架构对比:屏幕管理机制、页面生命周期、事件驱动模型差异
好,咱们今天聊点硬核的。屏幕怎么管、页面怎么生怎么死、事件怎么跑——这三个问题,是迁移路上绕不开的坎。我当年从TouchGFX往LVGL切的时候,第一个踩的坑就是屏幕管理。TouchGFX那套Screen-based的思维太根深蒂固了,换到LVGL的Object tree,脑子转不过弯来。
说白了,两个框架的哲学完全不同。一个像页面栈,一个像节点树。你想想看,这背后的差异会影响到你写代码的每一个细节。
3.1 屏幕管理机制:Screen vs Object Tree
TouchGFX里,每个屏幕是一个Screen对象。你切屏,框架帮你做入栈出栈。每个Screen有自己的生命周期,你重写setupScreen()和tearDownScreen()就行。简单、粗暴、好理解。
LVGL呢?它没有“屏幕”这个概念。你看到的“页面”,其实是一个lv_obj,挂在lv_scr_act()下面。你想切屏?把旧的删了,建个新的,或者隐藏旧的显示新的。全得自己来。
我刚开始迁移时,习惯性地在LVGL里找ScreenManager,找了半天没找到。后来才明白,LVGL的设计哲学是“给你工具,不给你流程”。你得自己搭一套页面管理器。
下面这张图,把两个框架的屏幕管理逻辑画清楚了:
lv_obj_t*数组维护页面栈,配合lv_obj_set_hidden()做显隐切换。我项目里就是这么干的,代码量不到200行。
3.2 页面生命周期:谁在管生管死?
TouchGFX的页面生命周期是框架驱动的。你继承Screen,重写下面几个方法:
// TouchGFX 页面生命周期
virtual void setupScreen(); // 页面创建时调用
virtual void tearDownScreen(); // 页面销毁时调用
virtual void handleTickEvent(); // 每帧调用
框架会在合适的时机自动调用这些方法。你不需要关心什么时候创建、什么时候销毁——框架全包了。
LVGL呢?它没有setupScreen、tearDownScreen这种概念。页面的创建和销毁,完全由你手动控制。你调用lv_obj_create(),页面就活了;你调用lv_obj_del(),页面就死了。中间的状态变化,靠事件来通知。
我刚开始写LVGL时,老想着找一个“页面加载完成”的回调。找了半天,发现LVGL的设计思路是:你创建完对象,它就是加载完成了。没有中间状态,没有生命周期钩子。
那怎么办?自己加呗。我常用的做法是:
// LVGL 手动模拟页面生命周期
typedef struct {
lv_obj_t *page;
void (*on_create)(lv_obj_t *page);
void (*on_destroy)(lv_obj_t *page);
void (*on_tick)(lv_obj_t *page);
} page_manager_t;
void page_create(page_manager_t *mgr) {
mgr->page = lv_obj_create(lv_scr_act());
if (mgr->on_create) mgr->on_create(mgr->page);
}
void page_destroy(page_manager_t *mgr) {
if (mgr->on_destroy) mgr->on_destroy(mgr->page);
lv_obj_del(mgr->page);
mgr->page = NULL;
}
on_destroy里还想访问子对象,记得先保存指针,或者用lv_obj_del_async()延迟删除。我曾经因为没注意这个,在删除页面时顺手把定时器对象也删了,结果定时器回调里访问了野指针——直接硬死机。
两个框架的生命周期差异,我整理了一张表:
| 生命周期阶段 | TouchGFX | LVGL |
|---|---|---|
| 创建 | 框架自动调用 setupScreen() |
手动调用 lv_obj_create() |
| 显示 | 框架自动切换 | 手动 lv_scr_load() 或显隐控制 |
| 更新 | handleTickEvent() 每帧调用 |
lv_timer_create() 或 LV_ANIM |
| 销毁 | 框架自动调用 tearDownScreen() |
手动调用 lv_obj_del() |
| 资源释放 | 框架自动回收 | 需手动释放动态分配的内存 |
3.3 事件驱动模型:回调 vs 事件系统
TouchGFX的事件处理,说白了就是回调函数。按钮点击?绑定一个回调。滑动条变化?绑定一个回调。简单直接,但有个问题:回调多了,代码容易变成“回调地狱”。
// TouchGFX 事件绑定示例
button1.setAction(button1ClickedHandler);
slider1.setValueChangedAction(slider1ValueChangedHandler);
LVGL的事件模型就灵活多了。它用的是事件系统,所有事件都通过lv_event_t传递。你可以给任意对象绑定事件处理器,而且支持事件冒泡——子对象的事件可以向上传递给父对象。
// LVGL 事件绑定示例
lv_obj_add_event_cb(btn1, btn_event_handler, LV_EVENT_CLICKED, NULL);
static void btn_event_handler(lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t *target = lv_event_get_target(e);
if (code == LV_EVENT_CLICKED) {
LV_LOG_USER("按钮被点击了");
}
}
你想想看,这个事件冒泡机制能带来什么好处?比如你有一个页面,里面有很多按钮。你不需要给每个按钮单独绑定事件处理器,只需要在父容器上绑定一个,然后通过lv_event_get_target()判断是哪个子对象触发的。代码量直接减半。
不过,LVGL的事件系统也有坑。我记得有一次,我给一个列表项绑定了LV_EVENT_CLICKED,结果点击时触发了两次。查了半天才发现,列表项本身会触发一次,它的父容器(列表)也会因为事件冒泡再触发一次。解决方案很简单:在事件处理器里检查lv_event_get_target()和lv_event_get_current_target()是否一致。
// 避免事件冒泡导致重复处理
static void list_item_handler(lv_event_t *e) {
lv_obj_t *target = lv_event_get_target(e);
lv_obj_t *current = lv_event_get_current_target(e);
if (target != current) return; // 只处理直接绑定的事件
// 真正的处理逻辑
LV_LOG_USER("列表项被点击");
}
最后说一句,LVGL的事件系统还有一个杀手级功能:用户可以自定义事件。你可以定义LV_EVENT_USER_START + 1、LV_EVENT_USER_START + 2,用来做页面间的通信。这个在TouchGFX里实现起来比较麻烦,但在LVGL里,几行代码就搞定了。