35. 布局管理
一、布局管理
我们可以使用 lv_obj_set_pos(obj, x, y)
函数调整一个控件的位置(或者使用类似的函数单独调整一个方向的坐标),将它放在相对父容器左上角的合适位置。如果两个控件间没有包含关系,可以使用 lv_obj_align_to(obj, base, align, x_ofs, y_ofs)
函数设置两个控件的相对对齐方式。不过这种布局方式非常死板,因为绝对坐标一旦设定就不能自动调整;而且当控件数量较多时,也很难确定合适的坐标值。
这种对齐的方式对于控件不多的情况下来说是足够了,但是有些时候需要对很多并列的控件布局(例如,一个计算机界面的所有按钮)。这个时候常规的对齐方式就难以满足需求了。因此,LVGL 提供了两种更复杂的布局方式:弹性盒子(flex)和 网格布局(grid)。如果不添加任何布局方式,那么所有的控件都会堆放在左上角。
如果,我们要启用 Flex 布局,需要在 lv_conf.h
文件中把 LV_USE_FLEX
配置项置 1 即可启用 Flex 布局。如果,我们要启动 Grid 布局,需要在 lv_conf.h
文件中把 LV_USE_GRID
配置项置 1 即可启用 Grid 布局。
二、弹性布局
Flexible Box 模型,通常被称为 flexbox,是一种一维的布局模型。它给 flexbox 的子元素之间提供了强大的空间分布和对齐能力。它可以将对象排列成行或列,并处理环绕,调整对象和行/列之间的间距,处理增长以使对象填充剩余空间的最小/最大宽度和高度。
当使用 flex 布局时,首先想到的是两根轴线,它们分别为 主轴 和 交叉轴。主轴是 flex 方向,另一根轴垂直于它。我们使用 flexbox 的所有属性都跟这两根轴线有关。
主轴:是定义对象的放置方向的,在 LVGL 中,它一般可取八个值,如下所示:
typedef enum {
LV_FLEX_FLOW_ROW = 0x00, // 将子类们排成一行并不换行
LV_FLEX_FLOW_COLUMN = _LV_FLEX_COLUMN, // 将子类们排成一列并不换行
LV_FLEX_FLOW_ROW_WRAP = LV_FLEX_FLOW_ROW | _LV_FLEX_WRAP, // 将子类们排成一行并换行
LV_FLEX_FLOW_ROW_REVERSE = LV_FLEX_FLOW_ROW | _LV_FLEX_REVERSE, // 将子类们排成一行并不换行且顺序相反
LV_FLEX_FLOW_ROW_WRAP_REVERSE = LV_FLEX_FLOW_ROW | _LV_FLEX_WRAP | _LV_FLEX_REVERSE, // 将子类们排成一行并换行且顺序相反
LV_FLEX_FLOW_COLUMN_WRAP = LV_FLEX_FLOW_COLUMN | _LV_FLEX_WRAP, // 将子类们排成一列并换行
LV_FLEX_FLOW_COLUMN_REVERSE = LV_FLEX_FLOW_COLUMN | _LV_FLEX_REVERSE, // 将子类们排成一列并不换行且顺序相反
LV_FLEX_FLOW_COLUMN_WRAP_REVERSE = LV_FLEX_FLOW_COLUMN | _LV_FLEX_WRAP | _LV_FLEX_REVERSE, // 将子类们排成一列并换行且顺序相反
} lv_flex_flow_t;
由此可知,如果我们设置 LV_FLEX_FLOW_ROW
~ LV_FLEX_FLOW_ROW_WRAP_REVERSE
为主轴配置项时,容器的主轴将沿着 inline 方向延伸,如下图所示:
从上图可知:容器就是绘画在显示屏上的部件,如果有些子类超出了容器范围,则系统自动使能滚动条。带有 WRAP 的配置项会把超出容器的子类以换行的形式添加在第二行中,带有 REVEESW 的配置项会把所有子类以翻转的顺序排列。
如果我们设置 LV_FLEX_FLOW_COLUMN
~ LV_FLEX_FLOW_COLUMN_WRAP_REVERSE
为主轴配置项时,容器的主轴会沿着上下方向延伸,也就是 block 排列的方向,如下图所示:
从上图可知:容器就是绘画在显示屏上的部件,如果有些子类超出了容器范围,则系统自动使能滚动条。带有 WRAP 的配置项会把超出容器的子类以换列的形式添加在第二列中,带有 REVEESW 的配置项会把所有子类以翻转的顺序排列。
交叉轴:交叉轴垂直于主轴,所以如果你的 flex-direction(主轴)设成了 ROW 的话,交叉轴的方向就是沿着列向下的,如下图所示:
如果你的 flex-direction(主轴)设成了 COLUMN 的话,交叉轴就是水平方向,如下图所示:
在 LVGL 中,使用 Flex 布局有两种方式,第一种是使用 lv_obj_set_layout()
函数方法设置对象启用 Flex 布局,第二种是使用 lv_obj_set_flex_flow()
函数启用 Flex 布局。
// 第一种方式
static lv_style_t style;
lv_style_init(&style);
lv_style_set_flex_flow(&style, LV_FLEX_FLOW_ROW_WRAP);
/* 设置 Flex 布局 */
lv_style_set_layout(&style, LV_LAYOUT_FLEX);
// 第二种方式
lv_obj_t* cont_row = lv_obj_create(lv_scr_act());
// 设置 Flex 布局
lv_obj_set_flex_flow(cont_row, LV_FLEX_FLOW_ROW_WRAP);
Flexbox 的一个关键特性是能够设置 flex 元素沿主轴方向和交叉轴方向的对齐方式以及它们之间的空间分配,这时,我们使用函数为 lv_obj_set_flex_align()
函数。
/**
* @brief 设置对齐方式
*
* @param obj 指向设置对齐的对象
* @param main_place 主轴上的对象对齐
* @param cross_place 交叉轴上的对象对齐
* @param track_place 行/列交叉轴上的对象对齐
*/
void lv_obj_set_flex_align(lv_obj_t * obj, lv_flex_align_t main_place, lv_flex_align_t cross_place, lv_flex_align_t track_place);
main_place、cross_place 和 track_place 形参的转入值如下表所示:
typedef enum {
LV_FLEX_ALIGN_START, // 水平方向左上和垂直上方向上(默认)
LV_FLEX_ALIGN_END, // 水平方向右侧和垂直底部
LV_FLEX_ALIGN_CENTER, // 居中
LV_FLEX_ALIGN_SPACE_EVENLY, // 任何两个对象之间的间距(它们的边缘空间相等),但不适用于track__place形参
LV_FLEX_ALIGN_SPACE_AROUND, // 对象在轨道上均匀分布,周围空间相等,但不适用于track_place形参
LV_FLEX_ALIGN_SPACE_BETWEEN, // 对象在轨道上均匀分布:第一个对象在开始行,最后一个项目在结束行,但不适用于track_cross_place形参
} lv_flex_align_t;
track_place 形参只能传入 LV_FLEX_ALIGN_START
、LV_FLEX_ALIGN_END
和 LV_FLEX_ALIGN_CENTER
配置项,它们分别代表显示的位置。
如果 track_place 形参设置为 LV_FLEX_ALIGN_START
,则该容器显示子类 1 和 2;如果 track_place 形参设置为 LV_FLEX_ALIGN_END
,则该容器显示子类 5 和 6;如果 track_place 形参设置为 LV_FLEX_ALIGN_CENTER
,则该容器显示子类 3 和 4。
cross_place 形参一般在子类对象具有不同的高度时会起作用。
flex-grow 若被赋值为一个正整数,它沿主轴方向增长尺寸。这会使该元素延展,并占据此方向轴上的可用空间(available space)。如果有其他元素也被允许延展,那么它们会各自占据可用空间的一部分。关于这个 grow 属性,LVGL 提供了 lv_obj_set_flex_grow()
函数来设置,该函数具有两个形参,第一个形参指向要增长的对象,而第二个形参表示增长的倍数。
/**
* @brief 设置增长
*
* @param obj 指向要增长的对象
* @param grow 增长的倍数,如果为0时禁用grow属性
*/
void lv_obj_set_flex_grow(lv_obj_t * obj, uint8_t grow);
Flex-gap 属性用来设置元素列之间的间隔(gutter)大小。如果读者不希望对象之间有任何填充,可调用 lv_style_set_pad_column()
函数设置。
void lv_style_set_pad_row(lv_style_t * style, int32_t value);
void lv_style_set_pad_column(lv_style_t * style, int32_t value);
三、网格布局
Grid 网格是一组相交的水平线和垂直线,它定义了网格的列和行。我们可以将网格元素放置在与这些行和列相关的位置上。简单来说:Grid 网格可以将对象排列到具有行或列(轨道)的二维“表”中,该对象可以跨越多个列或行。轨道的大小可以设置为 像素
、LV_GRID_CONTENT
或 空闲单元
(FR)以按比例分配空闲空间。
Grid 描述符是用来描述行/列的数量以及单元格的像素,它一般使用两个数组来描述网格的行数和列数。这些数组的最后一个元素必须是 LV_GRID_TEMPLATE_LAST
配置项。
我们可以调用 lv_obj_set_style_grid_column_dsc_array()
函数和 lv_obj_set_style_grid_row_dsc_array()
函数创建网格的。
void lv_obj_set_style_grid_column_dsc_array(lv_obj_t * obj, const int32_t * value, lv_style_selector_t selector);
void lv_obj_set_style_grid_row_dsc_array(lv_obj_t * obj, const int32_t * value, lv_style_selector_t selector);
当然除了以像素为单位的简单设置之外,我们还可以使用两个特殊值:
#define LV_GRID_CONTENT (LV_COORD_MAX - 101) // 将宽度设置为此轨道上最大的子类
#define LV_GRID_FR(x) (LV_COORD_MAX - 100 + x) // 告诉该轨道应使用剩余空间的哪一部分,数值越大,空间越大
默认情况下,我们的子项不会自动添加到网格中。这些子类需要读者手动添加到单元格中,这个添加方式需要读者调用 lv_obj_set_grid_cell()
把子类对象添加到指定网格位置。
/**
* @brief 设置对象的单元格
*
* @param obj 指向对象的指针
* @param x_align 单元格中的垂直对齐方
* @param col_pos 列号
* @param col_span 要取的列数
* @param y_align 单元格中的水平对齐方式
* @param row_pos 行号
* @param row_span 获取的行数
*/
void lv_obj_set_grid_cell(lv_obj_t * obj, lv_grid_align_t x_align, int32_t col_pos, int32_t col_span,
lv_grid_align_t y_align, int32_t row_pos, int32_t row_span);
Grid 对齐使用的是 lv_obj_set_grid_align()
函数
void lv_obj_set_grid_align(lv_obj_t * obj, lv_grid_align_t column_align, lv_grid_align_t row_align);
四、实验例程
4.1、弹性布局
#include "lvgl.h"
#include "lv_port_disp_template.h"
#include "lv_port_indev_template.h"
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
SPI_Simulate_Init();
// SRAM_Init();
TIM_Base_Init(&g_tim6_handle, TIM6, 83, 999);
__HAL_TIM_CLEAR_IT(&g_tim6_handle, TIM_IT_UPDATE); // 清除更新中断标志位
HAL_TIM_Base_Start_IT(&g_tim6_handle); // 使能更新中断,并启动计数器
lv_init();
lv_port_disp_init();
lv_port_indev_init();
// 测试代码
static lv_style_t style;
lv_style_init(&style);
lv_style_set_flex_flow(&style, LV_FLEX_FLOW_ROW);
lv_style_set_layout(&style, LV_LAYOUT_FLEX);
lv_obj_t* cont_row = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont_row, 300, 70);
lv_obj_add_style(cont_row, &style, 0);
lv_obj_t * cont_col = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont_col, 200, 150);
lv_obj_align_to(cont_col, cont_row, LV_ALIGN_OUT_BOTTOM_MID, 0, 5);
lv_obj_set_flex_flow(cont_col, LV_FLEX_FLOW_COLUMN);
for(uint32_t i = 0; i < 10; i++)
{
lv_obj_t * obj;
lv_obj_t * label;
obj = lv_button_create(cont_row);
lv_obj_set_size(obj, 100, LV_PCT(100));
label = lv_label_create(obj);
lv_label_set_text_fmt(label, "Item: %"LV_PRIu32"", i);
lv_obj_center(label);
obj = lv_button_create(cont_col);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
label = lv_label_create(obj);
lv_label_set_text_fmt(label, "Item: %"LV_PRIu32, i);
lv_obj_center(label);
}
while (1)
{
lv_timer_handler();
Delay_ms(5);
}
return 0;
}
4.2、网格布局
#include "lvgl.h"
#include "lv_port_disp_template.h"
#include "lv_port_indev_template.h"
int main(void)
{
HAL_Init();
System_Clock_Init(8, 336, 2, 7);
Delay_Init(168);
SPI_Simulate_Init();
// SRAM_Init();
TIM_Base_Init(&g_tim6_handle, TIM6, 83, 999);
__HAL_TIM_CLEAR_IT(&g_tim6_handle, TIM_IT_UPDATE); // 清除更新中断标志位
HAL_TIM_Base_Start_IT(&g_tim6_handle); // 使能更新中断,并启动计数器
lv_init();
lv_port_disp_init();
lv_port_indev_init();
// 测试代码
static int32_t col_dsc[] = {70, 70, 70, LV_GRID_TEMPLATE_LAST};
static int32_t row_dsc[] = {50, 50, 50, LV_GRID_TEMPLATE_LAST};
lv_obj_t * cont = lv_obj_create(lv_screen_active());
lv_obj_set_style_grid_column_dsc_array(cont, col_dsc, 0);
lv_obj_set_style_grid_row_dsc_array(cont, row_dsc, 0);
lv_obj_set_size(cont, 300, 220);
lv_obj_center(cont);
lv_obj_set_layout(cont, LV_LAYOUT_GRID);
lv_obj_t * label;
lv_obj_t * obj;
uint32_t i;
for(i = 0; i < 9; i++)
{
uint8_t col = i % 3;
uint8_t row = i / 3;
obj = lv_button_create(cont);
lv_obj_set_grid_cell(obj, LV_GRID_ALIGN_STRETCH, col, 1, LV_GRID_ALIGN_STRETCH, row, 1);
label = lv_label_create(obj);
lv_label_set_text_fmt(label, "c%d, r%d", col, row);
lv_obj_center(label);
}
while (1)
{
lv_timer_handler();
Delay_ms(5);
}
return 0;
}