第7章:Flex/Grid 布局引擎

排兵布阵的两副棋盘:一维的流,二维的表格

🪶

本章导师:诸葛亮

核心方法论:运筹帷幄,把布局当排兵布阵来设计

「行军布阵,讲究的是'规则先行':将领只定阵列的规矩,士卒自己找位置。Flex 与 Grid 正是这样的将令——你声明方向、对齐、行列,无数个子控件自动各就其位。阵型一变,全军重排,这就是运筹帷幄之道。」

7.1 布局引擎总览:从手动定位到自动排布

第 3、5 章里你手动用 lv_obj_align() / lv_obj_set_pos() 摆放控件。这套方式在控件少时很直观,但对象一多、或屏幕尺寸一变,"手摆"就难以为继——改一处坐标,整片界面都要跟着挪。布局引擎(layout)解决的正是"子对象如何自动排布"这个共性问题:你在父对象上声明规则,子对象的位置、尺寸由引擎统一计算,尺寸变化时自动重排,无需你干预。

启用方式是一条 API:lv_obj_set_layout(obj, LV_LAYOUT_FLEX)LV_LAYOUT_GRIDLV_LAYOUT_NONE 关闭)。布局枚举 lv_layout_t 定义在 src/layouts/lv_layout.h,两种布局的实现分别在 src/layouts/flex/lv_flex.c)与 src/layouts/grid/lv_grid.c),由配置宏 LV_USE_FLEX / LV_USE_GRID 决定是否编译进库。还要记住一条铁律:布局只作用于当前对象的直接子对象,子对象仍可再开自己的布局——这就是嵌套组合的根基。

/* 三种布局模式(lv_layout_t,src/layouts/lv_layout.h) */
lv_obj_set_layout(row, LV_LAYOUT_FLEX);     /* 一维:沿主轴排布 */
lv_obj_set_layout(box, LV_LAYOUT_GRID);     /* 二维:行列网格 */
lv_obj_set_layout(row, LV_LAYOUT_NONE);     /* 关闭布局,回到手动定位 */
/* 布局本身也是一个样式属性:LV_STYLE_LAYOUT */
lv_obj_set_style_layout(container, LV_LAYOUT_FLEX, 0);

