修改菜单显示方式

This commit is contained in:
2026-09-08 21:07:04 +08:00
parent 891c9ef0d9
commit 6662f3a455
4 changed files with 212 additions and 158 deletions
+8 -8
View File
@@ -155,25 +155,25 @@ screen
```
- 4 层容器**持久存在**永不删(焦点/键入口,见 §5.2);子对象全部绝对定位(级联框须同屏叠加,层间不用 flex 堆叠)
- 框体 = `lv_obj`(透明底 + 1px 白边框 + 纵向 flex 堆行 + 自动滚动条),行建在框体内
- 框体 = `lv_obj`(透明底 + 1px 白边框 + 自动滚动条),行在框体内自绘(无子对象)
### 4.4 条目行与选中态
- 行 = `lv_obj` 容器(整行宽、高 15px,承载整行反色高亮)+ 文本 label(左留白 2px)+ 箭头 label(紧跟文本、间距 2px;叶子无箭头)
- 行**不入输入组**:纯展示,按键一律由容器 → 逻辑层处理
- 选中行 = CHECKED 状态 → 整行反色(白底黑字),**箭头随反色变黑**(参考图 02/04 实测:选中行保留 ▶);非选中行透明底白字
- 行 = 数据(节点下标 `s_row_node[d][i]`,无对象)+ 框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行整行白带 + 每行 `lv_draw_label` 文本(左留白 2px)/箭头(紧跟文本、间距 2px;叶子无箭头)
- 行**无对象、不入输入组**:纯展示,按键一律由容器 → 逻辑层处理
- 选中行 = 整行反色(白带、黑字),**箭头随反色变黑**(参考图 02/04 实测:选中行保留 ▶);非选中行黑底白字
### 4.5 框生命周期(按需建毁)
```
ENTER/→ 进子框:
现算几何(x = 父框右缘 − 1 共享边框列;y = 父"选中行"行顶,超屏底则上移使本框底对齐该行行底)
→ cont[depth+1] 建框体 + 行 → 焦点移至新容器
→ cont[depth+1] 建框体 + 注册行数据 → 焦点移至新容器
←/Esc 返回:
销毁最右框(框体连行整体删除,容器保留)→ 焦点回父容器;父层选中/滚动位原样保留
销毁最右框(框体连自绘回调整体删除,容器保留)→ 焦点回父容器;父层选中/滚动位原样保留
```
- **容器持久、框动态**:池内存 = 屏上各框行数之和(实测根页约 13KB,随深度增加但受"屏上框数 × 行数"有界,远低于 40KB 池)
- **容器持久、框动态**:行自绘无对象,池内存 = 屏上框体 + 装饰(实测根页约 1~2 KB,远低于 40KB 池)
- 同节点再次进入:还原该框上次选中与滚动位(`s_sel/s_scroll`);换节点则清零
- 叶子节点(无子项):ENTER 建空白终点框(w=6、h=2 占位),仅 ESC 可返回
- 深度保护:depth+1 ≥ 4 不再下钻
@@ -182,7 +182,7 @@ ENTER/→ 进子框:
- 焦点对象 = 最右(最深)框的容器;进/退时 `lv_group_focus_obj` 显式转移
- 每层容器挂 `menu_key_cb`(user_data = depth),统一分发到逻辑层 `menu_key_handle`:↑↓ 移选(首尾环绕)+ 跟随滚动、→/ENTER 进、←/ESC 退
- 选中变化只改行状态 + `lv_obj_scroll_to_view` 跟随,不重建对象;滚动位回读持久化到 `s_scroll[d]`
- 选中变化只改选中数据 + 跟随滚动 + 重绘框体,不重建;滚动位回读持久化到 `s_scroll[d]`
## 五、设计决策与考量(为什么这样做)
+23 -22
View File
@@ -548,9 +548,9 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框
- 屏幕根容器(全屏 flat)→ 4 层**持久全屏容器** `s_cont[0..3]`(每深度一个,`LV_OBJ_FLAG_IGNORE_LAYOUT` + 绝对定位,透明,永不删;级联框须同屏叠加,层间不用 flex 堆叠)
- 根框体 `s_frame_box[0]` 持久(位置 (0,44)、宽自适应);子框体进入时建 / 返回时毁(`lv_obj_create(s_cont[d])` + flat + 边框样式,`IGNORE_LAYOUT` 绝对定位)
- 框体:纵向 flex 堆行 + 自动滚动条(`LV_SCROLLBAR_MODE_AUTO`);行超框高才框内滚动
- 行 = `lv_obj` 容器(整行宽、高 15;整行反色高亮的载体)+ 文本 label(左留白 2)+ 箭头 label(紧跟文本、间距 2;叶子无箭头);文本/箭头 `IGNORE_LAYOUT` 行内绝对定位,静态字符串(`lv_label_set_text_static`);**反色颜色设在行容器上、文本/箭头继承**(`text_color` 可继承,label 指针不另存)
- 选中行 = `LV_STATE_CHECKED` → 整行反色(白底黑字),**箭头随反色变黑**(§4.4);非选中行透明底白字
- 框体:自动滚动条(`LV_SCROLLBAR_MODE_AUTO`);行自绘(无子对象),行内容超框高才框内滚动(内容高经 `LV_EVENT_GET_SELF_SIZE` 虚报)
- 行 = **数据**(节点下标注册 `s_row_node[d][i]`,无对象);框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行 = 整行白带(`lv_draw_rect`),每行 = `lv_draw_label` 文本(左留白 2)+ 箭头(紧跟文本、间距 2;叶子无箭头),静态字符串(Flash 表、不拷贝);按滚动偏移枚举可见行、重绘区外的行跳过(culling)
- 选中行 = 整行反色(白带、黑字),**箭头随反色变黑**(§4.4);非选中行黑底白字(白箭头)
- 根框装饰:两腿"浅 V"(`lv_line` 宽 3,点集 (7,7)-(16,17) / (158,7)-(145,17))+ 水平线(宽 2,(16,17)-(145,17))+ 状态行 label(17,20)
- 空终点框:叶子 ENTER 后建 w=6、h=2 的占位框(无行),仅 ESC 可返回
@@ -560,7 +560,7 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框
|---|---|---|
| `int ui_menu_view_init(void)` | 样式 + 4 层容器 + 根框装饰 + 根框框体(一次性) | 逻辑层 `ui_menu_init` |
| `lv_obj_t *ui_menu_view_container(uint8_t depth)` | 取该深度持久容器(逻辑层在其上绑输入组/键回调) | 逻辑层 |
| `void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)` | 建该深度框:现算几何 + 建行对象 + 应用选中/滚动(d=0 仅(重)建行) | 逻辑层(进 / 初始化) |
| `void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)` | 建该深度框:现算几何 + 注册行数据 + 应用选中/滚动(d=0 仅(重)注册行) | 逻辑层(进 / 初始化) |
| `void ui_menu_view_frame_destroy(uint8_t depth)` | 毁该深度框(框体+行,容器保留;根框 d=0 不可毁) | 逻辑层(退) |
| `void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)` | 刷选中高亮 + 跟随滚动(不重建对象) | 逻辑层(移选后) |
| `void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)` | 应用滚动偏移(行重建后按此还原) | 逻辑层 |
@@ -574,13 +574,13 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框
| 字段 | 类型 | 大小 | 用途 |
|---|---|---|---|
| `s_cont[4]` | `lv_obj_t*`×4 | 16 B | 4 个全屏容器(每深度一个);透明常驻、**永不删**(框在容器内建/毁,防焦点对象悬空) |
| `s_row[4][16]` | `lv_obj_t*` | 256 B | 行容器(文本/箭头 label 是其子对象,不另存指针);**仅屏上框填** |
| `s_row_node[4][16]` | `uint8_t` | 64 B | 行数据:节点下标(名称/箭头绘制时现查,无对象);**仅屏上框注册** |
| `s_root_deco` / `s_root_status` | `lv_obj_t*`×2 | 8 B | 根框装饰:"浅 V"水平段(两腿为局部对象,不落句柄)+ 状态行 |
| `s_style_flat/selected/frame/scrollbar` | `lv_style_t`×4 | ~400 B | 样式(一次性初始化,常驻) |
| `s_style_flat/frame/scrollbar` | `lv_style_t`×3 | ~300 B | 样式(一次性初始化,常驻) |
| `s_frame_box[4]` | `lv_obj_t*`×4 | 16 B | 框体(d=0 持久;d>0 进入时建、返回时毁) |
| `s_frame[4]` | `menu_frame_t`(built/sel/item_cnt/x/y/w/h) | 48 B | 每框几何缓存:子框对齐要用**父框刚算出**的 x/w/选中行,故按层留一份;随时可由 `s_path`/`s_sel` 重算,**不属于导航状态** |
> **只给"屏上的框"建行对象**(每框 ≤16 行);返回时销毁被关闭最右框的行 → LVGL 池只占**屏上各框**(2~3 框),关闭即释放。
> **只给"屏上的框"注册行数据**(每框 ≤16 行);返回时清掉被关闭最右框的行数据 → 行自绘无对象,LVGL 池只占**屏上框体**(2~3 框),关闭即释放。
---
@@ -603,7 +603,7 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框
| `s_path[4]` | `uint8_t`×4 | 4 B | **路径**:`s_path[d]`=第 d 层"展示的节点下标"(`s_path[0]=0` 主界面);即"从根到当前(最右)框"的节点链 |
| `s_sel[4]` | `uint8_t`×4 | 4 B | **各层选中下标**:返回时还原该层高亮(选中不跳) |
| `s_depth` | `uint8_t` | 1 B | **当前深度**(0=根框)= 焦点所在(最右)框 |
| `s_scroll[4]` | `uint16_t`×4 | 8 B | **每框滚动偏移**(px):该框销毁再进入、行对象重建时按此还原位置 |
| `s_scroll[4]` | `uint16_t`×4 | 8 B | **每框滚动偏移**(px):该框销毁再进入、行重建时按此还原位置 |
| **合计** | | **17 B** | 其余(条目列表/名字/菜单叶子/框几何)全部**现算**,不落 RAM(见 §7.3) |
### 6.3 导航规则:数据如何驱动"移 / 进 / 退"
@@ -660,13 +660,13 @@ s_path[0]=0 ─▶ s_path[1]=6 ─▶ s_path[2]=7 ─▶ s_path[3]=8
- 链上**每个 node 都是上一个的孩子**(parent 关系)→ 这条链天然就是路径,**无需额外记录"我从哪来"**。
- **进 = 链尾追加**(选中的子节点接上去);**退 = 链尾弹掉**。
- 内存只占"当前这条链"(`s_path[0..s_depth]`)+ 屏上各框各一套行对象(屏外框行销毁),退回即释放(§7.2/§7.5)。
- 内存只占"当前这条链"(`s_path[0..s_depth]`)+ 屏上各框各一套行数据(屏外框行清空),退回即释放(§7.2/§7.5)。
**对应到代码**(逻辑层决定、显示层执行;三层见 §一):
| 逻辑 | 函数 | 读 / 写的数据 |
|---|---|---|
| 建框(显示某节点子项) | 显示层 `ui_menu_view_frame_build(d, node, sel, scroll)`(由逻辑层调用) | 读 `s_path[d]`+`g_menu_tab` 现算条目;算 x/顶边/框宽、建 `s_row[d][*]`、恢复 `s_scroll[d]` |
| 建框(显示某节点子项) | 显示层 `ui_menu_view_frame_build(d, node, sel, scroll)`(由逻辑层调用) | 读 `s_path[d]`+`g_menu_tab` 现算条目;算 x/顶边/框宽、注册 `s_row_node[d][*]`、恢复 `s_scroll[d]` |
| 进(压层 / 终点) | 逻辑层 `menu_open(child)` | 写 `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]`(同节点还原 / 换节点清零);再 `frame_build` + `focus` |
| 退(弹层) | 逻辑层 `menu_back()` | `ui_menu_view_frame_destroy(s_depth)`;`s_depth-1`、焦点回父框(父框不动、无需重建,`s_sel`/`s_scroll` 原样保留) |
| 移选 | 逻辑层 `menu_key_handle(d, key)` → `menu_refresh_layer(d)` | 写 `s_sel[d]`(首尾环绕);`frame_refresh`(高亮 + 跟随滚动);滚动位回读入 `s_scroll[d]` |
@@ -713,14 +713,14 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 键 | 逻辑层(17 B 状态) | 显示层(画面) |
|---|---|---|
| **移** ↑/↓ | `s_sel[d] = (s_sel[d] ∓ 1 + cnt) % cnt`(首尾环绕);`cnt=0`(空框)不变 | `frame_refresh(d, s_sel[d])`:改行状态+文字色+跟随滚动(不重建对象);随后逻辑层 `get_scroll(d)` 回读入 `s_scroll[d]` |
| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 建行对象 → 应用选中/滚动;`focus(d+1)`;叶子 → 空终点框(w=6, h=2) |
| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体+行(容器保留,池内存即回落);`focus(d−1)`;父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 |
| **移** ↑/↓ | `s_sel[d] = (s_sel[d] ∓ 1 + cnt) % cnt`(首尾环绕);`cnt=0`(空框)不变 | `frame_refresh(d, s_sel[d])`:改选中数据 + 跟随滚动 + 重绘框体(不重建);随后逻辑层 `get_scroll(d)` 回读入 `s_scroll[d]` |
| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 注册行数据 → 应用选中/滚动;`focus(d+1)`;叶子 → 空终点框(w=6, h=2) |
| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体(含自绘回调,容器保留,池内存即回落);`focus(d−1)`;父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 |
### 7.2 框生命周期(进建退毁)
- **进时建**(`frame_build`):现算几何(§5.3;父框几何取父层 `s_frame` 缓存)→ 建行对象(行 = 子项,现算不落地)→ 应用选中高亮 + 滚动位。
- **退时毁**(`frame_destroy`):框体连同行对象销毁、容器保留(永不删,防焦点对象悬空);LVGL 池内存随即回落。
- **进时建**(`frame_build`):现算几何(§5.3;父框几何取父层 `s_frame` 缓存)→ 注册行数据(行 = 子项,现算不落地)→ 应用选中高亮 + 滚动位。
- **退时毁**(`frame_destroy`):框体(含自绘回调)整体销毁、容器保留(永不删,防焦点对象悬空);LVGL 池内存随即回落。
- **根框持久**(`ui_menu_view_init` 建一次,永不毁);子框随进/退建/毁。
- **焦点移动**:4 层容器同组、任一时刻仅最右框容器获焦;进/退时 `focus()` 显式移动,无焦点争抢。
- **空终点框**:叶子 ENTER 后建 w=6、h=2 占位框(无行),仅 ESC 可返回。
@@ -747,8 +747,8 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 操作 | 读 | 写 |
|---|---|---|
| **建框**(进 / 初始化) | `s_path[d]`、`g_menu_tab`(现算条目)、`s_sel[d]`、`s_scroll[d]` | 算该框 x/顶边/框宽(§四);建 `s_row[d][*]`;恢复滚动位 |
| **移**(↑/↓) | `s_sel[d]`、`s_scroll[d]`、`s_row[d][*]` | `s_sel[d]`(首尾环绕);到边缘则改 `s_scroll[d]`(滚);刷新高亮 |
| **建框**(进 / 初始化) | `s_path[d]`、`g_menu_tab`(现算条目)、`s_sel[d]`、`s_scroll[d]` | 算该框 x/顶边/框宽(§四);注册 `s_row_node[d][*]`;恢复滚动位 |
| **移**(↑/↓) | `s_sel[d]`、`s_scroll[d]`、`s_row_node[d][*]` | `s_sel[d]`(首尾环绕);到边缘则改 `s_scroll[d]`(滚);刷新高亮 |
| **进**(ENTER/→,菜单) | `s_sel[d]`→子节点 M、`ui_menu_tab_has_children(M)`=真 | `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]=0`;新框贴父框右缘生成(既有框不动、不重排) |
| **进**(ENTER/→,叶子) | `ui_menu_tab_has_children(M)`=假 | `s_depth+1`、`s_path[d+1]=M`(终点框) |
| **退**(ESC/←) | `s_depth` | `s_depth-1`、销毁最右框行、焦点回父框(父框不动、无需重建,`s_sel[d-1]`/`s_scroll[d-1]` 原样保留) |
@@ -764,15 +764,15 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 导航历史 `s_path`+`s_sel`+`s_depth` | 逻辑层 | 9 B |
| 每框滚动 `s_scroll[4]` | 逻辑层 | 8 B |
| 容器 `s_cont[4]` | 显示层 | 16 B |
| 行句柄 `s_row[d][i]` `[4][16]` | 显示层 | 256 B |
| 行数据 `s_row_node[d][i]` `[4][16]` | 显示层 | 64 B |
| 根框装饰指针(2 个:水平段 + 状态行) | 显示层 | 8 B |
| 框体句柄 `s_frame_box[4]` | 显示层 | 16 B |
| 框几何缓存 `s_frame[4]`(`menu_frame_t`) | 显示层 | 48 B |
| 样式 `s_style_*`(4 个,一次性) | 显示层 | ~400 B |
| **小计** | | **~1.4 KB**(逻辑层 17 B + 显示层 ~1.4 KB) |
| 样式 `s_style_*`(3 个,一次性) | 显示层 | ~300 B |
| **小计** | | **~0.5 KB**(逻辑层 17 B + 显示层 ~0.5 KB) |
**LVGL 池(动态,`LV_MEM` 40 KB 预算内)**:占**屏上各框**对象 = 屏上框数(2~3) × 行数 ×(行容器+文本+箭头),
典型 ≈ **4–8 KB**,最坏 3 框满 16 行 ≈ 15 KB(仍远在 40 KB 内)。
**LVGL 池(动态,`LV_MEM` 40 KB 预算内)**:行自绘无对象,池只占**屏上框体**(2~3 个 `lv_obj`)+ 根框装饰/状态行,
典型 ≈ **1~2 KB**,仍远小于 40 KB(池可再缩减,见 `docs/待处理内容/RAM使用分析与优化方案.md`)。
---
@@ -827,3 +827,4 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 2026-09-08 | §5.3 几何计算重写为图文并茂:新增 `images/级联框几何计算.svg` 四格图(① 行剖面定框宽 ② X 共享边框列 ③ Y 顶对齐 ④ Y 溢出上移,含虚线幽灵框 + 超屏红区示意),正文改为四步叙述 + 实测框宽表 + 公式汇总(五式内容不变) |
| 2026-09-08 | `menu_frame_t` 瘦身:`built` bool→`uint8_t`(与系统存储类型一致);`x/y/w/h` `int32_t`→`int16_t`(值域 0..160 / ≤242,16 位够;y 需有符号——钳制前可为负);`s_frame[4]` 96 B→48 B,省 48 B RAM(§5.4 对象模型表 / §7.5 内存预算表同步) |
| 2026-09-08 | 行 label 指针合并:删 `s_row_text` / `s_row_arrow` 两数组(省 512 B);文本/箭头颜色改在行容器 `s_row` 上设置、子 label 继承(LVGL `text_color` 为可继承属性,`lv_style.c` 属性标志表证实);§5.4 对象模型表 / §7.5 内存预算表同步(768 B→256 B) |
| 2026-09-08 | **行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税")**:行不再是 LVGL 对象——行数据改 `s_row_node[4][16]`(`uint8_t` 节点下标,256 B→64 B),框体 `LV_EVENT_DRAW_MAIN` 自绘(选中行白带 `lv_draw_rect` + 每行 `lv_draw_label` 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 `LV_EVENT_GET_SELF_SIZE` 虚报内容高 + 手动跟随滚动(`view_scroll_to_sel`,替代 `lv_obj_scroll_to_view`);删 `s_style_selected`、`view_add_row` 与 flex 堆行;`frame_build/destroy/refresh` 改行数据注册/清空/重绘;公开 API 签名不变。§5.4/§5.6/§6.2/§7.1~7.5 同步;静态显示层 ~1.4→~0.5 KB、LVGL 池典型 4~8→1~2 KB。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS |