LVGL的布局
上一节介绍了如何在 LVGL 中创建控件。如果在创建控件时不给控件安排布局,那么控件默认会被放在父容器的左上角。
可以使用 lv_obj_set_pos(obj, x, y)
调整一个控件的位置(或者使用类似的函数单独调整一个方向的坐标),将它放在相对父容器左上角的合适位置。不过这种布局方式非常死板,因为绝对坐标一旦设定就不能自动调整;而且当控件数量较多时,也很难确定合适的坐标值。
上一节介绍过,可以使用 lv_obj_align(obj, align, x_ofs, y_ofs)
设置一个控件相对父容器的对齐,并用以下图片展示所有的对齐方式:
从图片中可以看到,控件之间不仅可以内对齐,也可以外对齐。如果两个控件间没有包含关系也不要紧,可以使用 lv_obj_align_to(obj, base, align, x_ofs, y_ofs);
设置两个控件的相对对齐方式。
这种对齐的方式对于控件不多的情况下来说是足够了,但是有些时候需要对很多并列的控件布局(例如,一个计算机界面的所有按钮)。这个时候常规的对齐方式就难以满足需求了。
因此,LVGL 提供了两种更复杂的布局方式:
- flex(弹性盒子)
- grid(网格)
这两种布局和 CSS3 新增的 flex 布局和 grid 布局比较相似,如果熟悉 CSS 的话对它们应该不会陌生。
flex布局
flex 是一个实验性质的布局,首先需要确定已经在 lv_conf.h
大约 588 行的位置启用了 flex 布局:
/*A layout similar to Flexbox in CSS.*/
#define LV_USE_FLEX 1
后续介绍的 grid 布局也是如此。
创建flex布局
如果不添加任何布局方式,那么所有的控件都会堆放在左上角。flex 布局可以将一些控件按行或列均匀布局,并且可以自动调整它们的间距。
可以给一个容器设置一个 flex-flow 属性,这样容器就可以使用 flex 布局方式:
lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW);
对于设置了 flex 布局的容器,在其中创建的元素都会在一个坐标轴上均匀排布。例如,以下使用 for
循环创建多个控件:
lv_obj_set_size(cont, 300, 75);
for (uint8_t i = 0; i < 9; i++) {
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "%d", i + 1);
}
效果为:
尽管没有设置按钮的位置,但是每一个按钮都会在水平位置上均匀排布。如果要让排布时不超过父容器的最大宽度,可以使用 LV_FLEX_FLOW_ROW_WRAP
折行。
也可以使用按列的方式排布控件。可以通过 lv_flex_flow_t
枚举类型检查更多的 flex 布局形式。
flex布局的对齐
以上 flex 布局中,各控件的尺寸和间距都是固定的,并且第一个控件依然会出现在左上角。如果
可以使用
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);
设置 flex 布局的对齐方式。该函数一次性会设置三个方面的对齐:
main_place
:设置行或列的对齐cross_place
:设置控件在一行或一列内的对齐(当控件高度或宽度不一致时就可以看出效果)track_place
:flex-flow 方向上的对齐
如果接触过 CSS 的话,可以明白这些对齐方式实际上就是 CSS 里的 justify-content
、align-items
和 align-content
。
例如,以下调用
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
创建的每个控件之间在水平方向上均匀对齐、行内上下居中对齐,并作为一个整体上下居中对齐,效果为:
又如,以下调用:
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
创建的每个控件之间在水平方向上两端对齐、行内顶端对齐,并作为一个整体顶端对齐,效果为:
flex 布局还可以通过
void lv_obj_set_flex_grow(lv_obj_t *obj, uint8_t grow);
动态调整各个控件的相对宽度,实现更灵活的布局规则。例如,以下代码在一个 flex-flow 框架内创建了 4 个按钮,并将第二个按钮的相对宽度设置为其它按钮的两倍:
for (uint8_t i = 0; i < 4; i++) {
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "%d", i);
if (i == 1)
lv_obj_set_flex_grow(btn, 2);
else
lv_obj_set_flex_grow(btn, 1);
}
效果为:
以下利用相对宽度创建了一个更复杂的类似数字输入键盘的布局规则:
lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_size(cont, 160, 180);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_base_dir(cont, LV_BASE_DIR_RTL, 0);
for (int8_t i = 9; i >= 0; i--) {
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "%d", i);
}
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_set_flex_grow(btn, 2);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text(label, "OK");
效果为:
这里使用 lv_obj_set_style_base_dir()
函数设置从右向左的书写方式,因此滚动条才会出现在左侧。后续介绍样式时还会介绍更多类似函数。
一般情况下 flex-grow 和带 wrap 的 flex-flow 是冲突的,也就是说所有设置了 flex-grow 的控件都会在同一行布局,但它们的宽度可能变得很窄。因此,以上的各个数字按钮相对宽度并不一致。
使用这种布局创建键盘非常别扭,不过好在 LVGL 提供了另一种形式的布局:grid 。
grid布局
创建grid布局
grid 布局是一种网格形式的布局,可以按行或列来对齐控件。
为了创建网格布局,首先要给出格子的长度和宽度。一般来说,可以通过两个数组分别描述网格每一行的宽度和每一列的宽度:
static lv_coord_t col_size[] = { 60, 60, 90, LV_GRID_TEMPLATE_LAST };
static lv_coord_t row_size[] = { 40, 40, 30, LV_GRID_TEMPLATE_LAST };
每一个数组都需要以 LV_GRID_TEMPLATE_LAST
结尾。然后就可以通过
void lv_obj_set_grid_dsc_array(lv_obj_t *obj, const lv_coord_t col_dsc[], const lv_coord_t row_dsc[])
函数为一个容器设置网格划分。
注意,创建的数组一定要声明为
static
或全局变量,因为这部分数据在后续渲染时才会被用上。
划分好了网格以后,接下来就可以使用以下函数:
void lv_obj_set_grid_cell(lv_obj_t * obj,
lv_grid_align_t x_align, uint8_t col_pos, uint8_t col_span,
lv_grid_align_t y_align, uint8_t row_pos, uint8_t row_span);
将每一个控件摆放在合适的网格位置。align
指定每一个放置在网格上的控件相对格线的对齐;pos
指定控件放置在哪个格子里,最左上角的格子位置为 (0, 0) ;有的控件可能占据不止一个格子的位置,那么就需要使用 span
来跨越多格。
例如,以下代码:
for (uint8_t i = 0; i < 9; i++) {
uint8_t col = i % 3;
uint8_t row = i / 3;
lv_obj_t* btn = lv_btn_create(cont);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, col, 1,
LV_GRID_ALIGN_STRETCH, row, 1);
lv_obj_t* label = lv_label_create(btn);
lv_label_set_text_fmt(label, "r%d c%d", row, col);
lv_obj_center(label);
}
得到的网格为:
这里使用 LV_GRID_ALIGN_STRETCH
让网格内的控件尺寸伸展至网格大小,使网格布局的特点更加明显。
grid布局的对齐
使用网格布局时,每个格子内的控件在创建时都可以在网格内对齐。除此之外,还可以设置网格自身的对齐方式:
void lv_obj_set_grid_align(lv_obj_t * obj, lv_grid_align_t column_align, lv_grid_align_t row_align);
网格在横向和竖向对齐摆放时,对齐方式都类似于 flex ,因此可以认为 grid 是一种二维的 flex 布局。
例如,如果略微修改以上代码,添加如下语句:
lv_obj_set_grid_align(cont, LV_GRID_ALIGN_SPACE_BETWEEN, LV_GRID_ALIGN_END);
for (uint8_t i = 0; i < 9; i++) {
/* ... */
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_START, col, 1,
LV_GRID_ALIGN_START, row, 1);
/* ... */
}
这里去除了控件尺寸的伸展,使网格的对齐特点更明显:
网格也可以使用相对大小,具体做法是利用 LV_GRID_FR(x)
宏计算相对宽度。例如,以下定义了一个这样的宽度数组:
static lv_coord_t col_pos[] = { LV_GRID_FR(1), 60, LV_GRID_FR(2), LV_GRID_TEMPLATE_LAST };
那么第二列的宽度是绝对宽度 60 ,剩余的宽度被划分为 3 份:第一列占一份,第三列占 2 份。这种形式创建的网格可以适应容器的尺寸大小:
组合控件
复选框
复选框(ckeckbox)是一种类似开关,但是带有标签的控件。可以使用以下代码创建复选框并设置标签文本:
lv_obj_t* check = lv_checkbox_create(cont);
lv_checkbox_set_text(check, "Use DMA");
一般用复选框并列表示一些“是/否”的选项,因此多个并列的复选项很适合使用 flex 布局表现。复选框可以通过状态 LV_STATE_CHECKED
检查是否被勾选。
LVGL 中没有提供单选按钮(radio button)这一控件,不过可以使用复选框表示单选按钮。单选按钮在同一时间内只有且必须有一个选择框被选中。首先创建一个框架并使用列模式的 flex 布局:
lv_obj_t* cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(cont, 140, 200);
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER);
然后可以在其中创建一些复选框:
#define CHECKBOX_ITEMS 4
char* checkbox_labels[CHECKBOX_ITEMS] = {
"Use parity bits", "Use stop bit", "Auto send", "Debug mode" };
for (uint8_t i = 0; i < CHECKBOX_ITEMS; i++) {
lv_obj_t* check = lv_checkbox_create(cont);
lv_checkbox_set_text(check, checkbox_labels[i]);
}
为了实现单选按钮的效果,需要在点击事件中清除上一个被选中的选择框。这里介绍一个技巧如何获取事件控件的父容器。如果一个控件被设置了冒泡事件标志 LV_OBJ_FLAG_EVENT_BUBBLE
,那么该控件被点击时,事件将会由它的父容器触发(如果父容器也设置了这一标志位,那么事件还会继续向上冒泡)。
可以通过
lv_obj_t* lv_event_get_current_target(lv_event_t* e);
获取最终触发真正送出事件的控件(也就是冒泡后的父控件),而之前介绍的 lv_event_get_target()
函数则获取的是最先触发事件的控件(也就是子控件)。这样通过设置合适的冒泡层数,就可以同时获取控件与它的父容器了。
了解了这一特性后,就可以编写合适的代码了。首先定义一个全局变量 checked_index
记录单选按钮组此刻选中的按钮索引号,并作为用户数据传给回调函数中:
static uint8_t checked_index = 0;
/* ... */
lv_obj_add_event_cb(cont, radio_checked_cb, LV_EVENT_CLICKED, &checked_index);
for (uint8_t i = 0; i < CHECKBOX_ITEMS; i++) {
/* ... */
lv_obj_add_flag(check, LV_OBJ_FLAG_EVENT_BUBBLE);
}
由于事件最终由父容器触发,因此要给父容器提供回调函数。然后,在回调函数中通过父容器与索引值取消上一个被点击的选择框选择,选择点击的选择框并更新索引值:
static void radio_checked_cb(lv_event_t* e) {
uint8_t* post_checked_index = lv_event_get_user_data(e);
lv_obj_t* target = lv_event_get_target(e);
lv_obj_t* parent = lv_event_get_current_target(e);
if (target == parent)
return;
lv_obj_clear_state(lv_obj_get_child(parent, *post_checked_index), LV_STATE_CHECKED);
lv_obj_add_state(target, LV_STATE_CHECKED);
*post_checked_index = lv_obj_get_index(target);
}
由于父容器也拥有点击事件,因此首先要判断事件是否是由选择框触发的。这种事件处理方式非常简洁高效,而且无需定义额外的辅助数组。
这样就可以使用复选框代替单选按钮了,并且这样的回调函数是可以复用的,如果有另一组单选按钮也可以使用类似的方式提供响应:
列表
LVGL 的列表(list)表现形式更像大多数界面提供的标题栏菜单。这里先介绍列表仅仅是因为它比较简单。列表的核心函数只有 3 个:
lv_obj_t *lv_list_create(lv_obj_t *parent);
lv_obj_t *lv_list_add_text(lv_obj_t *list, const char *txt);
lv_obj_t *lv_list_add_btn(lv_obj_t *list, const void *icon, const char *txt);
以下应用这三个函数创建一个列表:
lv_obj_t* list = lv_list_create(lv_scr_act());
lv_list_add_text(list, "group1");
for (uint8_t i = 0; i < 2; i++)
lv_list_add_btn(list, NULL, "item");
lv_list_add_text(list, "group2");
for (uint8_t i = 0; i < 3; i++)
lv_list_add_btn(list, NULL, "item");
效果为:
默认创建的列表尺寸较大,可以手动调整尺寸大小。
列表中的按钮和一般创建的按钮没有区别,可以给返回值提供回调函数。按钮在创建时还可以指定按钮的图标,图标的本质就是 Unicode 中的特殊符号,在 lvgl/src/font/lv_symbol_def.h
中可以查看提供的特殊符号。
参考资料/延伸阅读
https://docs.lvgl.io/master/layouts/index.html
官方文档——布局部分。