最近在使用LVGL的Keypad输入设备进行UI界面的编写,不过发现网上对这方面介绍比较少,于是打算在此记录一下自己对Keypad的一些看法:
LVGL版本:8.3
首先我们来到有关Keypad键值定义的lv_group.h文件,其中关于键值定义的代码为:
lv_group.h:
/*********************
* DEFINES
*********************/
/*Predefined keys to control the focused object via lv_group_send(group, c)*/
enum {
LV_KEY_UP = 17, /*0x11*/
LV_KEY_DOWN = 18, /*0x12*/
LV_KEY_RIGHT = 19, /*0x13*/
LV_KEY_LEFT = 20, /*0x14*/
LV_KEY_ESC = 27, /*0x1B*/
LV_KEY_DEL = 127, /*0x7F*/
LV_KEY_BACKSPACE = 8, /*0x08*/
LV_KEY_ENTER = 10, /*0x0A, '\n'*/
LV_KEY_NEXT = 9, /*0x09, '\t'*/
LV_KEY_PREV = 11, /*0x0B, '*/
LV_KEY_HOME = 2, /*0x02, STX*/
LV_KEY_END = 3, /*0x03, ETX*/
};
typedef uint8_t lv_key_t;
上面有段注释:
/*Predefined keys to control the focused object via lv_group_send(group, c)*/
翻译过来大意为通过 lv_group_send(group,c)函数传递预定义键键值来控制当前聚焦对象,我搜索了一下lv_group_send(group,c)函数,没有找到,只有lv_group_send_data(lv_group_t * group, uint32_t c)函数,功能是一致的不过名称不一样罢了;lv_group_send_data函数在LVGL源码的lv_group.c文件中定义,在indev.c文件中被使用,我们先来讲一下lv_group_send_data函数,即Keypad的键值是如何被传递的。
首先给一个事件触发回调函数的案例:
//事件触发回调函数案例
static void Screen_Main_event_handler (lv_event_t *e)
{
lv_event_code_t code = lv_event_get_code(e); //获取当前事件触发的触发类型
lv_obj_t *target = lv_event_get_target(e); //获取触发该回调的控件
switch (code) {
case LV_EVENT_KEY:
{
uint32_t key = lv_event_get_key(e); //获取当前事件触发对应的keypad键值
if (key == LV_KEY_BACKSPACE) {
}
break;
}
default:
break;
}
}
我们从上面这个例子可以知道,在确认事件类型为LV_EVENT_KEY后,即该事件由keypad按键触发,接下来我们就通过 lv_event_get_key(e);函数来获取触发该LV_EVENT_KEY事件的具体键值;我们看一下lv_event_get_key(e);函数的具体实现如下:
lv_event.c:
uint32_t lv_event_get_key(lv_event_t * e)
{
if(e->code == LV_EVENT_KEY) {
uint32_t * k = lv_event_get_param(e);
if(k) return *k;
else return 0;
}
else {
LV_LOG_WARN("Not interpreted with this event code");
return 0;
}
}
void * lv_event_get_param(lv_event_t * e)
{
return e->param;
}
比较简单,基本上就是返回当前事件触发结构体lv_event_t * e中的parm;那按理来说这个parm就是我们要的键值了,我们可以合理推断键值传递函数lv_res_t lv_group_send_data(lv_group_t * group, uint32_t c)就是把键值传递到e->param,然后由用户通过lv_event_get_key函数获取,我们看看是不是这样:
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;
}
我们从上面的代码中可以看出来,键值传递函数lv_group_send_data(lv_group_t * group, uint32_t c)内部首先获取了输入参数group中聚焦的对象,当对象不为disable状态时,就将当前对象act和事件触发类型LV_EVENT_KEY以及输入参数c通过lv_event_send函数传递给事件触发结构体lv_event_t * e,而lv_event_send函数又将参数c传递到e->param;这里如果想要验证我们的刚刚的猜测,如果输入参数c为按下的键值,那么一切都合理了。另外我们可以猜测lv_group_send_data(lv_group_t * group, uint32_t c)的参数group应该就是和当前输入设备绑定的组;
前面我们提到了lv_group_send_data函数LVGL源码的indev.c文件中被使用,我们现在来看看这个函数是如何被使用的:
indev.c:
/**
* Process a new point from LV_INDEV_TYPE_KEYPAD input device
* @param i pointer to an input device
* @param data pointer to the data read from the input device
*/
static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)
{
if(data->state == LV_INDEV_STATE_PRESSED && i->proc.wait_until_release) return;
if(i->proc.wait_until_release) {
i->proc.wait_until_release = 0;
i->proc.pr_timestamp = 0;
i->proc.long_pr_sent = 0;
i->proc.types.keypad.last_state = LV_INDEV_STATE_RELEASED; /*To skip the processing of release*/
}
lv_group_t * g = i->group;
if(g == NULL) return;
indev_obj_act = lv_group_get_focused(g);
if(indev_obj_act == NULL) return;
bool dis = lv_obj_has_state(indev_obj_act, LV_STATE_DISABLED);
/*Save the last key to compare it with the current latter on RELEASE*/
uint32_t prev_key = i->proc.types.keypad.last_key;
/*Save the last key.
*It must be done here else `lv_indev_get_key` will return the last key in events*/
i->proc.types.keypad.last_key = data->key;
/*Save the previous state so we can detect state changes below and also set the last state now
*so if any event handler on the way returns `LV_RES_INV` the last state is remembered
*for the next time*/
uint32_t prev_state = i->proc.types.keypad.last_state;
i->proc.types.keypad.last_state = data->state;
/*Key press happened*/
if(data->state == LV_INDEV_STATE_PRESSED && prev_state == LV_INDEV_STATE_RELEASED) {
LV_LOG_INFO("%" LV_PRIu32 " key is pressed", data->key);
i->proc.pr_timestamp = lv_tick_get();
/*Move the focus on NEXT*/
if(data->key == LV_KEY_NEXT) {
lv_group_set_editing(g, false); /*Editing is not used by KEYPAD is be sure it is disabled*/
lv_group_focus_next(g);
if(indev_reset_check(&i->proc)) return;
}
/*Move the focus on PREV*/
else if(data->key == LV_KEY_PREV) {
lv_group_set_editing(g, false); /*Editing is not used by KEYPAD is be sure it is disabled*/
lv_group_focus_prev(g);
if(indev_reset_check(&i->proc)) return;
}
else if(!dis) {
/*Simulate a press on the object if ENTER was pressed*/
if(data->key == LV_KEY_ENTER) {
/*Send the ENTER as a normal KEY*/
lv_group_send_data(g, LV_KEY_ENTER);
if(indev_reset_check(&i->proc)) return;
if(!dis) lv_event_send(indev_obj_act, LV_EVENT_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
else if(data->key == LV_KEY_ESC) {
/*Send the ESC as a normal KEY*/
lv_group_send_data(g, LV_KEY_ESC);
if(indev_reset_check(&i->proc)) return;
lv_event_send(indev_obj_act, LV_EVENT_CANCEL, indev_act);
if(indev_reset_check(&i->proc)) return;
}
/*Just send other keys to the object (e.g. 'A' or `LV_GROUP_KEY_RIGHT`)*/
else {
lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
}
}
/*Pressing*/
else if(!dis && data->state == LV_INDEV_STATE_PRESSED && prev_state == LV_INDEV_STATE_PRESSED) {
if(data->key == LV_KEY_ENTER) {
lv_event_send(indev_obj_act, LV_EVENT_PRESSING, indev_act);
if(indev_reset_check(&i->proc)) return;
}
/*Long press time has elapsed?*/
if(i->proc.long_pr_sent == 0 && lv_tick_elaps(i->proc.pr_timestamp) > i->driver->long_press_time) {
i->proc.long_pr_sent = 1;
if(data->key == LV_KEY_ENTER) {
i->proc.longpr_rep_timestamp = lv_tick_get();
lv_event_send(indev_obj_act, LV_EVENT_LONG_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
}
/*Long press repeated time has elapsed?*/
else if(i->proc.long_pr_sent != 0 &&
lv_tick_elaps(i->proc.longpr_rep_timestamp) > i->driver->long_press_repeat_time) {
i->proc.longpr_rep_timestamp = lv_tick_get();
/*Send LONG_PRESS_REP on ENTER*/
if(data->key == LV_KEY_ENTER) {
lv_event_send(indev_obj_act, LV_EVENT_LONG_PRESSED_REPEAT, indev_act);
if(indev_reset_check(&i->proc)) return;
}
/*Move the focus on NEXT again*/
else if(data->key == LV_KEY_NEXT) {
lv_group_set_editing(g, false); /*Editing is not used by KEYPAD is be sure it is disabled*/
lv_group_focus_next(g);
if(indev_reset_check(&i->proc)) return;
}
/*Move the focus on PREV again*/
else if(data->key == LV_KEY_PREV) {
lv_group_set_editing(g, false); /*Editing is not used by KEYPAD is be sure it is disabled*/
lv_group_focus_prev(g);
if(indev_reset_check(&i->proc)) return;
}
/*Just send other keys again to the object (e.g. 'A' or `LV_GROUP_KEY_RIGHT)*/
else {
lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
}
}
/*Release happened*/
else if(!dis && data->state == LV_INDEV_STATE_RELEASED && prev_state == LV_INDEV_STATE_PRESSED) {
LV_LOG_INFO("%" LV_PRIu32 " key is released", data->key);
/*The user might clear the key when it was released. Always release the pressed key*/
data->key = prev_key;
if(data->key == LV_KEY_ENTER) {
lv_event_send(indev_obj_act, LV_EVENT_RELEASED, indev_act);
if(indev_reset_check(&i->proc)) return;
if(i->proc.long_pr_sent == 0) {
lv_event_send(indev_obj_act, LV_EVENT_SHORT_CLICKED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
lv_event_send(indev_obj_act, LV_EVENT_CLICKED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
i->proc.pr_timestamp = 0;
i->proc.long_pr_sent = 0;
}
indev_obj_act = NULL;
}
该函数代码有点长,我们看其中一部分和键值传递函数有关的:
static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*Just send other keys to the object (e.g. 'A' or `LV_GROUP_KEY_RIGHT`)*/
else {
lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
......
}
我们可以看到,在源码中使用键值传递函数时,输入参数分别为g和data->key,而indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)函数中定义lv_group_t * g = i->group;i在indev_keypad_proc上面的注释中有说明为输入设备,我们很自然就可以联想到应该就是当前输入设备,因此“lv_group_send_data(lv_group_t * group, uint32_t c)的参数group应该就是和当前输入设备绑定的组”这个猜测成立;其中 if(indev_reset_check(&i->proc)) return;这个判断是为了检查输入设备是否需要重置,如果需要重置,清理相关全局状态,并指示调用方中止当前输入查询流程,一般我们不会主动重置输入设备,所以这个一般不用管。
/**
* Process a new point from LV_INDEV_TYPE_KEYPAD input device
* @param i pointer to an input device
* @param data pointer to the data read from the input device
*/
static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)
{
if(data->state == LV_INDEV_STATE_PRESSED && i->proc.wait_until_release) return;
if(i->proc.wait_until_release) {
i->proc.wait_until_release = 0;
i->proc.pr_timestamp = 0;
i->proc.long_pr_sent = 0;
i->proc.types.keypad.last_state = LV_INDEV_STATE_RELEASED; /*To skip the processing of release*/
}
lv_group_t * g = i->group;
......
}
而键值传递函数lv_group_send_data(g, data->key);在这里的另一个参数为data->key,通过indev_keypad_proc()函数上面的注释我们可以知道data为输入设备对应的结构体,而data->key我们从lv_port_indev.c文件中关于keypad的读取函数static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)可以知道,data->key就是我们输入的keypad键值。那么“键值传递函数lv_res_t lv_group_send_data(lv_group_t * group, uint32_t c)就是把键值传递到e->param”这个猜测也成立,且输入参数c就为按下的键值;
lv_port_indev.c:
static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;
/*Get whether the a key is pressed and save the pressed key*/
uint32_t act_key = keypad_get_key();
if(act_key != 0) {
data->state = LV_INDEV_STATE_PR;
/*Translate the keys to LVGL control characters according to your key definitions*/
switch(act_key) {
case 1:
act_key = LV_KEY_ESC;
break;
case 2:
act_key = LV_KEY_BACKSPACE;
break;
case 3:
act_key = LV_KEY_DOWN;
break;
case 4:
act_key = LV_KEY_UP;
break;
default:
break;
}
last_key = act_key;
}
else {
data->state = LV_INDEV_STATE_REL;
}
data->key = last_key;
}
/*Get the currently being pressed key. 0 if no key is pressed*/
static uint32_t keypad_get_key(void)
{
/*Your code comes here*/
uint8_t i;
/*Check to buttons see which is being pressed (assume there are 2 buttons)*/
for(i = 0; i < keypad_num; i++) {
/*Return the pressed button's ID*/
if(keypad_is_pressed(i)) {
return i + 1; //由于keypad值不能为0,因此这里+1,保证从1开始
}
}
/*No button pressed*/
return 0; //当没有按钮按下时需要返回0
}
static bool keypad_is_pressed(uint8_t id)
{
if(id >= keypad_num) {
return false; // 检查id是否在有效范围
}
//增加按钮消抖
return HAL_GPIO_ReadPin(keypad_ports[id], keypad_pins[id]) == GPIO_PIN_RESET;
}
由此,我们知道了LVGL中关于Keypad键值传递的大致流程,同时需要注意的是,在indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)中,我们可以看出源码对预定义的键值如 LV_KEY_ESC、LV_KEY_ENTER、LV_KEY_NEXT、LV_KEY_PREV的功能进行了一些预定义,因此在使用上述键值时需要注意,比如LV_KEY_NEXT、LV_KEY_PREV这两个个键值被触发,用lv_event_get_key()函数是获取不到键值的,因为这两个按键按下后并没有调用lv_group_send_data键值传递函数,还有就是ENTER和ESC在一些情况下不止会触发LV_EVENT_KEY事件,还会触发其他类型事件,具体可以自行看函数内部的实现;其他的键值则没有做功能预定义,使用时在事件触发回调函数中直接用v_event_get_key()函数获取键值然后做对应处理即可。同时由于typedef uint8_t lv_key_t,键值类型为uint8_t,最大255,因此我们还可以在键值定义的enum中定义自己的keypad键,只要保证键值不重复即可,类似:
enum {
LV_KEY_UP = 17, /*0x11*/
LV_KEY_DOWN = 18, /*0x12*/
LV_KEY_RIGHT = 19, /*0x13*/
LV_KEY_LEFT = 20, /*0x14*/
LV_KEY_ESC = 27, /*0x1B*/
LV_KEY_DEL = 127, /*0x7F*/
LV_KEY_BACKSPACE = 8, /*0x08*/
LV_KEY_ENTER = 10, /*0x0A, '\n'*/
LV_KEY_NEXT = 9, /*0x09, '\t'*/
LV_KEY_PREV = 11, /*0x0B, '*/
LV_KEY_HOME = 2, /*0x02, STX*/
LV_KEY_END = 3, /*0x03, ETX*/
//自定义键值
LV_KEY_MODE = 200, /*0x03, ETX*/
LV_KEY_BACK = 201, /*0x03, ETX*/
};
typedef uint8_t lv_key_t;
我们知道,在LVGL中关于设备参数_lv_indev_drv_t中有两个值,分别是long_press_time和 long_press_repeat_time;它们的定义如下,我们创建设备初始化时这两个参数的值分别为400和100,意思是按键按下400ms后就认为是长按,在接下来的时间里,如果保持按下,那么每100ms就会执行一次重复触发事件;反映到keypad键值里,就是当你按下keypad的时候,如果长按的话就会频繁触发该键值和对应的回调函数事件。
typedef struct _lv_indev_drv_t {
/**< Input device type*/
lv_indev_type_t type;
/**< Function pointer to read input device data.*/
void (*read_cb)(struct _lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
/** Called when an action happened on the input device.
* The second parameter is the event from `lv_event_t`*/
void (*feedback_cb)(struct _lv_indev_drv_t *, uint8_t);
#if LV_USE_USER_DATA
void * user_data;
#endif
/**< Pointer to the assigned display*/
struct _lv_disp_t * disp;
/**< Timer to periodically read the input device*/
lv_timer_t * read_timer;
/**< Number of pixels to slide before actually drag the object*/
uint8_t scroll_limit;
/**< Drag throw slow-down in [%]. Greater value means faster slow-down*/
uint8_t scroll_throw;
/**< At least this difference should be between two points to evaluate as gesture*/
uint8_t gesture_min_velocity;
/**< At least this difference should be to send a gesture*/
uint8_t gesture_limit;
/**< Long press time in milliseconds*/
uint16_t long_press_time;
/**< Repeated trigger period in long press [ms]*/
uint16_t long_press_repeat_time;
} lv_indev_drv_t;
那么如果你想你按下禁止keypad按键由于按下超过long_press_repeat_time导致重复触发回调函数该怎么做呢?我目前有两种方法:
第一种在输入设备初始化时把long_press_repeat_time设置为最大值,即0xFFFF,这样只能保证按下按键大概一分钟内不会重复触发LV_EVENT_KEY事件回调函数;
lv_indev_port.c:
void lv_port_indev_init(void)
{
......
/*Initialize your keypad or keyboard if you have*/
keypad_init();
/*Register a keypad input device*/
lv_indev_drv_init(&indev_keypad_drv); //初始化输入设备结构体
indev_keypad_drv.type = LV_INDEV_TYPE_KEYPAD;
indev_keypad_drv.read_cb = keypad_read;
indev_keypad_drv.long_press_time = 1000; //自定义
indev_keypad_drv.long_press_repeat_time = 0xFFFF;
indev_keypad = lv_indev_drv_register(&indev_keypad_drv);
......
}
第二种就是修改LVGL源码中indev.c文件中的indev_keypad_proc函数,将长按连续触发判断中发送LV_EVENT_KEY事件的代码注释掉,这样按下按键不管多久都能禁止重复触发LV_EVENT_KEY事件回调函数,如下:
lv_indev_port.c:
static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*Long press repeated time has elapsed?*/
......
/*Just send other keys again to the object (e.g. 'A' or `LV_GROUP_KEY_RIGHT)*/
else {
//lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
}
......
}
另外开一个坑,前面我们提到了一个特别长的函数为static void indev_keypad_proc(lv_indev_t * i, lv_indev_data_t * data),这个是关于keypad设备的键值处理函数,同时还有其他例如encoder编码器的键值处理函数等,他们都在设备定时器回调函数lv_indev_read_timer_cb()函数中被调用,我大概看了一下,我们的事件处理函数lv_task_handler();里面就有调用定时器链表并执行了相关回调函数,lv_indev_read_timer_cb()函数应该也是在lv_task_handler();里面被执行,由此就实现了对外部输入按键的实时监测和判断。感觉这个定时器链表还挺有意思的,后面有时间可以讲一下lv_task_handler()是如何工作的,同时lv_indev_read_timer_cb()又是如何被调用的
void lv_indev_read_timer_cb(lv_timer_t * timer)
{
INDEV_TRACE("begin");
lv_indev_data_t data;
indev_act = timer->user_data;
/*Read and process all indevs*/
if(indev_act->driver->disp == NULL) return; /*Not assigned to any displays*/
/*Handle reset query before processing the point*/
indev_proc_reset_query_handler(indev_act);
if(indev_act->proc.disabled ||
indev_act->driver->disp->prev_scr != NULL) return; /*Input disabled or screen animation active*/
bool continue_reading;
do {
/*Read the data*/
_lv_indev_read(indev_act, &data);
continue_reading = data.continue_reading;
/*The active object might be deleted even in the read function*/
indev_proc_reset_query_handler(indev_act);
indev_obj_act = NULL;
indev_act->proc.state = data.state;
/*Save the last activity time*/
if(indev_act->proc.state == LV_INDEV_STATE_PRESSED) {
indev_act->driver->disp->last_activity_time = lv_tick_get();
}
else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER && data.enc_diff) {
indev_act->driver->disp->last_activity_time = lv_tick_get();
}
if(indev_act->driver->type == LV_INDEV_TYPE_POINTER) {
indev_pointer_proc(indev_act, &data);
}
else if(indev_act->driver->type == LV_INDEV_TYPE_KEYPAD) {
indev_keypad_proc(indev_act, &data);
}
else if(indev_act->driver->type == LV_INDEV_TYPE_ENCODER) {
indev_encoder_proc(indev_act, &data);
}
else if(indev_act->driver->type == LV_INDEV_TYPE_BUTTON) {
indev_button_proc(indev_act, &data);
}
/*Handle reset query if it happened in during processing*/
indev_proc_reset_query_handler(indev_act);
} while(continue_reading);
/*End of indev processing, so no act indev*/
indev_act = NULL;
indev_obj_act = NULL;
INDEV_TRACE("finished");
}