Files
STM32F4-Base/Drivers/BSP/UI/ui_menu_view.c
T
2026-09-08 18:36:01 +08:00

596 lines
25 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI Menu View (菜单显示模型渲染层)
* 模块功能:以 LVGL 基元控件实现《菜单页面设计.md》§四⑧"横向级联子框"显示模型:
* 右贴靠(子框左缘=父框右缘、共享边框列)+ 框宽内容自适应 +
* 顶对齐/底对齐上移;持有全部 LVGL 对象(样式/4 层容器/框/行/根框装饰),
* 不持有任何导航状态——状态由逻辑层 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 颜色继承)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_menu_view.h"
#include "ui_menu_tab.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 /* 状态行顶 */
#define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */
#define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽 */
/* 私有类型定义区 */
/*
* 框几何缓存:位置/尺寸是"现算"的(不落导航状态),但子框对齐要用父框刚算出的值,
* 故按层缓存一份;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[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 menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */
static lv_obj_t *s_root_deco = NULL; /* 根框装饰:顶部"浅 V"水平段(两腿为临时对象,样式同源) */
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 宽 */
/* 函数定义区 */
/*
* 函数功能:初始化显示层样式(静态样式,仅执行一次)
* 入口参数:无
* 返回值:无
* 限定条件:无
* 函数说明: 1. flat 剥离主题给对象附加的卡片样式(边框/圆角/内边距)
* 2. frame 为所有级联框(含根框)的 1px 白边框
* 3. selected 为选中行整行反色(1bpp 白底);文字色在刷新时显式置黑
*/
static void view_styles_init(void) {
static bool inited = false;
/* 一次性初始化:已初始化则直接返回 */
if (inited) {
return;
}
inited = true;
/* flat:透明、无边框/圆角/内边距(剥离主题附加的卡片样式) */
lv_style_init(&s_style_flat);
lv_style_set_bg_opa(&s_style_flat, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_flat, 0);
lv_style_set_radius(&s_style_flat, 0);
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);
lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER);
lv_style_set_border_color(&s_style_frame, lv_color_white());
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, lv_color_white());
lv_style_set_width(&s_style_scrollbar, 4);
}
/*
* 函数功能:计算框外廓宽(内容自适应)
* 入口参数: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;
}
/*
* 函数功能:在框体上创建一条条目行(行容器 + 文本 + 可选箭头)
* 入口参数: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 做行内绝对定位,是行的子对象、不单独存
*/
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);
/* 挂选中行反色样式(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;
}
/*
* 函数功能:应用指定深度框的选中高亮(整行反色 + 文字/箭头颜色)
* 入口参数:depth - 深度 uint8_t
* sel - 选中行 uint8_t
* 返回值:无
* 限定条件:该层行对象已建
* 函数说明:1. 选中行整行反色(白底黑字),箭头随反色变黑(参考图实测:选中行保留 ▶)
* 2. 非选中行透明底白字、白色箭头
* 3. 文本/箭头颜色设在行容器上,子 label 继承(text_color 为可继承属性)
*/
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);
}
}
/*
* 函数功能:配置框体布局(纵向 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:根框框体已存在,仅(重)建行对象
* 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[depth]);
if (box == NULL) {
return; /* LVGL 池耗尽:框未建,状态回滚 */
}
lv_obj_add_style(box, &s_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);
s_frame_box[depth] = box;
}
/* 更新框几何缓存(子框对齐的依据) */
f->built = 1u;
f->item_cnt = cnt;
f->sel = sel;
/* 行对象:条目现算(= parent==node 过滤),进时建、退时毁 */
lv_obj_clean(box);
/* 清空旧行指针(旧对象已随框体删除) */
for (uint8_t i = 0; i < MENU_ITEM_MAX; i++) {
s_row[depth][i] = NULL;
}
/* 逐行建行对象 */
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) */
}
}
/* 选中高亮 + 滚动位 */
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);
}
}
/*
* 函数功能:销毁指定深度的框(框体连同行对象,容器保留)
* 入口参数: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[depth][i] = NULL;
}
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) {
menu_frame_t *f = &s_frame[depth];
/* 未建或越界直接返回 */
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);
}
/*
* 函数功能:应用指定深度框的滚动偏移
* 入口参数: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]);
}
/*
* 函数功能:焦点移至指定深度的容器
* 入口参数:depth - 深度 uint8_t
* 返回值:无
* 限定条件:ui_menu_view_init() 已完成且容器已加入输入组
* 函数说明:焦点=最右框;进/退时由逻辑层调用
*/
void ui_menu_view_focus(uint8_t depth) {
if (depth < MENU_PAGE_MAX && s_cont[depth] != NULL) {
/* 焦点移到目标深度容器 */
lv_group_focus_obj(s_cont[depth]);
}
}
/*
* 函数功能:取指定深度的持久容器
* 入口参数:depth - 深度 0..MENU_PAGE_MAX-1 uint8_t
* 返回值:容器对象 lv_obj_t * 越界返回 NULL
* 限定条件:ui_menu_view_init() 已完成
* 函数说明:逻辑层在容器上绑定输入组与键事件(显示层不碰输入)
*/
lv_obj_t *ui_menu_view_container(uint8_t depth) {
return (depth < MENU_PAGE_MAX) ? s_cont[depth] : NULL;
}
/*
* 函数功能:应用主界面顶部"浅 V"装饰线段的公共样式
* 入口参数:line - lv_line 对象;width - 线宽(px)
* 返回值:无
* 限定条件:line 已创建且已设点集
* 函数说明:绝对定位于 (0,0)、忽略布局、白色实线、指定线宽
*/
static void view_deco_line_apply(lv_obj_t *line, int32_t width) {
lv_obj_add_flag(line, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(line, 0, 0);
lv_obj_set_size(line, MENU_SCREEN_W, 20);
lv_obj_set_style_line_width(line, width, 0);
lv_obj_set_style_line_color(line, lv_color_white(), 0);
lv_obj_set_style_line_opa(line, LV_OPA_COVER, 0);
}
/*
* 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体)
* 入口参数:无
* 返回值:0 - 成功,其他 - 失败(对象创建失败)
* 限定条件:LVGL 已初始化且活动屏幕存在(ui_port_init / sim_port_init 已完成)
* 函数说明:1. 4 层容器每深度一个、全屏覆盖、持久不删(焦点/键入口,防悬空)
* 2. 根框装饰(浅 V + 状态行)与根框框体只属根层(d=0)
* 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, &s_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */
lv_obj_set_size(menu, sw, sh);
/* 4 层持久容器:每深度一个、全屏覆盖、永不删(焦点/键入口,防悬空) */
for (uint8_t d = 0; d < MENU_PAGE_MAX; d++) {
s_cont[d] = lv_obj_create(menu);
if (s_cont[d] == NULL) {
return -1;
}
lv_obj_add_style(s_cont[d], &s_style_flat, 0); /* 透明、无边框 */
lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */
lv_obj_set_pos(s_cont[d], 0, 0);
lv_obj_set_size(s_cont[d], sw, sh); /* 全屏覆盖 */
lv_obj_add_flag(s_cont[d], LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
}
/* 根框装饰:顶部"开口浅 V"(两腿加粗、水平线细)+ 状态行(绝对定位于 d=0 容器)
* 1bpp 下斜线边缘易被阈值吃掉,腿须比水平线粗才接近参考图 */
{
static const lv_point_precise_t deco_leg_l[] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */
static const lv_point_precise_t deco_hoz[] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */
static const lv_point_precise_t deco_leg_r[] = { {158, 7}, {145, 17} }; /* 右腿 */
lv_obj_t *leg_l = lv_line_create(s_cont[0]);
lv_obj_t *leg_r = lv_line_create(s_cont[0]);
s_root_deco = lv_line_create(s_cont[0]); /* 仅水平线留存句柄;两腿建后无需再引用 */
if (leg_l == NULL || leg_r == NULL || s_root_deco == NULL) {
return -1;
}
lv_line_set_points(leg_l, deco_leg_l, 2);
lv_line_set_points(leg_r, deco_leg_r, 2);
lv_line_set_points(s_root_deco, deco_hoz, 2);
view_deco_line_apply(leg_l, MENU_DECO_LEG_W); /* 腿:3px 粗 */
view_deco_line_apply(leg_r, MENU_DECO_LEG_W);
view_deco_line_apply(s_root_deco, MENU_DECO_HORIZ_W); /* 水平线:2px 细 */
}
/* 状态行:当前模式(顶部区域,根框上方) */
s_root_status = lv_label_create(s_cont[0]);
if (s_root_status == NULL) {
return -1;
}
lv_obj_add_style(s_root_status, &s_style_flat, 0);
lv_obj_add_flag(s_root_status, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_root_status, MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y);
lv_label_set_text_static(s_root_status, "当前模式 : 无模式"); /* 静态字符串,不拷内存 */
/* 根框框体:持久;宽 = 内容自适应(主界面 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[0]);
if (s_frame_box[0] == NULL) {
return -1;
}
lv_obj_add_style(s_frame_box[0], &s_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]); /* 纵向 flex 堆行 + 自动滚动条(行对象后由 frame_build 添加) */
/* 写根框几何缓存:子框右贴靠其右缘、顶对齐其选中行,均以此为准 */
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;
}