LVGL学习笔记(六)
事件冒泡
前言
在上一篇中我们介绍了事件的相关概念以及其初步的使用,在阐述的过程中提及了事件冒泡,这一篇主要围绕事件冒泡相关概念进行阐述,并举实例实现一个事件冒泡的实例。
-
什么是事件冒泡?
当一个事件在对象上被触发后,LVGL首先执行该对象上所有回调函数。如果事件冒泡未被阻止,同一事件会被重新派发给父对象,然后继续向上传播,直到抵达屏幕根对象。
由于这个过程像水中的气泡从深处不断向上冒出,因此得名事件冒泡。
-
为什么需要事件冒泡,事件冒泡的使用场景是什么?
举例来说,你需要设计一个计算器面板,按照正常事件的逻辑,每一个按钮你都需要绑定一个回调函数,然后将按钮标签上对应的值传递到回调函数中进行处理。
如果只有1-2个按钮,问题还不是太大。但如果是几十个按钮,挨个绑定回调函数造成的代码重复不说,单单是后期几十份回调函数的维护就十分麻烦,更不用说还会增加内存的开销,造成不需要的浪费。
这个时候就需要使用冒泡事件了,他的核心价值就是把一堆子控件的事件,交给父对象统一处理,而不用给每一个按钮单独绑定回调函数。
事件冒泡有如下的使用场景:
1.一组同类按钮,统一管理,例如数字键盘、功能按键栏、一排选项卡、九宫格菜单等;
2.容器整体拦截事件。比如一个卡片容器,卡片里面包含文字、小图标按钮。点击卡片内部任意位置,都可以触发卡片的点击效果。开启冒泡后,点里面文字,事件上浮到卡片父对象,卡片回调被触发。
3.事件统一过滤。父对象先收到冒泡上来的事件,可以做统一处理,过滤掉不需要的信息。
-
如何实现事件冒泡?
还是以上面的计算器举例,创建按钮对象后,开启回调函数,然后启动启动函数lv_obj_add_flag(),函数原型如下:
cvoid lv_obj_add_flag(lv_obj_t * obj, lv_obj_flag_t f);函数lv_obj_add_flag()是LVGL 中用于给对象添加特定行为或状态标志的函数。通过为对象设置不同的 lv_obj_flag_t 枚举值,可以控制对象在界面中的交互方式、显示效果以及布局行为。
形参如下:
形参 说明 obj 目标对象指针 f 要添加的标志 标志结构体如下所示:
cenum { LV_OBJ_FLAG_HIDDEN = (1L << 0), 隐藏对象(不参与布局与绘制) LV_OBJ_FLAG_CLICKABLE = (1L << 1), 对象可被点击 LV_OBJ_FLAG_CLICK_FOCUSABLE = (1L << 2), LV_OBJ_FLAG_CHECKABLE = (1L << 3), LV_OBJ_FLAG_SCROLLABLE = (1L << 4), LV_OBJ_FLAG_SCROLL_ELASTIC = (1L << 5), LV_OBJ_FLAG_SCROLL_MOMENTUM = (1L << 6), LV_OBJ_FLAG_SCROLL_ONE = (1L << 7), LV_OBJ_FLAG_SCROLL_CHAIN_HOR = (1L << 8), LV_OBJ_FLAG_SCROLL_CHAIN_VER = (1L << 9), LV_OBJ_FLAG_SCROLL_ON_FOCUS = (1L << 10), 获得焦点时自动滚动到可见区域 LV_OBJ_FLAG_SCROLL_WITH_ARROW = (1L << 11), LV_OBJ_FLAG_SNAPPABLE = (1L << 12), LV_OBJ_FLAG_PRESS_LOCK = (1L << 13), LV_OBJ_FLAG_EVENT_BUBBLE = (1L << 14), 事件冒泡到父对象 LV_OBJ_FLAG_GESTURE_BUBBLE = (1L << 15), 手势事件冒泡到父对象 LV_OBJ_FLAG_ADV_HITTEST = (1L << 16), 启用高级命中测试(自定义点击区域) LV_OBJ_FLAG_IGNORE_LAYOUT = (1L << 17), LV_OBJ_FLAG_FLOATING = (1L << 18), LV_OBJ_FLAG_OVERFLOW_VISIBLE = (1L << 19), 允许子对象绘制超出父对象范围 LV_OBJ_FLAG_LAYOUT_1 = (1L << 23), LV_OBJ_FLAG_LAYOUT_2 = (1L << 24), LV_OBJ_FLAG_WIDGET_1 = (1L << 25), LV_OBJ_FLAG_WIDGET_2 = (1L << 26), LV_OBJ_FLAG_USER_1 = (1L << 27), LV_OBJ_FLAG_USER_2 = (1L << 28), LV_OBJ_FLAG_USER_3 = (1L << 29), LV_OBJ_FLAG_USER_4 = (1L << 30), }; typedef uint32_t lv_obj_flag_t;这里选择LV_OBJ_FLAG_EVENT_BUBBLE,事件冒泡。
事件从标签依次冒泡到按钮->父对象面板->屏幕,展现了层级传递机制。
接下来需要获取对象传递过来的信息,这就需要上篇中我们未详细说明的函数lv_event_get_user_data(),其原型如下:
cvoid * lv_event_get_user_data(lv_event_t * e)函数lv_event_get_user_data()是 LVGL库中用于获取事件回调函数中用户自定义数据的函数。它在事件处理逻辑中起到了跨作用域传递数据的作用,常用于在回调函数中访问绑定的自定义数据。
- 形参: e: 事件对象指针,由 LVGL 事件系统自动传递到回调函数中。
- 返回值: 返回 void* 类型的用户自定义数据指针,需根据实际类型进行强制转换后使用。
接下来,将上述举例的计算器面板实现出来,代码如下所示:
定义全局变量
c
// 显示屏标签全局对象
static lv_obj_t* label_display;
static char calc_buffer[64] = { 0 };
// 全局按键字符串表
static const char* key_list[] =
{
"0","1","2","3","4","5","6","7","8","9","+","-","*","/","="
};
设置LVGL主函数,网格布局函数内容可以翻阅学习笔记四
c
void lv_mainstart(void)
{
//主屏幕
lv_obj_t* root = lv_scr_act();
//创建显示屏
label_display = lv_label_create(root);
lv_label_set_text(label_display, "");
lv_obj_set_size(label_display, 300, 80);
lv_obj_align(label_display, LV_ALIGN_TOP_MID, 0, 20);
lv_obj_set_style_text_align(label_display, LV_TEXT_ALIGN_RIGHT, 0);
//按键父对象(网格布局,放置所有按键)
lv_obj_t* keypad_panel = lv_obj_create(root);
lv_obj_set_size(keypad_panel, 300, 400);
lv_obj_align_to(keypad_panel, label_display, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);
//给父对象添加点击回调函数,所有子按钮冒泡上来的事件在这里接收
lv_obj_add_event_cb(keypad_panel, keypad_parent_cb, LV_EVENT_CLICKED, NULL);
// 开启网格布局排列
lv_obj_set_layout(keypad_panel, LV_LAYOUT_GRID);
// 定义列描述:4列,宽度都为50px
static lv_coord_t col_dsc[] = { 50,50,50,50, LV_GRID_TEMPLATE_LAST };
// 定义行描述:4行,高度都为50px
static lv_coord_t row_dsc[] = { 50,50,50,50, LV_GRID_TEMPLATE_LAST };
// 设置对象网格布局行描述数组
lv_obj_set_style_grid_column_dsc_array(keypad_panel, col_dsc, 0);
// 设置对象网格布局列描述数组
lv_obj_set_style_grid_row_dsc_array(keypad_panel, row_dsc, 0);
//设置网格布局中列与行的整体对齐方式
lv_obj_set_grid_align(keypad_panel, LV_GRID_ALIGN_CENTER, LV_GRID_ALIGN_CENTER);
//创建按键
lv_obj_t* btn;
btn = create_key_btn(keypad_panel, "1", key_list[1]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 0, 1);
btn = create_key_btn(keypad_panel, "2", key_list[2]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 0, 1);
btn = create_key_btn(keypad_panel, "3", key_list[3]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 0, 1);
btn = create_key_btn(keypad_panel, "+", key_list[10]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 0, 1);
btn = create_key_btn(keypad_panel, "4", key_list[4]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
btn = create_key_btn(keypad_panel, "5", key_list[5]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
btn = create_key_btn(keypad_panel, "6", key_list[6]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
btn = create_key_btn(keypad_panel, "-", key_list[11]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 1, 1);
btn = create_key_btn(keypad_panel, "7", key_list[7]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 2, 1);
btn = create_key_btn(keypad_panel, "8", key_list[8]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 2, 1);
btn = create_key_btn(keypad_panel, "9", key_list[9]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 2, 1, LV_GRID_ALIGN_STRETCH, 2, 1);
btn = create_key_btn(keypad_panel, "*", key_list[12]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 2, 1);
btn = create_key_btn(keypad_panel, "0", key_list[0]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 3, 1);
btn = create_key_btn(keypad_panel, "=", key_list[14]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 1, 2, LV_GRID_ALIGN_STRETCH, 3, 1);
btn = create_key_btn(keypad_panel, "/", key_list[13]);
lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, 3, 1, LV_GRID_ALIGN_STRETCH, 3, 1);
}
设置按钮创建函数
c
static lv_obj_t* create_key_btn(lv_obj_t* parent, const char* text, const char* key_text)
{
//创建按键
lv_obj_t* btn = lv_btn_create(parent);
//设置按钮大小
lv_obj_set_size(btn, 50, 50);
//开启事件冒泡:点击事件向上传递给父对象
lv_obj_add_flag(btn, LV_OBJ_FLAG_EVENT_BUBBLE);
//获取用户数据
lv_obj_set_user_data(btn, (void*)key_text);
//创建标签
lv_obj_t* label = lv_label_create(btn);
//设置标签内容
lv_label_set_text(label, text);
//对象居中
lv_obj_center(label);
return btn;
}
设置回调函数
c
/**
* @brief 父容器的事件回调(利用冒泡事件统一处理所有子按键)
*/
static void keypad_parent_cb(lv_event_t* e)
{
// 获取真正被点击的子对象
lv_obj_t* target = lv_event_get_target(e);
//取出按键上绑定的字符
char* key_char = (char*)lv_obj_get_user_data(target);
if (key_char == NULL)
return;
// 判断按下等于号
if (strcmp(key_char, "=") == 0)
{
float res;
int ret = calc_two_num(calc_buffer, &res);
if (ret == 0)
{
//计算成功,打印结果
snprintf(calc_buffer, sizeof(calc_buffer), "%.2f", res);
LV_LOG_USER("sum = %f", res);
}
else
{
//算式非法
strcpy(calc_buffer, "ERROR");
}
//刷新显示屏标签
lv_label_set_text(label_display, calc_buffer);
//计算完成清空缓存,准备下一轮计算
memset(calc_buffer, 0, sizeof(calc_buffer));
}
//普通数字/运算符按键,追加字符
else
{
//追加按键字符到全局缓冲区
strcat(calc_buffer, key_char);
//更新屏幕标签
lv_label_set_text(label_display, calc_buffer);
}
}
设置计算器应用逻辑(只是举例,可以自行设计)
c
/**
* @brief 简易计算器,解析字符串算式,只支持 两个数运算,例:"12+34"
* @param expr 算式字符串
* @param result 输出结果
* @return 0成功,‑1失败
*/
static int calc_two_num(const char* expr, float* result)
{
char op = 0;
int index = -1;
float num1, num2;
//遍历字符串找到运算符 + - * /
for (int i = 0; expr[i] != '\0'; i++)
{
if (expr[i] == '+' || expr[i] == '-' || expr[i] == '*' || expr[i] == '/')
{
op = expr[i];
index = i;
break;
}
}
//没有找到运算符
if (index <= 0)
return -1;
//拆分前后两个数字
char num1_str[32] = { 0 }, num2_str[32] = { 0 };
strncpy(num1_str, expr, index);
strcpy(num2_str, expr + index + 1);
num1 = atof(num1_str);
num2 = atof(num2_str);
switch (op)
{
case '+': *result = num1 + num2; break;
case '-': *result = num1 - num2; break;
case '*': *result = num1 * num2; break;
case '/':
if (num2 == 0) return -1; //除零报错
*result = num1 / num2;
break;
default: return -1;
}
return 0;
}
运行结果:
LVGL计算器演示实例_事件冒泡