修改菜单显示方式

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
+173 -121
View File
@@ -2,7 +2,8 @@
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框体/根框装饰),
* 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动);
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
@@ -12,6 +13,9 @@
* 2026-09-08 王建锋 按代码规范 V1.0 补全:分区注释、公共函数注释、关键行注释
* 2026-09-08 王建锋 menu_frame_t 瘦身(uint8_t/int16_t,96→48 B/层);
* s_row_text/s_row_arrow 并入 s_row(省 512 B,行 label 颜色继承)
* 2026-09-08 王建锋 行对象→自绘重写:删行容器/文本/箭头 label 对象,框体
* DRAW_MAIN 自绘行(反色带/文本/▶),GET_SELF_SIZE 虚报
* 内容高实现虚拟滚动(省约 2 KB/行 对象开销)
*/
/* 头文件包含区 */
@@ -66,8 +70,8 @@ typedef struct {
/* 静态变量定义区 */
static lv_obj_t *s_cont[MENU_PAGE_MAX]; /* 4 层持久容器(每深度一个;焦点/键入口,永不删) */
static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁 */
static lv_obj_t *s_row[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行容器(整行反色高亮的载体;文本/箭头是其子对象,不单独存) */
static lv_obj_t *s_frame_box[MENU_PAGE_MAX]; /* 框体:d=0 持久(根框);d>=1 进入时建、返回时毁(含自绘回调) */
static uint8_t s_row_node[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行数据:节点下标(名称/箭头绘制时现查,无对象) */
static menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */
static lv_obj_t *s_root_deco = NULL; /* 根框装饰:顶部"浅 V"水平段(两腿为临时对象,样式同源) */
@@ -75,7 +79,6 @@ static lv_obj_t *s_root_status = NULL; /* 根框状态行 */
/* 样式(静态,一次性初始化,常驻) */
static lv_style_t s_style_flat; /* 通用:透明、无边框/圆角/内边距(剥离主题 card) */
static lv_style_t s_style_selected; /* 选中态:整行反色(白底) */
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
@@ -88,7 +91,7 @@ static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
* 限定条件:无
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. frame 为所有级联框(含根框)的 1px 白边框
* 3. selected 为选中行整行反色(1bpp 白底);文字色在刷新时显式置黑
* 3. 选中行反色不用样式:自绘回调按 sel 直接画白带(行不是对象)
*/
static void view_styles_init(void) {
static bool inited = false;
@@ -107,12 +110,6 @@ static void view_styles_init(void) {
lv_style_set_pad_all(&s_style_flat, 0);
lv_style_set_pad_gap(&s_style_flat, 0);
/* selected:选中行整行反色(白底黑字) */
lv_style_init(&s_style_selected);
lv_style_set_bg_opa(&s_style_selected, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_selected, lv_color_white());
lv_style_set_text_color(&s_style_selected, lv_color_black());
/* frame:透明底 + 1px 白边框(所有级联框含根框) */
lv_style_init(&s_style_frame);
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
@@ -167,120 +164,184 @@ static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) {
}
/*
* 函数功能:在框体上创建一条条目行(行容器 + 文本 + 可选箭头)
* 入口参数:box - 所属框体 lv_obj_t *
* d - 框所在深度 uint8_t
* i - 行下标 uint8_t
* node - 条目节点下标 uint8_t
* has_arrow - 是否创建箭头(=条目有子项) bool
* 返回值:true - 成功;false - 对象创建失败(LVGL 池耗尽,调用方停止建行)
* 限定条件:d < MENU_PAGE_MAX,i < MENU_ITEM_MAX
* 函数说明: 1. 行容器整行宽、高 MENU_ROW_H,承载选中整行反色高亮
* 2. 文本左留白 MENU_ROW_PAD_L;箭头紧跟文本(间距 MENU_ARROW_GAP,自适应)
* 3. 文本/箭头子 label 加 IGNORE_LAYOUT 做行内绝对定位,是行的子对象、不单独存
* 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_GET_SELF_SIZE,布局阶段由 LVGL 调用
* 函数说明:行不是子对象,经 GET_SELF_SIZE 虚报内容高,
* LVGL 据此算滚动范围与自动滚动条(替代旧"行对象撑内容高")
*/
static bool view_add_row(lv_obj_t *box, uint8_t d, uint8_t i, uint8_t node, bool has_arrow) {
/* 行容器:整行宽、行高 MENU_ROW_H(整行反色高亮的载体) */
lv_obj_t *row = lv_obj_create(box);
if (row == NULL) {
return false;
}
lv_obj_add_style(row, &s_style_flat, 0);
static void view_frame_self_size_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
lv_point_t *p = lv_event_get_param(e);
/* 挂选中行反色样式(CHECKED 态时触发白底) */
lv_obj_add_style(row, &s_style_selected, LV_STATE_CHECKED);
lv_obj_set_width(row, lv_pct(100));
lv_obj_set_height(row, MENU_ROW_H);
/* 文本 label:左留白,行内绝对定位(IGNORE_LAYOUT) */
lv_obj_t *lbl = lv_label_create(row);
if (lbl == NULL) {
lv_obj_delete(row);
return false;
}
lv_obj_add_style(lbl, &s_style_flat, 0);
lv_obj_add_flag(lbl, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(lbl, MENU_ROW_PAD_L, MENU_ROW_TXT_TOP);
/* 静态字符串:直接指向 ROM 表,不拷贝 */
lv_label_set_text_static(lbl, ui_menu_tab_name(node));
/* 箭头:仅菜单行(有子项),紧跟文本(间距 MENU_ARROW_GAP) */
lv_obj_t *ar = NULL;
if (has_arrow) {
const char *name = ui_menu_tab_name(node);
/* 箭头 x = 左留白 + 实测文本宽 + 间距 */
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
ar = lv_label_create(row);
if (ar != NULL) {
lv_obj_add_style(ar, &s_style_flat, 0);
lv_obj_add_flag(ar, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(ar, MENU_ROW_PAD_L + tw + MENU_ARROW_GAP, MENU_ROW_TXT_TOP);
lv_label_set_text_static(ar, "▶");
}
}
/* 只存行容器:文本/箭头是其子对象(颜色在行上设置、子对象继承) */
s_row[d][i] = row;
return true;
p->x = 0; /* 无横向虚拟内容(行整宽,无横向滚动) */
p->y = (int32_t)s_frame[depth].item_cnt * MENU_ROW_H; /* 内容高 = 行数×行高 */
}
/*
* 函数功能:应用指定深度框的选中高亮(整行反色 + 文字/箭头颜色)
* 函数功能:框体自绘(选中行反色带 + 行文本 + 箭头)
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,框体已建、行已注册
* 函数说明: 1. 行不是对象:按滚动偏移枚举可见行号,逐行画白带/文本/▶
* 2. 选中行反色(白底黑字)、非选中黑底白字(与旧 label 渲染同
* 字体光栅化路径,1bpp 下像素级一致)
* 3. 不在重绘区的行跳过(culling)
*/
static void view_frame_draw_cb(lv_event_t *e) {
uint8_t depth = (uint8_t)(uintptr_t)lv_event_get_user_data(e);
const menu_frame_t *f = &s_frame[depth];
lv_obj_t *box = s_frame_box[depth];
lv_layer_t *layer = lv_event_get_layer(e);
lv_area_t area;
int32_t i_first, i_last, i;
lv_obj_get_content_coords(box, &area); /* 内容区(框内、边框之内) */
int32_t sy = lv_obj_get_scroll_y(box); /* 纵向滚动偏移(px) */
/* 可见行范围:行 i 占内容 y ∈ [i*15, i*15+15) */
i_first = sy / MENU_ROW_H;
i_last = (sy + lv_obj_get_content_height(box) - 1) / MENU_ROW_H;
if (i_last >= (int32_t)f->item_cnt) {
i_last = (int32_t)f->item_cnt - 1;
}
for (i = i_first; i <= i_last; i++) {
int32_t row_y = area.y1 + i * MENU_ROW_H - sy;
lv_area_t row_area;
lv_area_t clip;
const char *name = ui_menu_tab_name(s_row_node[depth][i]);
bool is_sel = (i == (int32_t)f->sel);
lv_color_t color = is_sel ? lv_color_black() : lv_color_white();
row_area.x1 = area.x1;
row_area.x2 = area.x2;
row_area.y1 = row_y;
row_area.y2 = row_y + MENU_ROW_H - 1;
/* 行 ∩ 重绘区(min/max 裁剪,与 lv_area_intersect 等价;空则跳过 culling) */
clip.x1 = row_area.x1 > layer->_clip_area.x1 ? row_area.x1 : layer->_clip_area.x1;
clip.y1 = row_area.y1 > layer->_clip_area.y1 ? row_area.y1 : layer->_clip_area.y1;
clip.x2 = row_area.x2 < layer->_clip_area.x2 ? row_area.x2 : layer->_clip_area.x2;
clip.y2 = row_area.y2 < layer->_clip_area.y2 ? row_area.y2 : layer->_clip_area.y2;
if (clip.x1 > clip.x2 || clip.y1 > clip.y2) {
continue; /* 行不在重绘区:跳过(culling) */
}
/* 选中行:整行反色(白带) */
if (is_sel) {
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = lv_color_white();
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &row_area);
}
/* 行文本:左留白、垂直居中偏移(与旧 label 同位) */
lv_draw_label_dsc_t ld;
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = name;
ld.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = MENU_ROW_PAD_L;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
/* 箭头:仅菜单行(有子项),紧跟文本(间距自适应) */
if (ui_menu_tab_has_children(s_row_node[depth][i])) {
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), LV_FONT_DEFAULT, 0);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = "▶";
ld.text_static = 1;
ld.color = color;
ld.ofs_x = MENU_ROW_PAD_L + tw + MENU_ARROW_GAP;
ld.ofs_y = MENU_ROW_TXT_TOP;
lv_draw_label(layer, &ld, &row_area);
}
}
}
/*
* 函数功能:把选中行滚入可见区(手动跟随滚动,替代 scroll_to_view)
* 入口参数:box - 框体 lv_obj_t *
* f - 框几何缓存(含 sel) const menu_frame_t *
* 返回值:无
* 限定条件:box 已创建且已虚报内容高
* 函数说明:选中行在可见区之上 → 顶对齐;之下 → 底对齐;已在区内 → 不动
* ANIM_OFF:回读滚动位 = 最终值(逻辑层靠回读持久化)
*/
static void view_scroll_to_sel(lv_obj_t *box, const menu_frame_t *f) {
int32_t sy;
int32_t row_top;
int32_t view_h;
if (f->item_cnt == 0u) {
return; /* 空框无行可跟随 */
}
sy = lv_obj_get_scroll_y(box);
row_top = (int32_t)f->sel * MENU_ROW_H;
view_h = lv_obj_get_content_height(box); /* 内容区高(框内、边框之内) */
if (row_top < sy) {
sy = row_top; /* 选中行在可见区之上:顶对齐 */
}
else if (row_top + MENU_ROW_H > sy + view_h) {
sy = row_top + MENU_ROW_H - view_h; /* 选中行在可见区之下:底对齐 */
}
if (sy < 0) {
sy = 0;
}
if (sy != lv_obj_get_scroll_y(box)) {
lv_obj_scroll_to_y(box, sy, LV_ANIM_OFF);
}
}
/*
* 函数功能:应用指定深度框的选中(选中数据 + 跟随滚动 + 失效重绘)
* 入口参数:depth - 深度 uint8_t
* sel - 选中行 uint8_t
* 返回值:无
* 限定条件:该层行对象已建
* 函数说明:1. 选中行整行反色(白底黑字),箭头随反色变黑(参考图实测:选中行保留 ▶)
* 2. 非选中行透明底白字、白色箭头
* 3. 文本/箭头颜色设在行容器上,子 label 继承(text_color 为可继承属性)
* 限定条件:该层框体已建
* 函数说明:1. 行不是对象:选中只是数据,自绘回调绘制时定反色带/文字色
* 2. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view)
* 3. 失效框体:下一帧重绘
*/
static void view_apply_sel(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
/* 逐行置/清 CHECKED 态(选中行经样式整行反色) */
for (uint8_t i = 0; i < f->item_cnt; i++) {
bool is_sel = (i == sel);
if (is_sel) {
lv_obj_add_state(s_row[depth][i], LV_STATE_CHECKED);
}
else {
lv_obj_clear_state(s_row[depth][i], LV_STATE_CHECKED);
}
/* 文本色设在行上:文本/箭头子对象继承(选中黑、非选中白,箭头随反色) */
lv_obj_set_style_text_color(s_row[depth][i],
is_sel ? lv_color_black() : lv_color_white(), 0);
}
f->sel = sel;
view_scroll_to_sel(s_frame_box[depth], f); /* 选中行必在可见区内 */
lv_obj_invalidate(s_frame_box[depth]); /* 重绘框体(反色带/文字色绘制时决定) */
}
/*
* 函数功能:配置框体布局(纵向 flex 堆行 + 自动滚动条)
* 函数功能:配置框体滚动行为(自绘行无子对象,仅配置自动滚动条)
* 入口参数:box - 框体 lv_obj_t *
* 返回值:无
* 限定条件:box 已创建
* 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留)
*/
static void view_box_layout(lv_obj_t *box) {
/* 纵向 flex 堆行、顶对齐(行自框顶依次排列) */
lv_obj_set_flex_flow(box, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(box, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
/* 自动滚动条:仅当行内容超框高时出现(框内滚动) */
lv_obj_set_scrollbar_mode(box, LV_SCROLLBAR_MODE_AUTO);
lv_obj_add_style(box, &s_style_scrollbar, LV_PART_SCROLLBAR);
}
/*
* 函数功能:构建指定深度的框(几何现算 + 行对象创建 + 选中/滚动应用)
* 函数功能:构建指定深度的框(几何现算 + 行数据注册 + 选中/滚动应用)
* 入口参数:depth - 深度(0=根框) uint8_t
* node - 本框展示节点(行 = 其子项) uint8_t
* sel - 本框选中行 uint8_t
* scroll - 本框滚动偏移(px,0=顶部) uint16_t
* 返回值:无
* 限定条件:depth>0 时 depth-1 层框已建(父框几何是其对齐依据)
* 函数说明: 1. d=0:根框框体已存在,仅(重)建行对象
* 函数说明: 1. d=0:根框框体已存在,仅(重)注册行数据
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
@@ -332,6 +393,9 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t
lv_obj_set_pos(box, f->x, f->y);
lv_obj_set_size(box, f->w, f->h);
view_box_layout(box);
/* 自绘 + 虚拟内容高:回调 user_data 装层深度(回调内拆回) */
lv_obj_add_event_cb(box, view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)depth);
lv_obj_add_event_cb(box, view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)depth);
s_frame_box[depth] = box;
}
@@ -340,35 +404,23 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t
f->item_cnt = cnt;
f->sel = sel;
/* 行对象:条目现算(= parent==node 过滤),进时建、退时毁 */
lv_obj_clean(box);
/* 清空旧行指针(旧对象已随框体删除) */
/* 行数据:条目现算(= parent==node 过滤),注册节点下标、自绘(无对象) */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_node[depth][i] = 0u; /* 清旧(超 cnt 的行不绘制) */
}
/* 逐行建行对象 */
for (uint8_t i = 0; i < cnt; i++) {
if (!view_add_row(box, depth, i, kids[i], ui_menu_tab_has_children(kids[i]))) {
break; /* LVGL 池耗尽:停止建行(池有界,见设计 §7.5) */
}
s_row_node[depth][i] = kids[i];
}
/* 选中高亮 + 滚动位 */
view_apply_sel(depth, sel);
/* 滚动位:有保存先还原,再跟随滚动保证选中行可见 */
if (scroll > 0u) {
/* 有保存的滚动位:还原 */
lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF);
}
else if (cnt > 0u) {
/* 无保存滚动位:选中行对齐到可见区 */
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
}
view_apply_sel(depth, sel); /* 选中 + 跟随滚动 + 失效重绘 */
}
/*
* 函数功能:销毁指定深度的框(框体连同行对象,容器保留)
* 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留)
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:depth>0(根框持久不可销毁)
@@ -380,12 +432,12 @@ void ui_menu_view_frame_destroy(uint8_t depth) {
if (depth == 0u || !f->built) {
return; /* 根框持久不可销毁 */
}
lv_obj_delete(s_frame_box[depth]); /* 框体连行对象整体删除(容器保留) */
lv_obj_delete(s_frame_box[depth]); /* 框体(含自绘回调)整体删除,容器保留 */
s_frame_box[depth] = NULL;
/* 清空行指针与几何缓存 */
/* 清空行数据与几何缓存 */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
s_row_node[depth][i] = 0u;
}
f->built = 0u;
f->item_cnt = 0u;
@@ -402,7 +454,7 @@ void ui_menu_view_frame_destroy(uint8_t depth) {
* sel - 新选中行 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
* 函数说明:只改行状态与文字色 + 跟随滚动,不重建对象
* 函数说明:只改选中数据 + 跟随滚动、重绘框体,不重建对象
*/
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
menu_frame_t *f = &s_frame[depth];
@@ -411,10 +463,7 @@ void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) {
return;
}
f->sel = sel;
view_apply_sel(depth, sel);
/* 选中变化只改行状态 + 跟随滚动,不重建对象 */
lv_obj_scroll_to_view(s_row[depth][sel], LV_ANIM_OFF);
view_apply_sel(depth, sel); /* 选中数据 + 跟随滚动 + 失效(自绘决定反色带) */
}
/*
@@ -496,7 +545,7 @@ static void view_deco_line_apply(lv_obj_t *line, int32_t width) {
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空)
* 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0)
* 3. 行对象不在此建,由逻辑层调 ui_menu_view_frame_build 按需建
* 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行
*/
int ui_menu_view_init(void) {
/* 取活动屏幕:NULL = ui_port_init/sim_port_init 未完成、LVGL 未就绪 */
@@ -581,10 +630,13 @@ int ui_menu_view_init(void) {
lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y); /* 左上 (0,44):装饰/状态行之下 */
lv_obj_set_size(s_frame_box[0], w, h);
view_box_layout(s_frame_box[0]); /* 纵向 flex 堆行 + 自动滚动条(行对象后由 frame_build 添加) */
view_box_layout(s_frame_box[0]); /* 自动滚动条(行自绘,无子对象) */
/* 自绘 + 虚拟内容高:根框持久,回调仅此处挂一次 */
lv_obj_add_event_cb(s_frame_box[0], view_frame_draw_cb, LV_EVENT_DRAW_MAIN, (void *)(uintptr_t)0u);
lv_obj_add_event_cb(s_frame_box[0], view_frame_self_size_cb, LV_EVENT_GET_SELF_SIZE, (void *)(uintptr_t)0u);
/* 写根框几何缓存:子框右贴靠其右缘、顶对齐其选中行,均以此为准 */
s_frame[0].built = 1u; /* 框体就绪;行对象由逻辑层 frame_build(0) 创建 */
s_frame[0].built = 1u; /* 框体就绪;行数据由逻辑层 frame_build(0) 注册 */
s_frame[0].x = MENU_ROOT_BOX_X;
s_frame[0].y = MENU_ROOT_BOX_Y;
s_frame[0].w = w;
+8 -7
View File
@@ -2,13 +2,14 @@
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/根框装饰),
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-04
* 修改记录:
* 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合)
* 2026-09-08 王建锋 行改自绘(节点下标注册 + 框体 DRAW_MAIN 自绘 + 虚拟滚动),接口不变
*/
#ifndef UI_MENU_VIEW_H
#define UI_MENU_VIEW_H
@@ -31,7 +32,7 @@
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空)
* 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0)
* 3. 行对象不在此建,由逻辑层调 ui_menu_view_frame_build 按需建
* 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行
*/
int ui_menu_view_init(void);
@@ -45,14 +46,14 @@ int ui_menu_view_init(void);
lv_obj_t *ui_menu_view_container(uint8_t depth);
/*
* 函数功能:构建指定深度的框(几何现算 + 行对象创建 + 选中/滚动应用)
* 函数功能:构建指定深度的框(几何现算 + 行数据注册 + 选中/滚动应用)
* 入口参数:depth - 深度(0=根框) uint8_t
* node - 本框展示节点(行 = 其子项) uint8_t
* sel - 本框选中行 uint8_t
* scroll - 本框滚动偏移(px,0=顶部) uint16_t
* 返回值:无
* 限定条件:depth>0 时 depth-1 层框已建(父框几何是其对齐依据)
* 函数说明: 1. d=0:根框框体已存在,仅(重)建行对象
* 函数说明: 1. d=0:根框框体已存在,仅(重)注册行数据
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
@@ -60,7 +61,7 @@ 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);
/*
* 函数功能:销毁指定深度的框(框体连同行对象,容器保留)
* 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留)
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:depth>0(根框持久不可销毁)
@@ -74,7 +75,7 @@ void ui_menu_view_frame_destroy(uint8_t depth);
* sel - 新选中行 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
* 函数说明:只改行状态与文字色 + 跟随滚动,不重建对象
* 函数说明:只改选中数据 + 跟随滚动、重绘框体,不重建对象
*/
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel);
@@ -84,7 +85,7 @@ void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel);
* scroll - 滚动偏移(px) uint16_t
* 返回值:无
* 限定条件:该层框已建
* 函数说明:行对象重建后按此还原用户滚动位置
* 函数说明:行重建后按此还原用户滚动位置
*/
void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll);
+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 |