/* 一个 Flex 容器 + 一个 Grid 容器,各自排布自己的子对象 */
lv_obj_t * flex_box = lv_obj_create(lv_screen_active());
lv_obj_set_layout(flex_box, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(flex_box, LV_FLEX_FLOW_ROW);

lv_obj_t * grid_box = lv_obj_create(lv_screen_active());
lv_obj_set_layout(grid_box, LV_LAYOUT_GRID);
布局模式lv_layout_t 值适用场景
手动定位LV_LAYOUT_NONE少量固定控件、像素级控制
FlexLV_LAYOUT_FLEX一行/一列、数量不定的同类项
GridLV_LAYOUT_GRID表格、键盘、行列规整的仪表盘
诸葛亮提示

父对象一旦开了布局,其直接子对象的手动坐标就会被布局结果覆盖。想对个别子对象做例外,用 lv_obj_set_style_flex_growlv_obj_set_grid_cell 这类"布局内的微调",而不要硬塞 lv_obj_set_pos——那会被引擎重新摆掉。

7.2 Flex 流动方向:主轴、换行与顺序

Flex 的模型一句话就能说清:子对象沿一条轴(主轴)依次排开,放不下就换行(track)。主轴方向由 flow 决定:lv_obj_set_flex_flow(obj, LV_FLEX_FLOW_ROW) 横排、LV_FLEX_FLOW_COLUMN 竖排。在此基础上还有两个"开关位"——WRAP 位允许超出后换行,REVERSE 位反转排列顺序;三者按位或,组合出 ROW_WRAPCOLUMN_WRAP_REVERSE 等共 8 种取值。枚举 lv_flex_flow_t 定义在 include/lvgl/layouts/lv_flex.h

lvgl-master/examples/layouts/flex/lv_example_flex_flow.c 是理解换行最直观的样本:一个 LV_FLEX_FLOW_ROW_WRAP 容器里塞了 10 个按钮,超出宽度后自动折到第二行。把它改成 ROWCOLUMN_WRAP_REVERSE 等其它值,同一批子对象会呈现完全不同的排布——这正是布局的威力:规则与内容解耦,改一行 flow,整组控件重新列阵。

/* 横排 + 自动换行:10 个按钮排成多行(参考 lv_example_flex_flow.c) */
lv_obj_t * container = lv_obj_create(lv_screen_active());
lv_obj_set_layout(container, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_size(container, lv_pct(90), lv_pct(50));

for(int i = 0; i < 10; i++) {
    lv_obj_t * btn = lv_button_create(container);   /* 子对象自动依次排布 */
    lv_obj_t * lbl = lv_label_create(btn);
    lv_label_set_text_fmt(lbl, "%d", i);
}
/* 8 种 flow:方向 × 换行 × 顺序反转 */
lv_obj_set_flex_flow(row,  LV_FLEX_FLOW_ROW);               /* 横排,不换行 */
lv_obj_set_flex_flow(row2, LV_FLEX_FLOW_ROW_WRAP);          /* 横排,超出换行 */
lv_obj_set_flex_flow(row3, LV_FLEX_FLOW_ROW_WRAP_REVERSE);  /* 横排换行 + 反向 */
lv_obj_set_flex_flow(col,  LV_FLEX_FLOW_COLUMN);            /* 竖排 */
lv_obj_set_flex_flow(col2, LV_FLEX_FLOW_COLUMN_REVERSE);    /* 竖排,反向 */
常量含义
LV_FLEX_FLOW_ROW横排,不换行
LV_FLEX_FLOW_ROW_WRAP横排,超出换行
LV_FLEX_FLOW_ROW_REVERSE横排,顺序反转
LV_FLEX_FLOW_ROW_WRAP_REVERSE横排换行 + 反转
LV_FLEX_FLOW_COLUMN竖排,不换行
LV_FLEX_FLOW_COLUMN_WRAP竖排,超出换列
LV_FLEX_FLOW_COLUMN_REVERSE竖排,顺序反转
LV_FLEX_FLOW_COLUMN_WRAP_REVERSE竖排换列 + 反转
诸葛亮提示

主轴方向一旦确定,"对齐"和"空隙"都沿它展开:LV_FLEX_FLOW_ROW 时主轴是水平方向,COLUMN 时主轴变成垂直方向。先定 flow,再谈对齐——顺序不要反,否则你以为是"左右分布",实际是"上下分布"。

7.3 Flex 对齐与 grow:把自由空间分出去

方向定了,接下来是"怎么分布"。lv_obj_set_flex_align(obj, main_place, cross_place, track_cross_place) 一次管三个维度:主轴上的分布方式(LV_FLEX_ALIGN_START/END/CENTER/SPACE_EVENLY/SPACE_AROUND/SPACE_BETWEEN)、交叉轴上的对齐(START/END/CENTER)、以及多行轨道整体在交叉轴上的位置。取值枚举 lv_flex_align_t 同样来自 lv_flex.h

三个"空隙"常量是分布美学的主力:SPACE_BETWEEN 首尾贴边、中间等距;SPACE_AROUND 每项两侧各留一半空隙;SPACE_EVENLY 相邻项之间空隙完全相等。而 lv_example_flex_align.c 用一个高按钮 + 开关 + 复选框演示了交叉轴对齐——三个控件高度不同,LV_FLEX_ALIGN_CENTER 让它们在垂直方向排齐。若要让某个子对象"吃下剩余空间",用 lv_obj_set_flex_grow(obj, n) 按权重瓜分(lv_example_flex_grow.c:固定宽 + grow=1 + grow=2,剩余宽度按 1:2 分配)。

/* 三个对齐维度:主轴分布 / 交叉轴对齐 / 轨道位置(参考 lv_example_flex_align.c) */
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(container,
                      LV_FLEX_ALIGN_SPACE_BETWEEN,   /* 主轴:首尾贴边、中间等距 */
                      LV_FLEX_ALIGN_CENTER,          /* 交叉轴:垂直居中 */
                      LV_FLEX_ALIGN_CENTER);         /* 多行轨道:整体居中 */

/* 项间距也可以单独控制:列间距 12px */
lv_obj_set_style_pad_column(container, 12, 0);
/* flex_grow:按权重瓜分剩余空间(参考 lv_example_flex_grow.c) */
lv_obj_t * fixed = lv_button_create(container);
lv_obj_set_size(fixed, 50, LV_SIZE_CONTENT);   /* 固定宽度,不参与瓜分 */

lv_obj_t * g1 = lv_button_create(container);
lv_obj_set_flex_grow(g1, 1);   /* 拿走 1 份剩余空间 */

lv_obj_t * g2 = lv_button_create(container);
lv_obj_set_flex_grow(g2, 2);   /* 拿走 2 份:宽度是 g1 的两倍 */
对齐常量含义
LV_FLEX_ALIGN_START对齐主轴起点
LV_FLEX_ALIGN_END对齐主轴终点
LV_FLEX_ALIGN_CENTER主轴居中
LV_FLEX_ALIGN_SPACE_EVENLY项与项之间空隙完全相等
LV_FLEX_ALIGN_SPACE_AROUND每项两侧各留一半空隙
LV_FLEX_ALIGN_SPACE_BETWEEN首尾贴边、中间等距
诸葛亮提示

换行之后,"主轴上的分布"仍只在每一行(track)内部生效。想让多行作为一个整体分布,得靠第三个参数 track_cross_place。三个维度各管一段,哪个方向出问题就查哪个参数——这是排查 Flex 布局最有效的思路。

7.4 Grid 轨道描述:声明行与列

Grid 与 Flex 的差别在"维度":Flex 只管一条主轴,Grid 同时声明行、列两条轨道。声明轨道用 lv_obj_set_grid_dsc_array(obj, col_dsc, row_dsc)(或等价的样式 API lv_obj_set_style_grid_column_dsc_array / grid_row_dsc_array),两个数组里每个元素代表一条轨道的尺寸,且必须以 LV_GRID_TEMPLATE_LAST 结尾——它在 lv_grid.h 中定义为 LV_COORD_MAX,充当"数组结束"的哨兵。

轨道尺寸支持四种写法:固定像素(如 70 表示 70px)、LV_GRID_FR(x) 按权重瓜分剩余空间、LV_GRID_CONTENT 按内容自适应、以及 LV_SIZE_CONTENT。固定像素的写法见 lv_example_grid_descriptors.c——{70, 70, 70} 三列 × {44, 44} 两行,拼出规整的 3×2 表格;比例写法见 lv_example_grid_fr.c——{80, LV_GRID_FR(1), LV_GRID_FR(2)}:第一列固定 80px,剩下的宽度按 1:2 分给后两列。

/* 固定像素轨道:3 列 × 2 行(参考 lv_example_grid_descriptors.c) */
lv_obj_t * grid = lv_obj_create(lv_screen_active());
lv_obj_set_layout(grid, LV_LAYOUT_GRID);

static const int32_t col_dsc[] = {70, 70, 70, LV_GRID_TEMPLATE_LAST};
static const int32_t row_dsc[] = {44, 44, LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(grid, col_dsc, row_dsc);
/* 混合轨道:固定 + 按比例(参考 lv_example_grid_fr.c) */
static const int32_t col_dsc[] = {80, LV_GRID_FR(1), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST};
static const int32_t row_dsc[] = {44, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};
lv_obj_set_grid_dsc_array(grid, col_dsc, row_dsc);
轨道尺寸写法含义示例
数字固定像素70 → 70px 宽
LV_GRID_FR(x)按权重瓜分剩余空间LV_GRID_FR(2) → 占 2 份
LV_GRID_CONTENT按内容自适应适合列表列
LV_GRID_TEMPLATE_LAST轨道数组结束标记必须放数组末尾
诸葛亮提示

忘了放 LV_GRID_TEMPLATE_LAST,轨道数组会被读到数组边界之外,行为未定义。把"轨道尺寸 + 结束标记"写进同一个 static 数组,是既省内存又不会出错的做法——哨兵必须跟着数据走。

7.5 Grid 单元格:比例、跨列与嵌套组合

轨道声明好之后,每个子对象要"落座"——用 lv_obj_set_grid_cell(obj, column_align, col_pos, col_span, row_align, row_pos, row_span) 指定它落在哪一列哪一行、横跨几列几行、以及单元格内的对齐方式(LV_GRID_ALIGN_START/CENTER/END/STRETCH)。等价的样式 API 是 lv_obj_set_style_grid_cell_column_pos / row_pos / column_span / row_span 一组,两者选其一即可。

跨列跨行让"一张网格"能承载不规则的布局:lv_example_grid_cell_span.c 里,一个子对象用 column_span = 2 横跨两列,另一个用 row_span = 2 竖跨两行。而"布局的组合"更是诸葛亮的用武之地——lv_example_grid_fr.ccontainer_2container_3container_4 就被分别落座到主网格的不同单元格;这些单元格内还可以再创建开启新布局的容器(外层 Grid 定骨架、内层 Flex 排同类项)。排布规则由此一层层叠加,从一块小面板到整个仪表盘都能运筹帷幄。

/* 子对象落座:第 0 行第 0 列,铺满整个单元格 */
lv_obj_t * cell = lv_label_create(grid);
lv_label_set_text(cell, "0,0");
lv_obj_set_grid_cell(cell,
                     LV_GRID_ALIGN_STRETCH, 0, 1,   /* 列对齐、第 0 列、占 1 列 */
                     LV_GRID_ALIGN_STRETCH, 0, 1);  /* 行对齐、第 0 行、占 1 行 */
/* 跨列 / 跨行 / 单元格内对齐(参考 lv_example_grid_cell_span.c) */
lv_obj_set_grid_cell(banner,
                     LV_GRID_ALIGN_STRETCH, 1, 2,   /* 从第 1 列开始,横跨 2 列 */
                     LV_GRID_ALIGN_CENTER, 1, 1);   /* 第 1 行,垂直居中 */

/* 组合布局:grid 单元格里再放一个 flex 容器,占满第 2 行整行 */
lv_obj_t * inner = lv_obj_create(grid);
lv_obj_set_grid_cell(inner,
                     LV_GRID_ALIGN_STRETCH, 0, 3,   /* 横跨全部 3 列 */
                     LV_GRID_ALIGN_STRETCH, 2, 1);  /* 第 2 行 */
lv_obj_set_layout(inner, LV_LAYOUT_FLEX);          /* 内层切回 flex */
lv_obj_set_flex_flow(inner, LV_FLEX_FLOW_ROW);
Grid API作用
lv_obj_set_grid_dsc_array()声明行/列轨道数组
lv_obj_set_grid_align()整表行列的整体对齐
lv_obj_set_grid_cell()指定子对象的单元格、对齐与跨度
lv_grid_fr()绑定友好的 LV_GRID_FR 包装函数
诸葛亮提示

选 Flex 还是 Grid,一句话判断:内容是一维的"流"(菜单、按钮行)就用 Flex;内容是二维的"表"(键盘、仪表、卡片墙)就用 Grid。两者还能无限嵌套——外层 Grid 定骨架、内层 Flex 排同类项,这就是布局的运筹帷幄之道。

章末练习

练习 1:用 Flex 排一行按钮 入门

创建一个 lv_obj 容器并开启 LV_LAYOUT_FLEX,把 5 个按钮横排在一行,居中排列。写出完整代码。

提示

回忆 7.2/7.3:lv_obj_set_layout(container, LV_LAYOUT_FLEX)lv_obj_set_flex_flow(container, LV_FLEX_FLOW_ROW),再创建按钮即可;要居中再加 lv_obj_set_flex_align(..., LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER)

参考答案

lv_obj_t * row = lv_obj_create(lv_screen_active()); lv_obj_set_layout(row, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); for(int i = 0; i < 5; i++) lv_button_create(row);。按钮作为直接子对象会自动排布,无需手动定位。

练习 2:做一条"标题 + 操作"的导航栏 进阶

用 Flex 做一条水平导航栏:左侧一个标题标签,右侧两个按钮("设置"、"退出")。要求:标题贴左、两个按钮贴右、三者垂直居中。需要用到哪个对齐常量组合?

提示

要让左侧标题与右侧按钮组分居两端,用主轴 LV_FLEX_ALIGN_SPACE_BETWEEN;右侧两个按钮之间想留空隙,可给它们单独套一个 flex 子容器,或调 lv_obj_set_style_pad_column

参考答案

导航栏容器:lv_obj_set_flex_flow(nav, LV_FLEX_FLOW_ROW)lv_obj_set_flex_align(nav, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER)。标题放最左,右侧放一个开启 flex 的小容器包住两个按钮(内部可再加 pad_column 控制按钮间距)。"首尾贴边、中间等距"正是 SPACE_BETWEEN 的典型用途。

练习 3:用 Grid 拼一个计算器键盘 进阶

LV_LAYOUT_GRID 做一个 4 列的计算器键盘:第一行是"0-9、+、-、×、÷"中的 4 个按键,再排 4 行 4 列共 16 个按键,第 4 列统一为操作符。请写出轨道数组,并说明如何让"0"键横跨 3 列。

提示

4 列轨道可写成 {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}(等宽四列);"0"跨 3 列用 lv_obj_set_grid_cell(..., LV_GRID_ALIGN_STRETCH, 0, 3, ...)

参考答案

轨道:static const int32_t col[] = {LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST};,行轨道同理(LV_GRID_FR(1) 保证按键随容器伸缩)。每个按键用 lv_obj_set_grid_cell 落座;数字"0"横跨 3 列:lv_obj_set_grid_cell(key0, LV_GRID_ALIGN_STRETCH, 0, 3, LV_GRID_ALIGN_STRETCH, last_row, 1)。这样键盘既定宽又能自适应屏幕。

练习 4:响应式仪表盘——布局的组合 挑战

设计一个"设置仪表盘":外层 Grid 3 列,第一列放一列竖排滑块(内层 Flex + COLUMN),后两列放一个横跨 2 列的图表区。要求:所有轨道用 LV_GRID_FR,让整个仪表盘随屏幕尺寸伸缩;并说明为什么外层用 Grid、内层用 Flex。

提示

外层:{LV_GRID_FR(1), LV_GRID_FR(2)} 两列或三列;滑块列容器开启 LV_LAYOUT_FLEX + LV_FLEX_FLOW_COLUMN 并用 lv_obj_set_flex_grow 让每个滑块均分高度;图表区用 column_span 跨列。

参考答案

外层容器 LV_LAYOUT_GRID,轨道 {LV_GRID_FR(1), LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST}(或三列后让图表 column_span=2);滑块列是 grid 的一个单元格,内部 lv_obj_set_layout(slider_col, LV_LAYOUT_FLEX) + LV_FLEX_FLOW_COLUMN,每根滑块 lv_obj_set_flex_grow(slider, 1) 均分高度;图表区 lv_obj_set_grid_cell(chart, LV_GRID_ALIGN_STRETCH, 1, 2, LV_GRID_ALIGN_STRETCH, 0, 1)。选型依据:外层是"左右两块区域"的二维骨架 → Grid;内层是"数量不定、要均分"的一维列表 → Flex。FR 保证任何分辨率下区域等比例伸缩。