重构显示逻辑为 MVP 架构,进行显示模块的解耦
This commit is contained in:
447
src/Drv/menu/view/menu_layout.c
Normal file
447
src/Drv/menu/view/menu_layout.c
Normal file
@@ -0,0 +1,447 @@
|
||||
#include <stddef.h>
|
||||
#include "menu_layout.h"
|
||||
#include "utf8.h"
|
||||
|
||||
/******************************************************************************
|
||||
* 模块: menu_layout.c
|
||||
* 职责: 根据菜单树结构计算各级菜单在 LCD 上的显示矩形区域
|
||||
*
|
||||
* 设计要点:
|
||||
* 1) 采用“先算当前层,再递归/级联算下一层”的方式布点;
|
||||
* 2) 每级菜单都维护:
|
||||
* - ptFirst[level]: 当前层循环链表的起点;
|
||||
* - ptIndex[level]: 当前层遍历游标;
|
||||
* - byMenuNum[level]: 当前层项数量(或当前位置计数);
|
||||
* 3) 对 Y 方向越界提供多级回退策略(向上翻转/贴顶重排),尽量保证菜单可见。
|
||||
*****************************************************************************/
|
||||
|
||||
/* 读取菜单节点导航指针集合(本文件局部辅助,避免 core 泄漏行为函数)。 */
|
||||
static MenuLinks MenuLayout_GetLinks(const MenuItem *item)
|
||||
{
|
||||
MenuLinks links;
|
||||
links.higher = item->ptHigher;
|
||||
links.lower = item->ptLower;
|
||||
links.before = item->ptBefore;
|
||||
links.behind = item->ptBehind;
|
||||
return links;
|
||||
}
|
||||
|
||||
/* 读取菜单节点矩形(本文件局部辅助)。 */
|
||||
static MenuRect MenuLayout_GetRect(const MenuItem *item)
|
||||
{
|
||||
MenuRect rect;
|
||||
rect.wSPosX = item->wSPosX;
|
||||
rect.wSPosY = item->wSPosY;
|
||||
rect.wEPosX = item->wEPosX;
|
||||
rect.wEPosY = item->wEPosY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
/* 给菜单项写入矩形坐标的轻量封装,避免到处重复构造 MenuRect。 */
|
||||
static void MenuLayout_SetRect(MenuItem *item, uint16_t sx, uint16_t sy, uint16_t ex, uint16_t ey)
|
||||
{
|
||||
item->wSPosX = sx;
|
||||
item->wSPosY = sy;
|
||||
item->wEPosX = ex;
|
||||
item->wEPosY = ey;
|
||||
}
|
||||
|
||||
/* 将理论挂接行压缩映射到可视区,避免超出可显示行数。 */
|
||||
static uint16_t MenuLayout_MapMenuPos(uint16_t menuPos, uint8_t itemNum, uint8_t maxNum)
|
||||
{
|
||||
if ((itemNum > maxNum) && (menuPos >= maxNum))
|
||||
{
|
||||
return (uint16_t)(maxNum - (itemNum - menuPos));
|
||||
}
|
||||
return menuPos;
|
||||
}
|
||||
|
||||
/* 子菜单 X 方向统一规则:从父框右边展开。 */
|
||||
static void MenuLayout_CalcSubMenuX(const MenuRect *parentRect,
|
||||
uint8_t maxLen,
|
||||
const MenuLayoutConfig *config,
|
||||
uint16_t *startX,
|
||||
uint16_t *endX)
|
||||
{
|
||||
*startX = parentRect->wEPosX;
|
||||
*endX = (uint16_t)(*startX + maxLen * config->menuWidth + config->menuXAdd);
|
||||
}
|
||||
|
||||
/* 尝试将菜单框整体翻转到上方显示;成功返回 1,失败返回 0。 */
|
||||
static uint8_t MenuLayout_TryFlipUp(uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
uint8_t itemNum,
|
||||
uint16_t flipMinY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t candidateEndY;
|
||||
uint16_t temp;
|
||||
|
||||
candidateEndY = (uint16_t)(*startY - (itemNum - 1) * config->lineHeight - config->menuYAdd);
|
||||
if ((candidateEndY > flipMinY) && (candidateEndY < config->menuYMax))
|
||||
{
|
||||
temp = *startY;
|
||||
*startY = candidateEndY;
|
||||
*endY = (uint16_t)(temp + config->lineHeight);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Sub2 兜底:回退到顶层起始 Y,并在底部做截断。 */
|
||||
static void MenuLayout_FallbackClampToTop(const tagPMenuItem *ptIndex,
|
||||
uint8_t itemNum,
|
||||
uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
*startY = ptIndex[0]->wSPosY;
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
if (*endY > config->menuYMax)
|
||||
{
|
||||
*endY = config->menuYMax;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sub1 兜底:从顶层起始 Y 逐行上探,直到菜单框不越界。 */
|
||||
static void MenuLayout_FallbackProbeUp(const tagPMenuItem *ptIndex,
|
||||
uint8_t itemNum,
|
||||
uint16_t *startY,
|
||||
uint16_t *endY,
|
||||
const MenuLayoutConfig *config)
|
||||
{
|
||||
*startY = ptIndex[0]->wSPosY;
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
for (uint16_t i = 1; i < (uint16_t)(ptIndex[0]->wSPosY / config->lineHeight + 1); i++)
|
||||
{
|
||||
if (*endY > config->menuYMax)
|
||||
{
|
||||
*startY = (uint16_t)(ptIndex[0]->wSPosY - config->lineHeight * i);
|
||||
if (*startY < config->lineHeight)
|
||||
{
|
||||
*startY = 0;
|
||||
}
|
||||
*endY = (uint16_t)(*startY + itemNum * config->lineHeight + config->menuYAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 计算单个菜单项“显示宽度”:
|
||||
* - 中文等多字节字符按 2 列计宽,ASCII 按 1 列;
|
||||
* - 若该项有下级菜单且名称末尾未带特殊标记 '\x10',额外预留 1 列用于层级指示。 */
|
||||
static uint8_t MenuLayout_ItemDisplayLen(const MenuItem *item)
|
||||
{
|
||||
uint8_t displayLen;
|
||||
displayLen = MenuLayout_Utf8LenCal((uint8_t *)item->byName);
|
||||
if (item->ptLower != NULL)
|
||||
{
|
||||
uint8_t byteLen = 0;
|
||||
while ((byteLen < 50) && (item->byName[byteLen] != '\0'))
|
||||
{
|
||||
byteLen++;
|
||||
}
|
||||
if ((byteLen == 0) || (item->byName[byteLen - 1] != '\x10'))
|
||||
{
|
||||
displayLen += 1;
|
||||
}
|
||||
}
|
||||
return displayLen;
|
||||
}
|
||||
|
||||
/* UTF-8 显示长度统计:
|
||||
* utf8_next() 每次返回一个 Unicode 字符占用的字节数 n,
|
||||
* 约定 n>1 视为“宽字符”(占 2 列),否则占 1 列。 */
|
||||
uint8_t MenuLayout_Utf8LenCal(uint8_t *str)
|
||||
{
|
||||
uint8_t strLen = 0;
|
||||
uint32_t unicode;
|
||||
uint8_t index = 0;
|
||||
uint8_t n = 0;
|
||||
|
||||
while (str[index] != '\0')
|
||||
{
|
||||
n = utf8_next(str + index, &unicode);
|
||||
strLen += (n > 1) ? 2 : 1;
|
||||
index += n;
|
||||
}
|
||||
return strLen;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_CharLenCal
|
||||
* 作用:
|
||||
* 1) 统计 bylevel 下一层(bylevel+1)菜单项总数;
|
||||
* 2) 计算该层中最长显示宽度(用于决定菜单框宽度);
|
||||
* 3) 为该层每个节点写 wPos(从 1 开始的位置序号);
|
||||
* 4) 把“下一层项数”写回父节点 wNum。
|
||||
*
|
||||
* 变量字典:
|
||||
* - bylevel: 输入为父层级,函数内部会自增到“子层级”;
|
||||
* - byMenuNum[level]: level 层累计项数(同时作为 wPos 的来源);
|
||||
* - ptFirst[level]: level 层循环链表的起点节点;
|
||||
* - ptIndex[level]: level 层当前遍历节点;
|
||||
* - byMaxLen: 本层所有项显示宽度的最大值(单位: 字符列)。
|
||||
*****************************************************************************/
|
||||
uint8_t MenuLayout_CharLenCal(uint8_t bylevel, uint8_t *byMenuNum, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex)
|
||||
{
|
||||
uint8_t displayLen;
|
||||
uint8_t byMaxLen = 0;
|
||||
MenuLinks links;
|
||||
|
||||
/* 进入下一层,从父节点的 lower 作为该层环形链表起点。 */
|
||||
ptFirst[bylevel + 1] = ptIndex[bylevel]->ptLower;
|
||||
bylevel = bylevel + 1;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
|
||||
byMenuNum[bylevel] = 1;
|
||||
ptIndex[bylevel]->wPos = 1;
|
||||
|
||||
/* 固定上限 300 作为保护,避免链表异常导致死循环。 */
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
displayLen = MenuLayout_ItemDisplayLen(ptIndex[bylevel]);
|
||||
|
||||
if (byMaxLen < displayLen)
|
||||
{
|
||||
byMaxLen = displayLen;
|
||||
}
|
||||
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
byMenuNum[bylevel]++;
|
||||
ptIndex[bylevel]->wPos = byMenuNum[bylevel];
|
||||
}
|
||||
|
||||
/* 父节点记录子项数量,供分页/显示逻辑复用。 */
|
||||
ptIndex[bylevel - 1]->wNum = byMenuNum[bylevel];
|
||||
return byMaxLen;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_Sub2PosCal
|
||||
* 作用: 计算二级子菜单(及其同层兄弟)的显示矩形
|
||||
* 策略:
|
||||
* - 先按“父项对齐”得到理想 Y;
|
||||
* - 若底部越界,尝试向上翻转;
|
||||
* - 仍不满足则使用贴近顶层菜单的兜底摆放。
|
||||
*
|
||||
* 变量字典:
|
||||
* - byMaxNum: 当前配置下可完整显示的最大行数;
|
||||
* - byMenuPos: 当前菜单相对父菜单的挂接行(理论位置);
|
||||
* - byItemNum: 当前层或下一层项数(代码中按阶段复用);
|
||||
* - wSPosY/wEPosY: 菜单框起止 Y 坐标(含间距边界);
|
||||
* - parentRect: 父菜单矩形,用于对子菜单做相对定位。
|
||||
*
|
||||
* 关键公式:
|
||||
* - X 起点 = parentRect.wEPosX(总是从父框右侧展开);
|
||||
* - X 终点 = X 起点 + byMaxLen * menuWidth + menuXAdd;
|
||||
* - Y 起点 = parentRect.wSPosY + (byMenuPos - 1) * lineHeight;
|
||||
* - Y 终点 = Y 起点 + 子项数 * lineHeight + menuYAdd。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_Sub2PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosY;
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byMaxNum;
|
||||
uint16_t byMenuPos;
|
||||
uint8_t byItemNum;
|
||||
uint8_t byMenuNum[4];
|
||||
MenuLinks links;
|
||||
MenuRect parentRect;
|
||||
|
||||
/* 当前可完整容纳的最大行数(预留边框/间距)。 */
|
||||
byMaxNum = (config->menuYMax - config->menuYMin - 6) / config->lineHeight;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
|
||||
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
|
||||
MenuLayout_CalcSubMenuX(&parentRect,
|
||||
byMaxLen,
|
||||
config,
|
||||
&ptIndex[bylevel]->wSPosX,
|
||||
&ptIndex[bylevel]->wEPosX);
|
||||
|
||||
/* byMenuPos: 当前项在同层中的理论挂接位置(必要时压缩映射)。 */
|
||||
byMenuPos = ptIndex[bylevel]->wPos;
|
||||
byItemNum = byMenuNum[bylevel];
|
||||
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
|
||||
|
||||
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
|
||||
byItemNum = byMenuNum[bylevel + 1];
|
||||
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
|
||||
|
||||
if (wEPosY < config->menuYMax)
|
||||
{
|
||||
/* 理想位置不越界,直接应用。 */
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* 底部越界:先尝试“向上翻转”放置整个菜单框。 */
|
||||
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->menuYMin, config))
|
||||
{
|
||||
/* 仍不满足:回退到顶层菜单起始 Y,再做截断兜底。 */
|
||||
MenuLayout_FallbackClampToTop(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
|
||||
}
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
}
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_Sub1PosCal
|
||||
* 作用: 计算一级子菜单(通常是主菜单下拉)的位置,并联动计算二级子菜单
|
||||
* 说明: 与 Sub2 逻辑相近,但在兜底时会逐步向上平移,直到菜单框可容纳。
|
||||
*
|
||||
* 变量字典:
|
||||
* - byMaxNum: 屏幕当前可显示的最大菜单行数;
|
||||
* - byMenuPos: 当前节点理论挂接行(可能被映射压缩);
|
||||
* - byItemNum: 当前下拉框条目数;
|
||||
* - wTemp: 用于“翻转布局”时交换起止 Y;
|
||||
* - byMenuNum[4]: 分层统计数组,索引与菜单层级一一对应。
|
||||
*
|
||||
* 与 Sub2 的核心差异:
|
||||
* - Sub1 兜底策略增加“逐行上探”,尝试找到不越界的最靠下位置,
|
||||
* 视觉上更适合主菜单下拉场景。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_Sub1PosCal(uint8_t bylevel, tagPMenuItem *ptFirst, tagPMenuItem *ptIndex, const MenuLayoutConfig *config)
|
||||
{
|
||||
uint16_t wSPosY;
|
||||
uint16_t wEPosY;
|
||||
uint8_t byMenuNum[4];
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byMaxNum;
|
||||
uint16_t byMenuPos;
|
||||
uint8_t byItemNum;
|
||||
MenuLinks links;
|
||||
MenuRect parentRect;
|
||||
|
||||
byMaxNum = config->menuYMax / config->lineHeight;
|
||||
ptIndex[bylevel] = ptFirst[bylevel];
|
||||
for (uint16_t wLoop = 0; wLoop < 300; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[bylevel]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
byMaxLen = MenuLayout_CharLenCal(bylevel, byMenuNum, ptFirst, ptIndex);
|
||||
parentRect = MenuLayout_GetRect(ptIndex[bylevel - 1]);
|
||||
MenuLayout_CalcSubMenuX(&parentRect,
|
||||
byMaxLen,
|
||||
config,
|
||||
&ptIndex[bylevel]->wSPosX,
|
||||
&ptIndex[bylevel]->wEPosX);
|
||||
|
||||
byMenuPos = ptIndex[bylevel]->wPos;
|
||||
byItemNum = byMenuNum[bylevel];
|
||||
byMenuPos = MenuLayout_MapMenuPos(byMenuPos, byItemNum, byMaxNum);
|
||||
|
||||
wSPosY = parentRect.wSPosY + (byMenuPos - 1) * config->lineHeight;
|
||||
byItemNum = byMenuNum[bylevel + 1];
|
||||
wEPosY = wSPosY + byItemNum * config->lineHeight + config->menuYAdd;
|
||||
|
||||
if (wEPosY < config->menuYMax)
|
||||
{
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* 底部越界:优先尝试一次整体翻转到上方。 */
|
||||
if (!MenuLayout_TryFlipUp(&wSPosY, &wEPosY, byItemNum, config->lineHeight, config))
|
||||
{
|
||||
/* 二次兜底:从顶层菜单起始 Y 往上逐行试探,直到不越界。 */
|
||||
MenuLayout_FallbackProbeUp(ptIndex, byItemNum, &wSPosY, &wEPosY, config);
|
||||
}
|
||||
MenuLayout_SetRect(ptIndex[bylevel], ptIndex[bylevel]->wSPosX, wSPosY, ptIndex[bylevel]->wEPosX, wEPosY);
|
||||
}
|
||||
/* 一级菜单定位完成后,继续计算二级菜单。 */
|
||||
MenuLayout_Sub2PosCal(2, ptFirst, ptIndex, config);
|
||||
}
|
||||
ptIndex[bylevel] = links.behind;
|
||||
if (ptIndex[bylevel] == ptFirst[bylevel])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* 函数: MenuLayout_PositionCal
|
||||
* 作用: 入口函数,计算从 0 级到 2 级菜单的显示坐标
|
||||
* 流程:
|
||||
* 1) 先给 0 级菜单按屏宽等分;
|
||||
* 2) 每个 0 级项若有下级,则计算其下拉框;
|
||||
* 3) 再级联计算 1/2 级子菜单。
|
||||
*
|
||||
* 变量字典:
|
||||
* - by0LevelNum: 顶层菜单数量;
|
||||
* - byInterval: 顶层菜单等分后的列宽;
|
||||
* - byMenuNum[0]: 当前顶层节点在同层中的序号(从 1 开始);
|
||||
* - byMenuNum[1]: 顶层节点的直接子项数量(由 CharLenCal 写入);
|
||||
* - ptFirst/ptIndex: 各层遍历上下文,在本函数中作为全流程共享状态。
|
||||
*****************************************************************************/
|
||||
void MenuLayout_PositionCal(tagPMenuItem ptMenuHead, uint8_t by0LevelNum, const MenuLayoutConfig *config)
|
||||
{
|
||||
tagPMenuItem ptFirst[4];
|
||||
tagPMenuItem ptIndex[4];
|
||||
uint8_t byMenuNum[4];
|
||||
uint8_t byMaxLen;
|
||||
uint8_t byInterval;
|
||||
MenuLinks links;
|
||||
|
||||
/* 输入保护:头指针/配置为空,或 0 级菜单数量为 0 时直接返回。 */
|
||||
if ((ptMenuHead == NULL) || (config == NULL) || (by0LevelNum == 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ptFirst[0] = ptMenuHead;
|
||||
ptIndex[0] = ptFirst[0];
|
||||
byMenuNum[0] = 1;
|
||||
ptIndex[0]->wPos = 1;
|
||||
/* 顶层菜单按等分宽度布局。 */
|
||||
byInterval = config->lcdSizeX / by0LevelNum;
|
||||
|
||||
for (uint16_t wLoop = 0; wLoop < by0LevelNum; wLoop++)
|
||||
{
|
||||
links = MenuLayout_GetLinks(ptIndex[0]);
|
||||
if (links.lower != NULL)
|
||||
{
|
||||
/* 先统计该分支子项宽高需求,再写入 0 级菜单框。 */
|
||||
byMaxLen = MenuLayout_CharLenCal(0, byMenuNum, ptFirst, ptIndex);
|
||||
MenuLayout_SetRect(
|
||||
ptIndex[0],
|
||||
(byMenuNum[0] - 1) * byInterval,
|
||||
config->lcdSizeY - config->lineHeight - byMenuNum[1] * config->lineHeight - config->menuYAdd,
|
||||
(byMenuNum[0] - 1) * byInterval + byMaxLen * config->menuWidth + config->menuXAdd,
|
||||
config->lcdSizeY - config->lineHeight);
|
||||
/* 0 级完成后继续展开 1/2 级。 */
|
||||
MenuLayout_Sub1PosCal(1, ptFirst, ptIndex, config);
|
||||
}
|
||||
ptIndex[0] = links.behind;
|
||||
if (ptIndex[0] == ptFirst[0])
|
||||
{
|
||||
break;
|
||||
}
|
||||
byMenuNum[0]++;
|
||||
ptIndex[0]->wPos = byMenuNum[0];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user