7708f12c10
按键召唤答的是按键前的旧帧:uiTask 还没消化键答案就发走了,新画面 只能等下一次周期召唤(280 ms/轮)才带得出——真机反馈 250-450 ms 的 主构成(根因账本 docs/子系统设计/屏板链路设计说明.md §7.1)。 改动(用户批准范围 A+B+C;屏板固件未动,决策三不变): - sb_link:按键召唤改为只入队不起发;uiTask 同拍重绘并组帧成功后交 凭据(sb_link_key_rendered),TIM3 1 kHz 的 sb_link_ms_tick 以 ≤1 ms 放行新帧。140 ms 截止(SB_KEY_RESP_MAX_MS,+59.6 ms 线上 <205 ms 屏板放弃线)保底发旧帧,"一召一答"契约不破;线忙时同旧丢单、 不留扣件。起发两中断源(USART1 IDLE/TIM3)共用 sb_tx_kick, "判忙→占位"在 PRIMASK 临界区原子完成。 - ui_task:排空注键挪到组帧之前,注键→即派即绘→组帧同拍闭环; 组帧返回 BSP_OK 才交重绘凭据(BUSY 拍帧还没反映按键)。 - ui_port:注键单槽改 8 深环(连按不再互相覆盖,满丢最旧+计数); 新增 ui_port_service_keys:同拍 lv_indev_read + lv_refr_now, 省掉 indev ≤20 ms + 刷新 ≤20 ms 两段等待。 - ui_menu_view:菜单移动选中只失效新旧两行带(黄金参考零像素变化), 滚动/异常回退整框失效。 - simulator:窗口循环 sleep 扣除本轮已耗时(原先无条件睡满 lv_timer_handler 返回值,叠加 Windows Sleep 15.6 ms 粒度放大观感)。 - 串口 TX 上报新增 dfok/dfold(凭据放行新帧数/超时保底旧帧数): dfold 连涨 = uiTask 被饿 ≥140 ms 的直接现场证据。 验证:test/host/sb_link 164/164(新增 18 条:不立刻起发/凭据放行/ 140 ms 截止保底/忙时不留扣件/陈旧凭据不放行等边界);菜单 8 状态 + 内容页 66 张黄金像素 PASS;Keil @build 0 错误 0 警告(两次); BOM 99 文件无漂移、代码规范、doc-links 本次文件零问题、cppcheck PASS。 文档:屏板规划文档已被主干收编进设计说明,延迟账本落 docs/子系统设计/屏板链路设计说明.md §7.1,并同步 §4 口径、§8 接口、 §9.1 单测台账 146→164、§10 坑账本补条。 RAM 台账(分支不跑 --write,并行分支提交约定):本笔盖戳行 RW_IRAM1 94,336 B (72.0%) / RW_CCM 57,376 B (87.9%),较上一台账行 +4,400 B——为距上次台账构建以来分支累计差(含批 3 状态灯等未盖戳 提交),非全部本笔引入;本次自身增量按对象计为 ui_port.o +12 B (8 深键环+丢计数+indev 指针)与 sb_link/o 数十字节级。 gate: only mem-budget/mem-numbers pending lead ledger backfill (合并落主干后 Lead 统一 --write 回填受管区)。
686 lines
29 KiB
C
686 lines
29 KiB
C
/*
|
||
* 模块名称:UI Menu View (菜单显示模型渲染层)
|
||
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
|
||
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
|
||
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/持久容器/框体),
|
||
* 行不是对象——框体 DRAW_MAIN 自绘(节点下标注册 + GET_SELF_SIZE 虚拟滚动);
|
||
* 不持有任何导航状态——状态由逻辑层 ui_menu.c 经本接口驱动
|
||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||
* 作者:王建锋
|
||
* 创建日期:2026-09-04
|
||
* 修改记录:
|
||
* 2026-09-04 王建锋 创建:自旧 ui_menu.c 单页模型拆分出显示层(高内聚低耦合)
|
||
* 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/行 对象开销)
|
||
* 2026-09-08 王建锋 4 层容器→1 个持久容器(焦点/键唯一入口;帧深度由逻辑层
|
||
* s_depth 决定,键回调 user_data=depth 冗余);根框装饰
|
||
* (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象)
|
||
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
|
||
* 2026-10-30 王建锋 按键提速:view_apply_sel 选中移动且未滚动时
|
||
* 只失效新旧两行竖带(lv_obj_invalidate_area),绘制量
|
||
* 从整框降到 ~2/7;滚动/首建仍整框兜底
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <string.h>
|
||
|
||
#include "ui_menu_view.h"
|
||
|
||
#include "ui_menu_tab.h"
|
||
|
||
#include "ui_deco.h"
|
||
|
||
#include "ui_style.h"
|
||
|
||
#include "ui_tokens.h"
|
||
|
||
#include "lvgl.h"
|
||
|
||
#define DBG_TAG "[UI_MENU_VIEW]"
|
||
#include "dbg_log.h"
|
||
|
||
/* 私有宏定义区 */
|
||
|
||
/*
|
||
* 布局常量(设备像素;参考图 02~07 实测 + 字体度量速查推导):
|
||
* 汉字步进 12px/字、▶ 步进 10px、行高 15(7 行×15+2 边框=107,与参考图根框一致);
|
||
* 行内留白取最小值 2px——最深 4 框级联(根 66 + 42 + 42 + 空框 6 - 3 共享边框)
|
||
* = 153px,须 < 屏宽 160
|
||
*/
|
||
#define MENU_ROW_H 15 /* 行高 */
|
||
#define MENU_ROW_TXT_TOP 1 /* 行内文字垂直居中偏移((15-13)/2,字体行高 13) */
|
||
#define MENU_ROW_PAD_L 2 /* 行内文字左留白(紧留白,保证级联总宽 < 160) */
|
||
#define MENU_ROW_PAD_R 2 /* 行内右侧留白 */
|
||
#define MENU_ARROW_GAP 2 /* 行内 文本→箭头 间距 */
|
||
#define MENU_FRAME_BORDER 1 /* 框边框宽(子框与父框共享该边框列) */
|
||
|
||
#define MENU_ROOT_BOX_X 0 /* 根框左(左对齐,贴左缘) */
|
||
#define MENU_ROOT_BOX_Y 44 /* 根框顶 */
|
||
#define MENU_ROOT_STATUS_X 17 /* 状态行左 */
|
||
#define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */
|
||
|
||
/* 私有类型定义区 */
|
||
|
||
/*
|
||
* 框几何缓存:位置/尺寸是"现算"的(不落导航状态),但子框对齐要用父框刚算出的值,
|
||
* 故按层缓存一份;sel 为最近一次应用的高亮(子框垂直对齐依据),由逻辑层经
|
||
* build/refresh 保持同步
|
||
*/
|
||
typedef struct {
|
||
uint8_t built; /* 框已建(框体 + 行对象存在):0/1(uint8_t 与系统存储一致) */
|
||
uint8_t sel; /* 最近应用的选中行 */
|
||
uint8_t item_cnt; /* 行数 */
|
||
int16_t x, y; /* 框左上角(含边框;16 位够:y 钳制前可为负) */
|
||
int16_t w, h; /* 框外廓尺寸(16 位够:最大 16×15+2=242) */
|
||
} menu_frame_t;
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
static lv_obj_t *s_cont; /* 单一持久全屏容器(焦点/键唯一入口;各层框均建其内;永不删) */
|
||
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]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */
|
||
|
||
/* 样式(静态,一次性初始化,常驻) */
|
||
/* flat(透明、无边框/圆角/内边距,剥离主题 card)取自共享样式表 ui_style.c:
|
||
* 与 ui_content_page / ui_standby 共用同一 const 对象,不再本地重复定义 */
|
||
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
|
||
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* 函数功能:初始化显示层样式(静态样式,仅执行一次)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:无
|
||
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距;常量样式,见上方定义)
|
||
* 2. frame 为所有级联框(含根框)的 1px 白边框
|
||
* 3. 选中行反色不用样式:自绘回调按 sel 直接画白带(行不是对象)
|
||
*/
|
||
static void view_styles_init(void) {
|
||
static bool inited = false;
|
||
|
||
/* 一次性初始化:已初始化则直接返回 */
|
||
if (inited) {
|
||
return;
|
||
}
|
||
inited = true;
|
||
|
||
/* frame:透明底 + 1px 白边框(所有级联框含根框) */
|
||
lv_style_init(&s_style_frame);
|
||
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
|
||
lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER);
|
||
lv_style_set_border_color(&s_style_frame, UI_COL_FG);
|
||
lv_style_set_radius(&s_style_frame, 0);
|
||
lv_style_set_pad_all(&s_style_frame, 0);
|
||
lv_style_set_pad_gap(&s_style_frame, 0);
|
||
|
||
/* scrollbar:白色 4px 宽(行超框高时自动出现) */
|
||
lv_style_init(&s_style_scrollbar);
|
||
lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER);
|
||
lv_style_set_bg_color(&s_style_scrollbar, UI_COL_FG);
|
||
lv_style_set_width(&s_style_scrollbar, UI_SCROLLBAR_W);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:计算框外廓宽(内容自适应)
|
||
* 入口参数:kids - 子节点下标数组(各行内容) const uint8_t *
|
||
* cnt - 条目数 uint8_t
|
||
* 返回值:框外廓宽 px int32_t = 最宽行内容 + 左右边框
|
||
* 限定条件:LVGL 已初始化(字体度量可用)
|
||
* 函数说明:行内容宽 = 左留白 + 文本 + (菜单 ? 间距 + ▶ : 0) + 右留白;
|
||
* 文本/▶ 宽取字体实际步进(汉字 12px、▶ 10px);
|
||
* 注意本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen
|
||
*/
|
||
static int32_t view_frame_width(const uint8_t *kids, uint8_t cnt) {
|
||
const lv_font_t *font = LV_FONT_DEFAULT;
|
||
int32_t w = 0;
|
||
|
||
/* 遍历各行,取内容宽最大值 */
|
||
for (uint8_t i = 0; i < cnt; i++) {
|
||
const char *name = ui_menu_tab_name(kids[i]);
|
||
|
||
/* 量文本实际步进(本 LVGL 版 length=0 意为"不测量",须传 strlen) */
|
||
int32_t tw = lv_text_get_width(name, (uint32_t)strlen(name), font, 0);
|
||
int32_t cw = MENU_ROW_PAD_L + tw + MENU_ROW_PAD_R;
|
||
|
||
/* 菜单行(有子项):加箭头间距 + ▶ 宽 */
|
||
if (ui_menu_tab_has_children(kids[i])) {
|
||
cw += MENU_ARROW_GAP + lv_text_get_width("▶", (uint32_t)strlen("▶"), font, 0);
|
||
}
|
||
|
||
/* 取最宽行 */
|
||
if (cw > w) {
|
||
w = cw;
|
||
}
|
||
}
|
||
|
||
/* 加左右边框 */
|
||
return w + 2 * MENU_FRAME_BORDER;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
|
||
* 入口参数:a - 区域 1 const lv_area_t *
|
||
* b - 区域 2 const lv_area_t *
|
||
* 返回值:true - 相交;false - 不相交
|
||
* 限定条件:无
|
||
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 lv_area.c 同语义)
|
||
*/
|
||
static bool areas_intersect(const lv_area_t *a, const lv_area_t *b) {
|
||
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
|
||
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
|
||
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
|
||
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
|
||
|
||
return (x1 <= x2 && y1 <= y2);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:框体虚拟内容高上报(滚动范围 = 内容高 - 内容区高)
|
||
* 入口参数:e - 事件指针(user_data = 层深度) lv_event_t *
|
||
* 返回值:无
|
||
* 限定条件:事件为 LV_EVENT_GET_SELF_SIZE,布局阶段由 LVGL 调用
|
||
* 函数说明:行不是子对象,经 GET_SELF_SIZE 虚报内容高,
|
||
* LVGL 据此算滚动范围与自动滚动条(替代旧"行对象撑内容高")
|
||
*/
|
||
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);
|
||
|
||
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;
|
||
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 ? UI_COL_SEL_FG : UI_COL_FG;
|
||
|
||
row_area.x1 = area.x1;
|
||
row_area.x2 = area.x2;
|
||
row_area.y1 = row_y;
|
||
row_area.y2 = row_y + MENU_ROW_H - 1;
|
||
|
||
/* 行不在重绘区:跳过(culling) */
|
||
if (!areas_intersect(&row_area, &layer->_clip_area)) {
|
||
continue;
|
||
}
|
||
|
||
/* 选中行:整行反色(白带) */
|
||
if (is_sel) {
|
||
lv_draw_rect_dsc_t fd;
|
||
lv_draw_rect_dsc_init(&fd);
|
||
fd.base.layer = layer;
|
||
fd.bg_color = UI_COL_SEL_BG;
|
||
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. 选中行滚入可见区(手动计算,无行对象可供 scroll_to_view)
|
||
* 3. 失效范围按改动面收敛(2026-10-30 按键提速 C):
|
||
* · 选中变了且**没发生滚动** → 只有新旧两行外观变化,
|
||
* 只脏这两行组成的竖带(根框 107 px 高、行高 15 px,绘制量
|
||
* 从整框降到 ~2/7;自绘回调本就有 clip 剔除,带外行直接跳过)
|
||
* · 发生滚动 / 首次应用(新旧同号)→ 整框失效兜底
|
||
* 4. 失效区用绝对坐标,与自绘回调同一坐标框架
|
||
* (content_coords 起算、减滚动偏移)
|
||
*/
|
||
static void view_apply_sel(uint8_t depth, uint8_t sel) {
|
||
menu_frame_t *f = &s_frame[depth];
|
||
lv_obj_t *box = s_frame_box[depth];
|
||
uint8_t sel_old = f->sel;
|
||
int32_t sy_before = lv_obj_get_scroll_y(box);
|
||
|
||
f->sel = sel;
|
||
view_scroll_to_sel(box, f); /* 选中行必在可见区内 */
|
||
|
||
if ((sel_old != sel) && (lv_obj_get_scroll_y(box) == sy_before)) {
|
||
lv_area_t ca;
|
||
lv_area_t band;
|
||
int32_t y_old;
|
||
int32_t y_new;
|
||
|
||
lv_obj_get_content_coords(box, &ca);
|
||
y_old = ca.y1 + (int32_t)sel_old * MENU_ROW_H - sy_before;
|
||
y_new = ca.y1 + (int32_t)sel * MENU_ROW_H - sy_before;
|
||
|
||
band.x1 = ca.x1;
|
||
band.x2 = ca.x2;
|
||
band.y1 = (y_old < y_new) ? y_old : y_new;
|
||
band.y2 = (y_old > y_new) ? y_old : y_new;
|
||
band.y2 += (MENU_ROW_H - 1);
|
||
if (band.y1 < ca.y1) {
|
||
band.y1 = ca.y1; /* 旧选中行可能被框顶裁掉(级联框贴屏上沿时) */
|
||
}
|
||
if (band.y2 > ca.y2) {
|
||
band.y2 = ca.y2; /* 同理框底 */
|
||
}
|
||
if (band.y1 <= band.y2) {
|
||
lv_obj_invalidate_area(box, &band);
|
||
return;
|
||
}
|
||
}
|
||
lv_obj_invalidate(box); /* 兜底:有滚动或首次建立,整框重绘 */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:配置框体滚动行为(自绘行无子对象,仅配置自动滚动条)
|
||
* 入口参数:box - 框体 lv_obj_t *
|
||
* 返回值:无
|
||
* 限定条件:box 已创建
|
||
* 函数说明:行内容超框高时框内滚动(本菜单最深 9 行不触发,机制为更长列表保留)
|
||
*/
|
||
static void view_box_layout(lv_obj_t *box) {
|
||
/* 自动滚动条:仅当行内容超框高时出现(框内滚动) */
|
||
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:根框框体已存在,仅(重)注册行数据
|
||
* 2. d>0:右贴靠父框右缘;顶边=父框选中行行顶,超屏底则
|
||
* 上移使本框底边对齐父框选中行行底,仍超顶则钳制顶部并改框内滚动
|
||
* 3. 框宽 = 最宽行(文本 + ▶ + 左右留白)+ 边框;条目现算不落地
|
||
*/
|
||
void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll) {
|
||
menu_frame_t *f = &s_frame[depth];
|
||
lv_obj_t *box;
|
||
uint8_t kids[MENU_ITEM_MAX];
|
||
uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX);
|
||
|
||
if (depth == 0u) {
|
||
box = s_frame_box[0]; /* 根框框体持久(view_init 已建) */
|
||
}
|
||
else {
|
||
if (f->built) {
|
||
ui_menu_view_frame_destroy(depth); /* 防御:重入 = 先毁后建 */
|
||
}
|
||
const menu_frame_t *p = &s_frame[depth - 1u];
|
||
|
||
/* X:右贴靠——子框左缘 = 父框右缘(共享边框列、零间隙) */
|
||
f->x = p->x + p->w - MENU_FRAME_BORDER;
|
||
|
||
/* Y:顶对齐父框"选中行"行顶;(顶边+框高) 超屏底 → 上移使本框底边
|
||
* 对齐父框"选中行"行底(顶部不钳制,可叠到状态行区);
|
||
* 仍超顶(比整屏高)→ 钳制顶部,框高截到可见区、行改框内滚动 */
|
||
int32_t row_top = p->y + MENU_FRAME_BORDER + (int32_t)p->sel * MENU_ROW_H;
|
||
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER;
|
||
f->y = row_top;
|
||
if (f->y + h > MENU_SCREEN_H) {
|
||
f->y = row_top + MENU_ROW_H - h;
|
||
}
|
||
if (f->y < 0) {
|
||
f->y = 0;
|
||
}
|
||
if (f->y + h > MENU_SCREEN_H) {
|
||
h = MENU_SCREEN_H - f->y;
|
||
}
|
||
f->h = h;
|
||
f->w = (cnt > 0u) ? view_frame_width(kids, cnt)
|
||
: (2 * MENU_FRAME_BORDER + MENU_ROW_PAD_L + MENU_ROW_PAD_R);
|
||
|
||
box = lv_obj_create(s_cont);
|
||
if (box == NULL) {
|
||
return; /* LVGL 池耗尽:框未建,状态回滚 */
|
||
}
|
||
lv_obj_add_style(box, &ui_style_flat, 0);
|
||
lv_obj_add_style(box, &s_style_frame, 0);
|
||
lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||
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;
|
||
}
|
||
|
||
/* 更新框几何缓存(子框对齐的依据) */
|
||
f->built = 1u;
|
||
f->item_cnt = cnt;
|
||
f->sel = sel;
|
||
|
||
/* 行数据:条目现算(= parent==node 过滤),注册节点下标、自绘(无对象) */
|
||
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
|
||
s_row_node[depth][i] = 0u; /* 清旧(超 cnt 的行不绘制) */
|
||
}
|
||
for (uint8_t i = 0; i < cnt; i++) {
|
||
s_row_node[depth][i] = kids[i];
|
||
}
|
||
|
||
/* 滚动位:有保存先还原,再跟随滚动保证选中行可见 */
|
||
if (scroll > 0u) {
|
||
lv_obj_scroll_to_y(box, (int32_t)scroll, LV_ANIM_OFF);
|
||
}
|
||
view_apply_sel(depth, sel); /* 选中 + 跟随滚动 + 失效重绘 */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:销毁指定深度的框(框体连自绘回调整体删除,容器保留)
|
||
* 入口参数:depth - 深度 uint8_t
|
||
* 返回值:无
|
||
* 限定条件:depth>0(根框持久不可销毁)
|
||
* 函数说明:返回(关最右框)时调用,LVGL 池内存随即回落
|
||
*/
|
||
void ui_menu_view_frame_destroy(uint8_t depth) {
|
||
menu_frame_t *f = &s_frame[depth];
|
||
|
||
if (depth == 0u || !f->built) {
|
||
return; /* 根框持久不可销毁 */
|
||
}
|
||
lv_obj_delete(s_frame_box[depth]); /* 框体(含自绘回调)整体删除,容器保留 */
|
||
s_frame_box[depth] = NULL;
|
||
|
||
/* 清空行数据与几何缓存 */
|
||
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
|
||
s_row_node[depth][i] = 0u;
|
||
}
|
||
f->built = 0u;
|
||
f->item_cnt = 0u;
|
||
f->sel = 0u;
|
||
f->x = 0;
|
||
f->y = 0;
|
||
f->w = 0;
|
||
f->h = 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:刷新指定深度框的选中高亮
|
||
* 入口参数:depth - 深度 uint8_t
|
||
* sel - 新选中行 uint8_t
|
||
* 返回值:无
|
||
* 限定条件:ui_menu_view_frame_build(depth,…) 已完成
|
||
* 函数说明:只改选中数据 + 跟随滚动、重绘框体,不重建对象
|
||
*/
|
||
void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel) {
|
||
const menu_frame_t *f = &s_frame[depth];
|
||
|
||
/* 未建或越界直接返回 */
|
||
if (!f->built || depth >= MENU_PAGE_MAX || sel >= f->item_cnt) {
|
||
return;
|
||
}
|
||
view_apply_sel(depth, sel); /* 选中数据 + 跟随滚动 + 失效(自绘决定反色带) */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:应用指定深度框的滚动偏移
|
||
* 入口参数:depth - 深度 uint8_t
|
||
* scroll - 滚动偏移(px) uint16_t
|
||
* 返回值:无
|
||
* 限定条件:该层框已建
|
||
* 函数说明:行对象重建后按此还原用户滚动位置
|
||
*/
|
||
void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll) {
|
||
/* 未建框直接返回 */
|
||
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
|
||
return;
|
||
}
|
||
lv_obj_scroll_to_y(s_frame_box[depth], (int32_t)scroll, LV_ANIM_OFF);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读取指定深度框当前滚动偏移
|
||
* 入口参数:depth - 深度 uint8_t
|
||
* 返回值:滚动偏移(px) uint16_t 未建框返回 0
|
||
* 限定条件:无
|
||
* 函数说明:跟随滚动后由逻辑层读回,写入导航状态持久化
|
||
*/
|
||
uint16_t ui_menu_view_get_scroll(uint8_t depth) {
|
||
/* 未建框返回 0 */
|
||
if (depth >= MENU_PAGE_MAX || !s_frame[depth].built || s_frame_box[depth] == NULL) {
|
||
return 0u;
|
||
}
|
||
return (uint16_t)lv_obj_get_scroll_y(s_frame_box[depth]);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:焦点置于持久容器
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:ui_menu_view_init() 已完成且容器已加入输入组
|
||
* 函数说明:焦点=唯一键入口(框深度由逻辑层 s_depth 决定);
|
||
* 进/退时由逻辑层调用(防御性:保证容器恒为焦点对象)
|
||
*/
|
||
void ui_menu_view_focus(void) {
|
||
if (s_cont != NULL) {
|
||
lv_group_focus_obj(s_cont);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:取持久容器
|
||
* 入口参数:无
|
||
* 返回值:容器对象 lv_obj_t * 未初始化返回 NULL
|
||
* 限定条件:ui_menu_view_init() 已完成
|
||
* 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入)
|
||
*/
|
||
lv_obj_t *ui_menu_view_container(void) {
|
||
return s_cont;
|
||
}
|
||
|
||
/*
|
||
* 根框装饰(浅 V):几何/绘制/culling 由共享 ui_deco 模块承载(无对象,容器自绘);
|
||
* 状态行 culling 区为本模块私有(文本约 104px 宽 + 余量)
|
||
*/
|
||
static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区 */
|
||
|
||
/*
|
||
* 函数功能:自绘根框装饰 + 状态行(容器 DRAW_MAIN;替代旧 4 个 LVGL 对象)
|
||
* 入口参数:e - 事件指针 lv_event_t *
|
||
* 返回值:无
|
||
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,容器已建
|
||
* 函数说明: 1. 浅 V:共享 ui_deco 模块(两腿 3px / 水平线 2px,绘制次序左腿、右腿、水平线)
|
||
* 2. 状态行:静态字符串、白色,与旧 label 同字体同位置
|
||
* 3. 重绘区不覆盖对应 culling 区则跳过(culling)
|
||
*/
|
||
static void view_deco_draw_cb(lv_event_t *e) {
|
||
lv_layer_t *layer = lv_event_get_layer(e);
|
||
|
||
/* "浅 V"装饰:共享 ui_deco 模块(几何/绘制次序/culling 均在其内) */
|
||
ui_deco_draw(layer, &layer->_clip_area);
|
||
|
||
/* 状态行:当前模式(静态文本) */
|
||
if (areas_intersect(&s_status_area, &layer->_clip_area)) {
|
||
lv_draw_label_dsc_t sd;
|
||
|
||
lv_draw_label_dsc_init(&sd);
|
||
sd.base.layer = layer;
|
||
sd.text = "当前模式 : 无模式";
|
||
sd.text_static = 1; /* Flash 字符串:不拷内存 */
|
||
sd.color = UI_COL_FG;
|
||
sd.ofs_x = 0; /* 区域原点即状态行左上 (17,20) */
|
||
sd.ofs_y = 0;
|
||
lv_draw_label(layer, &sd, &s_status_area);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:初始化显示层(样式 + 持久容器 + 根框框体;装饰/状态行自绘)
|
||
* 入口参数:无
|
||
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
|
||
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
|
||
* 函数说明:1. 持久容器:全屏覆盖、持久不删(焦点/键唯一入口;各层框均建其内)
|
||
* 2. 根框装饰(浅 V + 状态行)由容器自绘(无对象);根框框体持久
|
||
* 3. 行数据由逻辑层经 ui_menu_view_frame_build 注册,框体自绘行
|
||
*/
|
||
int ui_menu_view_init(void) {
|
||
/* 取活动屏幕:NULL = ui_port_init/sim_port_init 未完成、LVGL 未就绪 */
|
||
lv_obj_t *scr = lv_screen_active();
|
||
|
||
if (scr == NULL) {
|
||
return -1;
|
||
}
|
||
|
||
/* 样式一次性初始化(静态样式、常驻,仅此处建一次) */
|
||
view_styles_init();
|
||
|
||
/* 读屏幕实际分辨率(不硬编码,MCU/模拟器两端共用本代码) */
|
||
lv_display_t *disp = lv_display_get_default();
|
||
int32_t sw = lv_display_get_horizontal_resolution(disp);
|
||
int32_t sh = lv_display_get_vertical_resolution(disp);
|
||
|
||
/* 屏幕根容器:铺满屏幕;子对象全部绝对定位(级联框须同屏叠加,不用 flex 堆叠) */
|
||
lv_obj_t *menu = lv_obj_create(scr);
|
||
if (menu == NULL) {
|
||
return -1;
|
||
}
|
||
lv_obj_add_style(menu, &ui_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */
|
||
lv_obj_set_size(menu, sw, sh);
|
||
|
||
/* 持久容器:全屏覆盖、永不删(焦点/键唯一入口;各层框均建其内,防悬空) */
|
||
s_cont = lv_obj_create(menu);
|
||
if (s_cont == NULL) {
|
||
return -1;
|
||
}
|
||
lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框 */
|
||
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */
|
||
lv_obj_set_pos(s_cont, 0, 0);
|
||
lv_obj_set_size(s_cont, sw, sh); /* 全屏覆盖 */
|
||
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
|
||
/* 根框装饰(浅 V + 状态行):容器自绘(无对象),回调仅此处挂一次 */
|
||
lv_obj_add_event_cb(s_cont, view_deco_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
|
||
|
||
/* 根框框体:持久;宽 = 内容自适应(主界面 node 0 的子项) */
|
||
uint8_t kids[MENU_ITEM_MAX];
|
||
uint8_t cnt = ui_menu_tab_children(0u, kids, MENU_ITEM_MAX); /* 主界面子项(7 个 L1 条目) */
|
||
int32_t w = view_frame_width(kids, cnt); /* 宽 = 最宽行内容 + 左右边框(当前 66) */
|
||
int32_t h = (int32_t)cnt * MENU_ROW_H + 2 * MENU_FRAME_BORDER; /* 高 = 行数×行高 + 上下边框(当前 107) */
|
||
|
||
s_frame_box[0] = lv_obj_create(s_cont);
|
||
if (s_frame_box[0] == NULL) {
|
||
return -1;
|
||
}
|
||
lv_obj_add_style(s_frame_box[0], &ui_style_flat, 0); /* 透明底 */
|
||
lv_obj_add_style(s_frame_box[0], &s_style_frame, 0); /* 1px 白边框 */
|
||
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]); /* 自动滚动条(行自绘,无子对象) */
|
||
/* 自绘 + 虚拟内容高:根框持久,回调仅此处挂一次 */
|
||
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].x = MENU_ROOT_BOX_X;
|
||
s_frame[0].y = MENU_ROOT_BOX_Y;
|
||
s_frame[0].w = w;
|
||
s_frame[0].h = h;
|
||
|
||
DBG_INFO("view init: root frame w=%d h=%d", (int)w, (int)h);
|
||
return 0;
|
||
}
|