diff --git a/Drivers/BSP/UI/ui_menu_view.c b/Drivers/BSP/UI/ui_menu_view.c index 1ad0e38c..21eb73b6 100644 --- a/Drivers/BSP/UI/ui_menu_view.c +++ b/Drivers/BSP/UI/ui_menu_view.c @@ -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; diff --git a/Drivers/BSP/UI/ui_menu_view.h b/Drivers/BSP/UI/ui_menu_view.h index c1f3f394..320ac41a 100644 --- a/Drivers/BSP/UI/ui_menu_view.h +++ b/Drivers/BSP/UI/ui_menu_view.h @@ -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); diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 2217f87c..8579f729 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -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]` ## 五、设计决策与考量(为什么这样做) diff --git a/docs/UI 设计/菜单页面设计.md b/docs/UI 设计/菜单页面设计.md index bd03665a..ae98d5d3 100644 --- a/docs/UI 设计/菜单页面设计.md +++ b/docs/UI 设计/菜单页面设计.md @@ -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 |