LVGL版本:8.3
前面我们说过了,LVGL实现大概分为三个步骤:检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面;而第二步“我们编写的逻辑”就是通过EVENT事件回调函数实现的;下面给出一个简单的应用Event事件的代码:
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_add_event_cb(btn, my_event_cb, LV_EVENT_CLICKED, NULL); /*Assign an event callback*/
...
static void my_event_cb(lv_event_t * event)
{
printf("Clicked\n");
}
我们可以看到,调用我们编写的逻辑即EVENT事件回调函数,我们只需要首先创建一个控件,然后写出对应的EVENT事件回调函数,最后通过struct _lv_event_dsc_t * lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb, lv_event_code_t filter,void * user_data)将控件、EVENT事件回调函数和外部操作三者绑定在一起即可,最后一项可选,是用户想向EVENT事件回调函数中传入的自定义数据指针。上面这个例子就是当btn控件受到外部操作为LV_EVENT_CLICKED时就调用my_event_cb()回调函数;
那接下来我们就来看看lv_obj_add_event_cb()函数参数的数据类型和函数的内部实现:
lv_event.c:
struct _lv_event_dsc_t * lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb, lv_event_code_t filter,
void * user_data)
{
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_obj_allocate_spec_attr(obj);
obj->spec_attr->event_dsc_cnt++;
obj->spec_attr->event_dsc = lv_mem_realloc(obj->spec_attr->event_dsc,
obj->spec_attr->event_dsc_cnt * sizeof(lv_event_dsc_t));
LV_ASSERT_MALLOC(obj->spec_attr->event_dsc);
obj->spec_attr->event_dsc[obj->spec_attr->event_dsc_cnt - 1].cb = event_cb;
obj->spec_attr->event_dsc[obj->spec_attr->event_dsc_cnt - 1].filter = filter;
obj->spec_attr->event_dsc[obj->spec_attr->event_dsc_cnt - 1].user_data = user_data;
return &obj->spec_attr->event_dsc[obj->spec_attr->event_dsc_cnt - 1];
}
lv_obj.h:
typedef struct _lv_obj_t {
const lv_obj_class_t * class_p;
struct _lv_obj_t * parent;
_lv_obj_spec_attr_t * spec_attr;
_lv_obj_style_t * styles;
#if LV_USE_USER_DATA
void * user_data;
#endif
lv_area_t coords;
lv_obj_flag_t flags;
lv_state_t state;
uint16_t layout_inv : 1;
uint16_t scr_layout_inv : 1;
uint16_t skip_trans : 1;
uint16_t style_cnt : 6;
uint16_t h_layout : 1;
uint16_t w_layout : 1;
} lv_obj_t;
typedef struct {
struct _lv_obj_t ** children; /**< Store the pointer of the children in an array.*/
uint32_t child_cnt; /**< Number of children*/
lv_group_t * group_p;
struct _lv_event_dsc_t * event_dsc; /**< Dynamically allocated event callback and user data array*/
lv_point_t scroll; /**< The current X/Y scroll offset*/
lv_coord_t ext_click_pad; /**< Extra click padding in all direction*/
lv_coord_t ext_draw_size; /**< EXTend the size in every direction for drawing.*/
lv_scrollbar_mode_t scrollbar_mode : 2; /**< How to display scrollbars*/
lv_scroll_snap_t scroll_snap_x : 2; /**< Where to align the snappable children horizontally*/
lv_scroll_snap_t scroll_snap_y : 2; /**< Where to align the snappable children vertically*/
lv_dir_t scroll_dir : 4; /**< The allowed scroll direction(s)*/
uint8_t event_dsc_cnt; /**< Number of event callbacks stored in `event_dsc` array*/
} _lv_obj_spec_attr_t;
lv_event.c:
typedef struct _lv_event_dsc_t {
lv_event_cb_t cb;
void * user_data;
lv_event_code_t filter : 8;
} lv_event_dsc_t;
函数内部逻辑主要为:首先通过lv_obj_allocate_spec_attr(obj);函数判断这次绑定事件的控件的spec_attr指针是否为空,我们可以看到lv_obj_t数据类型中spec_attr是一个_lv_obj_spec_attr_t类型指针,这里判断没有为spec_attr分配地址的话,就创建一个_lv_obj_spec_attr_t大小的内存并让spec_attr指针指向该内存;接下来我们就开始将控件obj和EVENT事件回调函数以及外部操作以及用户数据指针绑定在一起,这里我们看到_lv_obj_spec_attr_t数据结构体中有一个struct _lv_event_dsc_t * event_dsc结构体指针,_lv_event_dsc_t数据类型中就包含EVENT事件回调函数和外部操作以及自定义数据指针,我们只需要将函数lv_obj_add_event_cb(lv_obj_t * obj, lv_event_cb_t event_cb, lv_event_code_t filter,void * user_data)的后三个输入参数和控件obj中的 lv_event_cb_t cb;void * user_data;和 lv_event_code_t filter : 8;一一赋值即可。
但是我们看到lv_obj_add_event_cb函数中并不是简单的赋值,而是有一些额外的操作,这就要说到直接赋值的缺点了,那就是一个obj控件只能和一个EVENT事件回调函数、外部操作和用户数据指针绑定在一起。因此这里我们发现_lv_event_dsc_t * event_dsc结构体指针实际上是指向一个以_lv_event_dsc_t 数据类型的数据为元素的动态数组,这里通过event_dsc_cnt表示数组元素个数,每次控件obj新绑定一个EVENT事件回调函数event_dsc_cnt就加一,且用lv_mem_realloc函数来分配一个新的数组空间,新的数组内容在原数组基础上不变,空间比原数组大一个数组元素的大小;最后为新增的数组元素和函数输入参数一一绑定即可实现控件和EVENT事件回调函数等绑定在一起的功能;
我们现在知道了控件已经和EVENT事件回调函数绑定在一起了,接下来我们就看看该回调函数是怎么通过控件被外部操作时得到触发的,这里我们以Keypad输入设备为例:
我们在之前的indev.c中可以看到关于keypad输入设备的处理函数static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data),在该函数中就会判断按下的按键的键值,然后做一些对应处理,最后调用lv_group_send_data(g, data->key);函数将按下的键值通过lv_event_send()函数传递到要触发的lv_event_t e事件中;我们可以看到当前group组的focus对象也一并传入到了lv_event_t e中,而关键就在lv_event_send()函数中调用的 event_send_core()函数,该函数就是EVENT事件的响应的核心;在event_send_core 函数中响应传入的event,如果我们之前通过lv_obj_add_event_cb()函数将当前group组的focus对象控件和对应EVENT事件回调函数绑定了,就依次调用和该控件绑定的所有EVENT事件回调函数。
lv_group.c:
lv_res_t lv_group_send_data(lv_group_t * group, uint32_t c)
{
lv_obj_t * act = lv_group_get_focused(group);
if(act == NULL) return LV_RES_OK;
if(lv_obj_has_state(act, LV_STATE_DISABLED)) return LV_RES_OK;
return lv_event_send(act, LV_EVENT_KEY, &c);
}
lv_event.c:
lv_res_t lv_event_send(lv_obj_t * obj, lv_event_code_t event_code, void * param)
{
if(obj == NULL) return LV_RES_OK;
LV_ASSERT_OBJ(obj, MY_CLASS);
lv_event_t e;
e.target = obj;
e.current_target = obj;
e.code = event_code;
e.user_data = NULL;
e.param = param;
e.deleted = 0;
e.stop_bubbling = 0;
e.stop_processing = 0;
/*Build a simple linked list from the objects used in the events
*It's important to know if this object was deleted by a nested event
*called from this `event_cb`.*/
e.prev = event_head;
event_head = &e;
/*Send the event*/
lv_res_t res = event_send_core(&e);
/*Remove this element from the list*/
event_head = e.prev;
return res;
}
static lv_res_t event_send_core(lv_event_t * e)
{
EVENT_TRACE("Sending event %d to %p with %p param", e->code, (void *)e->current_target, e->param);
/*Call the input device's feedback callback if set*/
lv_indev_t * indev_act = lv_indev_get_act();
if(indev_act) {
if(indev_act->driver->feedback_cb) indev_act->driver->feedback_cb(indev_act->driver, e->code);
if(e->stop_processing) return LV_RES_OK;
if(e->deleted) return LV_RES_INV;
}
lv_res_t res = LV_RES_OK;
lv_event_dsc_t * event_dsc = lv_obj_get_event_dsc(e->current_target, 0);
uint32_t i = 0;
while(event_dsc && res == LV_RES_OK) {
if(event_dsc->cb && ((event_dsc->filter & LV_EVENT_PREPROCESS) == LV_EVENT_PREPROCESS)
&& (event_dsc->filter == (LV_EVENT_ALL | LV_EVENT_PREPROCESS) ||
(event_dsc->filter & ~LV_EVENT_PREPROCESS) == e->code)) {
e->user_data = event_dsc->user_data;
event_dsc->cb(e);
if(e->stop_processing) return LV_RES_OK;
/*Stop if the object is deleted*/
if(e->deleted) return LV_RES_INV;
}
i++;
event_dsc = lv_obj_get_event_dsc(e->current_target, i);
}
res = lv_obj_event_base(NULL, e);
event_dsc = res == LV_RES_INV ? NULL : lv_obj_get_event_dsc(e->current_target, 0);
i = 0;
while(event_dsc && res == LV_RES_OK) {
if(event_dsc->cb && ((event_dsc->filter & LV_EVENT_PREPROCESS) == 0)
&& (event_dsc->filter == LV_EVENT_ALL || event_dsc->filter == e->code)) {
e->user_data = event_dsc->user_data;
event_dsc->cb(e);
if(e->stop_processing) return LV_RES_OK;
/*Stop if the object is deleted*/
if(e->deleted) return LV_RES_INV;
}
i++;
event_dsc = lv_obj_get_event_dsc(e->current_target, i);
}
if(res == LV_RES_OK && e->current_target->parent && event_is_bubbled(e)) {
e->current_target = e->current_target->parent;
res = event_send_core(e);
if(res != LV_RES_OK) return LV_RES_INV;
}
return res;
}
event_send_core函数执行步骤如下:
一、获取输入设备的句柄,调用 feedback_cb 来处理触摸,按键这些事件,feedback_cb 是输入设备驱动程序中的一个可选回调函数,用于在事件触发后给输入设备提供反馈如
- 在触摸屏或按钮交互时提供振动反馈(比如点击按钮后手机振动)。
- 在键盘输入时触发声音提示(如按键音)。
- 其他自定义反馈机制,例如 LED 闪烁等。
我们可以在lv_indev_port.c中注册设备时定义该设备对应的 feedback_cb,如
lv_indev_port.c:
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.feedback_cb = my_feedback_cb;
lv_indev_t * indev = lv_indev_drv_register(&indev_drv);
void my_feedback_cb(lv_indev_drv_t * drv, lv_event_code_t event_code) {
if(event_code == LV_EVENT_CLICKED) {
vibrate_device(100); // 触发 100 毫秒的震动
}
}
二、事件预处理:优先级高的事件处理。LV_EVENT_PREPROCESS标志位用于标记高优先级事件。用法类似lv_obj_add_event_cb(btn, my_event_cb, LV_EVENT_PREPROCESS | LV_EVENT_CLICKED, NULL);注意只有该标志位才能和其他事件描述符 |,事件描述符和事件描述符之间只能独立使用不能通过 |使用;
三、基础事件处理:对象的基类的事件处理,控件面向LVGL系统的事件,这个基础事件和高优先级事件以及常规优先级事件不一样,后两者是由我们程序员自己定义的,而基础事件是LVGL内部规定好的。不同的控件有不同的基础事件,我们通过每个控件对应的基础事件内容也可以了解该控件的一些使用方法和特性。这里拿下拉列表控件dropdowm举例:
lv_dropdown.:
const lv_obj_class_t lv_dropdown_class = {
.constructor_cb = lv_dropdown_constructor,
.destructor_cb = lv_dropdown_destructor,
.event_cb = lv_dropdown_event,
.width_def = LV_DPI_DEF,
.height_def = LV_SIZE_CONTENT,
.instance_size = sizeof(lv_dropdown_t),
.editable = LV_OBJ_CLASS_EDITABLE_TRUE,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.base_class = &lv_obj_class
};
const lv_obj_class_t lv_dropdownlist_class = {
.constructor_cb = lv_dropdownlist_constructor,
.destructor_cb = lv_dropdownlist_destructor,
.event_cb = lv_dropdown_list_event,
.instance_size = sizeof(lv_dropdown_list_t),
.base_class = &lv_obj_class
};
static void lv_dropdown_event(const lv_obj_class_t * class_p, lv_event_t * e)
{
LV_UNUSED(class_p);
lv_res_t res;
/*Call the ancestor's event handler*/
res = lv_obj_event_base(MY_CLASS, e);
if(res != LV_RES_OK) return;
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * obj = lv_event_get_target(e);
lv_dropdown_t * dropdown = (lv_dropdown_t *)obj;
if(code == LV_EVENT_FOCUSED) {
lv_group_t * g = lv_obj_get_group(obj);
bool editing = lv_group_get_editing(g);
lv_indev_type_t indev_type = lv_indev_get_type(lv_indev_get_act());
/*Encoders need special handling*/
if(indev_type == LV_INDEV_TYPE_ENCODER) {
/*Open the list if editing*/
if(editing) {
lv_dropdown_open(obj);
}
/*Close the list if navigating*/
else {
dropdown->sel_opt_id = dropdown->sel_opt_id_orig;
lv_dropdown_close(obj);
}
}
}
else if(code == LV_EVENT_DEFOCUSED || code == LV_EVENT_LEAVE) {
lv_dropdown_close(obj);
}
else if(code == LV_EVENT_RELEASED) {
res = btn_release_handler(obj);
if(res != LV_RES_OK) return;
}
else if(code == LV_EVENT_STYLE_CHANGED) {
lv_obj_refresh_self_size(obj);
}
else if(code == LV_EVENT_SIZE_CHANGED) {
lv_obj_refresh_self_size(obj);
}
else if(code == LV_EVENT_GET_SELF_SIZE) {
lv_point_t * p = lv_event_get_param(e);
const lv_font_t * font = lv_obj_get_style_text_font(obj, LV_PART_MAIN);
p->y = lv_font_get_line_height(font);
}
else if(code == LV_EVENT_KEY) {
char c = *((char *)lv_event_get_param(e));
if(c == LV_KEY_RIGHT || c == LV_KEY_DOWN) {
if(!lv_dropdown_is_open(obj)) {
lv_dropdown_open(obj);
}
else if(dropdown->sel_opt_id + 1 < dropdown->option_cnt) {
dropdown->sel_opt_id++;
position_to_selected(obj);
}
}
else if(c == LV_KEY_LEFT || c == LV_KEY_UP) {
if(!lv_dropdown_is_open(obj)) {
lv_dropdown_open(obj);
}
else if(dropdown->sel_opt_id > 0) {
dropdown->sel_opt_id--;
position_to_selected(obj);
}
}
else if(c == LV_KEY_ESC) {
dropdown->sel_opt_id = dropdown->sel_opt_id_orig;
lv_dropdown_close(obj);
}
else if(c == LV_KEY_ENTER) {
/* Handle the ENTER key only if it was send by an other object.
* Do no process it if ENTER is sent by the dropdown because it's handled in LV_EVENT_RELEASED */
lv_obj_t * indev_obj = lv_indev_get_obj_act();
if(indev_obj != obj) {
res = btn_release_handler(obj);
if(res != LV_RES_OK) return;
}
}
}
else if(code == LV_EVENT_DRAW_MAIN) {
draw_main(e);
}
}
static void lv_dropdown_list_event(const lv_obj_class_t * class_p, lv_event_t * e)
{
LV_UNUSED(class_p);
lv_res_t res;
/*Call the ancestor's event handler*/
lv_event_code_t code = lv_event_get_code(e);
if(code != LV_EVENT_DRAW_POST) {
res = lv_obj_event_base(MY_CLASS_LIST, e);
if(res != LV_RES_OK) return;
}
lv_obj_t * list = lv_event_get_target(e);
lv_obj_t * dropdown_obj = ((lv_dropdown_list_t *)list)->dropdown;
lv_dropdown_t * dropdown = (lv_dropdown_t *)dropdown_obj;
if(code == LV_EVENT_RELEASED) {
if(lv_indev_get_scroll_obj(lv_indev_get_act()) == NULL) {
list_release_handler(list);
}
}
else if(code == LV_EVENT_PRESSED) {
list_press_handler(list);
}
else if(code == LV_EVENT_SCROLL_BEGIN) {
dropdown->pr_opt_id = LV_DROPDOWN_PR_NONE;
lv_obj_invalidate(list);
}
else if(code == LV_EVENT_DRAW_POST) {
draw_list(e);
res = lv_obj_event_base(MY_CLASS_LIST, e);
if(res != LV_RES_OK) return;
}
}
上述函数就展示了下拉列表dropdown控件的基础事件,是LVGL预先定义好的,每个控件创建时都会和自己的基础事件进行绑定;
四、事件回调:调用常规优先级的事件回调函数,即我们前面通过lv_obj_add_event_cb(btn, my_event_cb, LV_EVENT_CLICKED, NULL);绑定的my_event_cb函数;
五、事件冒泡:向父对象传递事件。如果当前 obj 是支持 Bubble 的话,那么它会向它的父对象 parent (如果有父对象)进行传递,直到某一个 parent 不支持 bubble 为止;
至此我们知道了LVGL的EVENT事件响应是如何触发的了,本质就是通过输入设备的处理函数类似static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)在其中调用event_send_core函数,将当前发生的事件和控件等参数传入event_send_core进行处理;而LVGL的三大步骤:检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面;我们也有了一个初步了解,知道LVGL大概是如何运行的了,后面有时间会讲一下keypad输入设备的导航态和编辑态以及一些控件的使用方法;
参考文章: