4、屏幕与导航基础:Screen与Container的概念、屏幕切换、导航栈管理、应用状态机

屏幕和导航,是嵌入式GUI的骨架。你想想看,用户打开设备,看到的第一个界面是什么?点了一个按钮,下一个界面怎么跳出来?返回上一级时,数据还在不在?

这些问题,都绕不开Screen、Container、导航栈和状态机这几个核心概念。今天我们就把它彻底讲透。

4.1 Screen与Container:到底有什么区别?

很多刚接触TouchGFX的朋友,会把Screen和Container搞混。我刚开始做项目时也犯过这个错——把业务逻辑全塞进Screen里,结果代码乱成一锅粥。

说白了,Screen是“页面”,Container是“组件”。

概念 本质 生命周期 典型用途
Screen 全屏页面 进入时创建,离开时销毁 主界面、设置页、二级菜单
Container 可复用的UI组件 随父Screen或Container存活 按钮组、进度条、自定义键盘

一个Screen里可以放多个Container。比如一个仪表盘Screen,上面有温度显示Container、转速表Container、报警灯Container。这些Container可以独立开发、独立测试,最后拼到Screen里。

我的经验:把可复用的UI逻辑封装成Container,Screen只做页面编排和导航控制。这样后期改需求时,你只需要改对应的Container,不用动整个Screen。

4.2 屏幕切换(Screen Transition)

屏幕切换,就是从一个Screen跳到另一个Screen。TouchGFX提供了几种切换方式,每种都有它的适用场景。

4.2.1 切换类型

  • 无动画切换:瞬间跳转,适合后台数据刷新场景
  • 滑动切换:左右或上下滑动,适合菜单导航
  • 淡入淡出:透明度渐变,适合弹窗或提示页
  • 缩放切换:从中心放大或缩小,适合图片浏览

代码实现很简单,在Screen的handleClickEvent或button回调里调用:

// 切换到下一个Screen,带滑动动画
Application::getInstance()->switchScreen<NextScreen>(SlideTransition(SOUTH));

// 无动画切换
Application::getInstance()->switchScreen<NextScreen>(NoTransition());

嗯,这里要注意:切换动画会占用CPU资源。我在一个低端MCU项目上,用了复杂的缩放动画,结果帧率掉到15fps。后来换成简单的滑动切换,流畅多了。

小技巧:如果你的设备主频不高,优先用滑动或淡入淡出。缩放和3D旋转动画,留给性能好的平台。

4.2.2 切换时的生命周期回调

每次Screen切换,都会触发几个回调函数。搞清楚它们的调用顺序,能帮你避免很多坑。

// 当前Screen离开时
void Screen1::tearDownScreen() {
    // 释放资源、停止定时器、保存临时数据
}

// 目标Screen进入时
void Screen2::setupScreen() {
    // 初始化UI、加载数据、启动定时器
}

我曾经在tearDownScreen里忘记停止一个定时器,结果Screen已经销毁了,定时器还在跑,触发了野指针。从那以后,我每次写切换逻辑,都会检查一遍资源释放。

4.3 导航栈管理

导航栈,说白了就是一个“页面历史记录”。你每进入一个新Screen,它就被压入栈顶。点击返回时,栈顶的Screen被弹出,回到上一个Screen。

导航栈工作原理 导航栈 Screen3(当前) Screen2 Screen1 压栈 弹栈 操作说明 • 进入新页面 → 压栈(push) • 返回上一页 → 弹栈(pop) • 回到首页 → 清空栈(clear) • 栈空时 → 退出应用 注意事项 • 栈深度不要超过10层,否则内存吃紧 • 弹栈时Screen会被销毁,数据要提前保存 • 用replace替代push+pop,减少内存碎片

4.3.1 栈操作API

TouchGFX的导航栈操作,主要通过Application类提供:

// 压栈:进入新页面
Application::getInstance()->switchScreen<NewScreen>();

// 弹栈:返回上一页
Application::getInstance()->switchScreen<PreviousScreen>();

// 替换栈顶:不保留历史
Application::getInstance()->switchScreen<ReplaceScreen>(SlideTransition(EAST));

// 清空栈并跳转
Application::getInstance()->switchScreen<HomeScreen>(NoTransition());

注意:不要滥用栈深度。我在一个项目里嵌套了7层页面,结果内存占用飙到80%。后来改用replace模式,把不需要返回的页面直接替换掉,内存降了一半。

4.4 应用状态机

状态机,是管理UI行为的“大脑”。它决定了:当前在哪个状态?能做什么操作?操作后跳到哪个状态?

举个例子,一个音乐播放器:

当前状态 用户操作 下一状态 UI变化
停止 点击播放 播放中 播放按钮→暂停按钮
播放中 点击暂停 暂停 暂停按钮→播放按钮
播放中 歌曲结束 停止 重置进度条
暂停 点击播放 播放中 恢复进度条动画

在TouchGFX里,我习惯用枚举定义状态,用switch-case处理状态转换:

enum class PlayerState {
    STOPPED,
    PLAYING,
    PAUSED
};

class PlayerScreen : public Screen {
public:
    void handleButtonPlay() {
        switch (currentState) {
            case PlayerState::STOPPED:
            case PlayerState::PAUSED:
                startPlayback();
                currentState = PlayerState::PLAYING;
                updateUI();
                break;
            case PlayerState::PLAYING:
                pausePlayback();
                currentState = PlayerState::PAUSED;
                updateUI();
                break;
        }
    }

private:
    PlayerState currentState = PlayerState::STOPPED;

    void updateUI() {
        // 根据currentState更新按钮图标、进度条等
    }
};

我的习惯:把状态机逻辑单独抽成一个类,不混在Screen里。这样单元测试好写,也方便复用。比如播放器状态机,可以同时被Screen和后台服务调用。

4.5 综合案例:一个简单的设置页面导航

把上面几个概念串起来,我们做一个设置页面:

  1. 主Screen显示设置列表
  2. 点击“显示设置”进入DisplayScreen
  3. DisplayScreen里有一个亮度调节Container
  4. 点击返回回到主Screen
  5. 主Screen的“关于”按钮,直接替换栈顶进入AboutScreen(不保留历史)

这个案例里,Screen负责页面切换,Container负责亮度调节这个独立功能,导航栈管理返回路径,状态机控制每个页面的内部行为。各司其职,代码清晰。

嗯,说到这,我想起一个教训。早期我做导航时,喜欢在Screen的析构函数里保存数据。但析构时机不确定,有时候数据还没存完,Screen就被回收了。后来我统一在tearDownScreen里做保存,再也没出过问题。

核心总结:

  • Screen是页面,Container是组件,各管各的
  • 切换动画选简单的,别跟性能过不去
  • 导航栈别太深,该replace就replace
  • 状态机让UI行为可预测,一定要用