LVGL版本:8.3
Group组:在 LVGL 中,Group 是用于管理一组对象的概念,允许用户通过单一输入设备(例如按键或旋转编码器)来操作和聚焦这些对象。它是为了支持 键盘操作、编码器导航或类似输入设备的场景设计的,弥补了触摸屏交互模式以外的需求。而对于触摸屏和鼠标来说就不存在Group这个概念。
Group 是一组控件的集合,其中只有一个控件处于焦点(Focus)状态。焦点控件会接收输入事件(如键盘上下键或编码器旋转信号),自动实现控件间的焦点切换。
导航态:导航态是指用户在界面中通过键盘、旋钮或编码器等输入设备在多个控件之间切换或导航的状态。
-
特点:
-
焦点切换:用户可以在界面中的不同控件之间移动焦点,但不会直接修改控件的值。
-
无直接操作:控件处于选中或聚焦状态,但用户尚未进入具体控件的编辑模式。
-
视觉提示:控件通常会有聚焦样式(如边框高亮或颜色变化)来提示用户当前的焦点位置。
-
-
典型场景:在菜单中选择不同的选项;在表单中移动光标以选中不同的输入框。
编辑态:编辑态是指用户进入一个控件并对其内容或属性进行修改的状态。
-
特点:
-
控件操作:用户可以对当前选中的控件进行操作(如输入文字、调整数值等)。
-
固定焦点:焦点不会在控件之间切换,而是锁定在当前控件。
-
编辑反馈:控件可能会显示实时的值变化(如数值增加或减少、文字输入等)。
-
-
典型场景:在数值控件中调整数值(如通过旋钮增加或减少);在文本框中输入或修改内容。
关于导航态和编辑态的使用,首先我们需要知道的是对于编码器和Keypad输入设备来说才有导航态和编辑态的概念(该概念个人认为更侧重于在编码器模式上的使用),和group组配合使用。从void lv_group_set_editing(lv_group_t * group, bool edit)函数以及lv_group_t数据类型就可以看出来group和导航态编辑态有很深刻的联系,目前关于切换导航态和编辑态的函数我也只看到lv_group_set_editing();其次所有输入设备的键值处理函数只有编码器和Keypad的indev_keypad_proc和indev_encoder_proc中才有关于导航态和编辑态的切换的逻辑;
在indev_keypad_proc函数中,短按或者长按LV_KEY_NEXT、LV_KEY_PREV键都会退出编辑态回到导航态;具体为调用了这个函数lv_group_set_editing(g, false); /*Editing is not used by KEYPAD is be sure it is disabled*/
在indev_encoder_proc函数中,就是短按LV_KEY_ENTER进入编辑态,长按LV_KEY_ENTER会切换状态,之前是编辑态就切换到导航态,之前是导航态就切换到编辑态。
LVGL 8.3 Tabview控件使用Keypad导航_lvgl tabview-优快云博客
拿控件举例:
下拉列表的基础事件如下:
lv_dropdown.c:
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 lv_res_t btn_release_handler(lv_obj_t * obj)
{
lv_dropdown_t * dropdown = (lv_dropdown_t *)obj;
lv_indev_t * indev = lv_indev_get_act();
if(lv_indev_get_scroll_obj(indev) == NULL) {
if(lv_dropdown_is_open(obj)) {
lv_dropdown_close(obj);
if(dropdown->sel_opt_id_orig != dropdown->sel_opt_id) {
dropdown->sel_opt_id_orig = dropdown->sel_opt_id;
lv_res_t res;
uint32_t id = dropdown->sel_opt_id; /*Just to use uint32_t in event data*/
res = lv_event_send(obj, LV_EVENT_VALUE_CHANGED, &id);
if(res != LV_RES_OK) return res;
lv_obj_invalidate(obj);
}
lv_indev_type_t indev_type = lv_indev_get_type(indev);
if(indev_type == LV_INDEV_TYPE_ENCODER) {
lv_group_set_editing(lv_obj_get_group(obj), false);
}
}
else {
lv_dropdown_open(obj);
}
}
else {
dropdown->sel_opt_id = dropdown->sel_opt_id_orig;
lv_obj_invalidate(obj);
}
return LV_RES_OK;
}
我们可以看出来,下拉列表基础事件中对LV_EVENT_RELEASED事件的响应是:使下拉列表本体展开/收起列表;对LV_EVENT_KEY事件的响应为:键值为LV_KEY_RIGHT或者LV_KEY_DOWN时,如果下拉列表本体未展开列表,则展开列表。如果已经展开列表,就切换选择的展开列表项;LV_KEY_LEFT或者LV_KEY_UP效果类似;键值为LV_KEY_ENTER时如果此时已经选中下拉列表的某个展开列表项,将该项值作为下拉列表本体显示值并收起列表;注意如果使用LV_KEY_PREV或者LV_KEY_NEXT的话,下拉列表会直接收起列表后不做任何操作,然后转移焦点到下一个控件;
而对于编码器模式,下拉列表会启用导航态和编辑态这个特性,处于导航态时当前focus的控件外面有一圈蓝色的细线做指示,而处于编辑态时当前focus的控件外面则是一圈橙色的细线;根据上面下拉列表的基础事件代码我们可以看到当处于编辑态是下拉列表会展开列表,处于导航态时下拉列表会收起列表;因此我们处于导航态时短按或者长按LV_KEY_ENTER会进入编辑态,下拉列表会展开列表,此时按下LV_KEY_LEFT或者LV_KEY_RIGHT就切换展开列表项进行选中,再按下LV_KEY_ENTER,将该项值作为下拉列表本体显示值并收起列表。处于编辑态时长按LV_KEY_ENTER进入导航态下拉列表会收起列表后不做任何操作,此时按下LV_KEY_LEFT或者LV_KEY_RIGHT会切换控件焦点;
编码器模式不一定必须要用编码器,三个按键也可以用编码器模式,不要被名字局限了。个人认为LVGL中交互按键三个就用编码器模式,编码器按键分别为LV_KEY_ENTER、LV_KEY_LEFT、LV_KEY_RIGHT
五个或以上可以用keypad模式,按键分别为LV_KEY_ENTER、LV_KEY_PREV、LV_KEY_NEXT、LV_KEY_UP、LV_KEY_DOWN
四个按键的话用编码器模式键值就是:LV_KEY_ENTER、LV_KEY_LEFT、LV_KEY_RIGHT、LV_KEY_ESC;用keypad模式的话键值就是LV_KEY_ENTER、LV_KEY_PREV/LV_KEY_NEXT、LV_KEY_UP、LV_KEY_DOWN
关于编码器驱动的编写,三按键式编码器和旋转编码器有所不同,三按键式编码器的驱动编写我参考了下列文章:
Littlevgl键盘和编码器驱动_littlevgl 键盘-优快云博客
STM32移植LVGL+旋转编码器接口对接_lvgl 编码器-优快云博客
他们的代码大概如下:
lv_port_indev.c:
static int32_t encoder_diff;
static lv_indev_state_t encoder_state;
void lv_port_indev_init(void)
{
lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_ENCODER;
indev_drv.read_cb = encoder_read;
indev_encoder= lv_indev_drv_register(&indev_drv);
g = lv_group_create();
lv_indev_set_group(indev_encoder, g);
}
static bool encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;
uint32_t act_key = KEY_Scan(0);
if(act_key != 0) {
switch(act_key) {
case 1:
act_key = LV_KEY_ENTER;
encoder_state = LV_INDEV_STATE_PR;
break;
case 2:
act_key = LV_KEY_LEFT;
encoder_diff = -1;
encoder_state = LV_INDEV_STATE_REL;
break;
case 3:
act_key = LV_KEY_RIGHT;
encoder_state = LV_INDEV_STATE_REL;
encoder_diff = 1;
break;
}
last_key = act_key;
}
else {
encoder_diff = 0;
encoder_state = LV_INDEV_STATE_REL;
}
data->key = last_key;
data->enc_diff = encoder_diff;
data->state = encoder_state;
/*Return `false` because we are not buffering and no more data to read*/
return false;
}
//按键处理函数
//返回按键值
//mode:0,不支持长按;1,支持长按;
//返回值:
//0,没有按键按下
//KEY_OK_PRES,KEY_OK 按下
//KEY_LEFT_PRES,KEY_LEFT 按下
//KEY_RIGHT_PRES,KEY_RIGHT 按下
u8 KEY_Scan(u8 mode)
{
static u8 key_up=1;//按键松开标志
if(mode)key_up=1; //支持长按
if(key_up&&(KEY_OK==0||KEY_LEFT==0||KEY_RIGHT==0))
{
delay_ms(20);//去抖动
key_up=0;
if(KEY_OK==0)return KEY_OK_PRES;
else if(KEY_LEFT==0)return KEY_LEFT_PRES;
else if(KEY_RIGHT==0)return KEY_RIGHT_LEFT_PRES;
}else if(KEY_OK==1&&KEY_LEFT==1&&KEY_RIGHT==1)key_up=1;
return 0;// 没有按键按下
}
不过我感觉使用时还是不太对,存在短按LV_KEY_LEFT、LV_KEY_RIGHT键,焦点不停改变的情况,理想状态应该是短按一次LV_KEY_LEFT或LV_KEY_RIGHT键改变一次焦点,于是我仔细看了一下LVGL的编码器输入设备处理函数indev_encoder_proc:
lv_indev.c:
/**
* Process a new point from LV_INDEV_TYPE_ENCODER input device
* @param i pointer to an input device
* @param data pointer to the data read from the input device
*/
static void indev_encoder_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*/
}
/*Save the last keys before anything else.
*They need to be already saved if the function returns for any reason*/
lv_indev_state_t last_state = i->proc.types.keypad.last_state;
i->proc.types.keypad.last_state = data->state;
i->proc.types.keypad.last_key = data->key;
lv_group_t * g = i->group;
if(g == NULL) return;
indev_obj_act = lv_group_get_focused(g);
if(indev_obj_act == NULL) return;
/*Process the steps they are valid only with released button*/
if(data->state != LV_INDEV_STATE_RELEASED) {
data->enc_diff = 0;
}
/*Refresh the focused object. It might change due to lv_group_focus_prev/next*/
indev_obj_act = lv_group_get_focused(g);
if(indev_obj_act == NULL) return;
/*Button press happened*/
if(data->state == LV_INDEV_STATE_PRESSED && last_state == LV_INDEV_STATE_RELEASED) {
LV_LOG_INFO("pressed");
i->proc.pr_timestamp = lv_tick_get();
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act, LV_OBJ_FLAG_SCROLLABLE);
if(lv_group_get_editing(g) == true || editable_or_scrollable == false) {
lv_event_send(indev_obj_act, LV_EVENT_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->key == LV_KEY_LEFT) {
/*emulate encoder left*/
data->enc_diff--;
}
else if(data->key == LV_KEY_RIGHT) {
/*emulate encoder right*/
data->enc_diff++;
}
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(data->state == LV_INDEV_STATE_PRESSED && last_state == LV_INDEV_STATE_PRESSED) {
/*Long press*/
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;
i->proc.longpr_rep_timestamp = lv_tick_get();
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act, LV_OBJ_FLAG_SCROLLABLE);
/*On enter long press toggle edit mode.*/
if(editable_or_scrollable) {
/*Don't leave edit mode if there is only one object (nowhere to navigate)*/
if(lv_group_get_obj_count(g) > 1) {
LV_LOG_INFO("toggling edit mode");
lv_group_set_editing(g, lv_group_get_editing(g) ? false : true); /*Toggle edit mode on long press*/
lv_obj_clear_state(indev_obj_act, LV_STATE_PRESSED); /*Remove the pressed state manually*/
}
}
/*If not editable then just send a long press Call the ancestor's event handler*/
else {
lv_event_send(indev_obj_act, LV_EVENT_LONG_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
}
i->proc.long_pr_sent = 1;
}
/*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();
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;
}
else if(data->key == LV_KEY_LEFT) {
/*emulate encoder left*/
data->enc_diff--;
}
else if(data->key == LV_KEY_RIGHT) {
/*emulate encoder right*/
data->enc_diff++;
}
else {
lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
}
}
/*Release happened*/
else if(data->state == LV_INDEV_STATE_RELEASED && last_state == LV_INDEV_STATE_PRESSED) {
LV_LOG_INFO("released");
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act, LV_OBJ_FLAG_SCROLLABLE);
/*The button was released on a non-editable object. Just send enter*/
if(editable_or_scrollable == false) {
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;
}
/*An object is being edited and the button is released.*/
else if(lv_group_get_editing(g)) {
/*Ignore long pressed enter release because it comes from mode switch*/
if(!i->proc.long_pr_sent || lv_group_get_obj_count(g) <= 1) {
lv_event_send(indev_obj_act, LV_EVENT_RELEASED, indev_act);
if(indev_reset_check(&i->proc)) return;
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;
lv_group_send_data(g, LV_KEY_ENTER);
if(indev_reset_check(&i->proc)) return;
}
else {
lv_obj_clear_state(indev_obj_act, LV_STATE_PRESSED); /*Remove the pressed state manually*/
}
}
/*If the focused object is editable and now in navigate mode then on enter switch edit
mode*/
else if(!i->proc.long_pr_sent) {
LV_LOG_INFO("entering edit mode");
lv_group_set_editing(g, true); /*Set edit mode*/
}
}
i->proc.pr_timestamp = 0;
i->proc.long_pr_sent = 0;
}
indev_obj_act = NULL;
/*if encoder steps or simulated steps via left/right keys*/
if(data->enc_diff != 0) {
/*In edit mode send LEFT/RIGHT keys*/
if(lv_group_get_editing(g)) {
LV_LOG_INFO("rotated by %+d (edit)", data->enc_diff);
int32_t s;
if(data->enc_diff < 0) {
for(s = 0; s < -data->enc_diff; s++) {
lv_group_send_data(g, LV_KEY_LEFT);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->enc_diff > 0) {
for(s = 0; s < data->enc_diff; s++) {
lv_group_send_data(g, LV_KEY_RIGHT);
if(indev_reset_check(&i->proc)) return;
}
}
}
/*In navigate mode focus on the next/prev objects*/
else {
LV_LOG_INFO("rotated by %+d (nav)", data->enc_diff);
int32_t s;
if(data->enc_diff < 0) {
for(s = 0; s < -data->enc_diff; s++) {
lv_group_focus_prev(g);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->enc_diff > 0) {
for(s = 0; s < data->enc_diff; s++) {
lv_group_focus_next(g);
if(indev_reset_check(&i->proc)) return;
}
}
}
}
}
我们对上述代码逐一进行分析,首先当按键状态不为LV_INDEV_STATE_RELEASED,即按键按下时,编码器的旋转量为0:
static void indev_encoder_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*Process the steps they are valid only with released button*/
if(data->state != LV_INDEV_STATE_RELEASED) {
data->enc_diff = 0;
}
......
}
随后就对短按进行判断,如果键值为LV_KEY_ENTER且当前控件为可编辑状态就发送LV_EVENT_PRESSED事件;如果键值为LV_KEY_LEFT,编码器旋转量就减一;如果键值为LV_KEY_RIGHT,编码器旋转量就加一;如果键值为LV_KEY_ESC就发送LV_EVENT_KEY事件,传递键值LV_KEY_ESC。此外还发送LV_EVENT_CANCEL事件;如果键值为其他就发送LV_EVENT_KEY事件,并传递对应键值;
从这里我们其实也能看到为什么Encoder模式三个按键时是用LV_KEY_ENTER、LV_KEY_LEFT、LV_KEY_RIGHT这三个键值了,这三个键值在编码器输入设备中做了特殊处理,保证通过三个按键能基于导航态编辑态达到切换焦点、选中控件和切换数值等操作。
static void indev_encoder_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act,
LV_OBJ_FLAG_SCROLLABLE);
if(lv_group_get_editing(g) == true || editable_or_scrollable == false) {
lv_event_send(indev_obj_act, LV_EVENT_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->key == LV_KEY_LEFT) {
/*emulate encoder left*/
data->enc_diff--;
}
else if(data->key == LV_KEY_RIGHT) {
/*emulate encoder right*/
data->enc_diff++;
}
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;
}
......
}
然后就是对长按的判断,如果键值为LV_KEY_ENTER且当前控件可编辑,就切换状态,之前是编辑态就切换到导航态,之前是导航态就切换到编辑态。如果当前控件不可编辑,就发送LV_EVENT_LONG_PRESSED事件;
如果当前已经为长按状态,开始进入长按重复触发判断,每过一次Long press repeated time,如果键值为LV_KEY_ENTER发送LV_EVENT_LONG_PRESSED_REPEAT事件;如果键值为LV_KEY_LEFT,编码器旋转量就减一;如果键值为LV_KEY_RIGHT,编码器旋转量就加一;如果键值为其他就发送LV_EVENT_KEY事件,并传递对应键值;
static void indev_encoder_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*Pressing*/
else if(data->state == LV_INDEV_STATE_PRESSED && last_state == LV_INDEV_STATE_PRESSED) {
/*Long press*/
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;
i->proc.longpr_rep_timestamp = lv_tick_get();
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act, LV_OBJ_FLAG_SCROLLABLE);
/*On enter long press toggle edit mode.*/
if(editable_or_scrollable) {
/*Don't leave edit mode if there is only one object (nowhere to navigate)*/
if(lv_group_get_obj_count(g) > 1) {
LV_LOG_INFO("toggling edit mode");
lv_group_set_editing(g, lv_group_get_editing(g) ? false : true); /*Toggle edit mode on long press*/
lv_obj_clear_state(indev_obj_act, LV_STATE_PRESSED); /*Remove the pressed state manually*/
}
}
/*If not editable then just send a long press Call the ancestor's event handler*/
else {
lv_event_send(indev_obj_act, LV_EVENT_LONG_PRESSED, indev_act);
if(indev_reset_check(&i->proc)) return;
}
}
i->proc.long_pr_sent = 1;
}
/*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();
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;
}
else if(data->key == LV_KEY_LEFT) {
/*emulate encoder left*/
data->enc_diff--;
}
else if(data->key == LV_KEY_RIGHT) {
/*emulate encoder right*/
data->enc_diff++;
}
else {
lv_group_send_data(g, data->key);
if(indev_reset_check(&i->proc)) return;
}
}
}
......
}
还有对按键释放的判断,如果键值为LV_KEY_ENTER且当前focus的控件不可编辑,则发送LV_EVENT_RELEASED事件和LV_EVENT_CLICKED事件,如果是短按后释放,还会发送LV_EVENT_SHORT_CLICKED事件;如果当前正处于编辑态,且为短按后释放或者当前group中的空间数量小于等于1,则发送LV_EVENT_RELEASED事件、LV_EVENT_CLICKED事件、LV_EVENT_SHORT_CLICKED事件和LV_EVENT_KEY事件,传递键值LV_KEY_ENTER。不然就是长按后释放,此时发送LV_STATE_PRESSED事件;如果当前控件可编辑但是处于导航态,按键短按释放后变为编辑态;
自此前面关于编码器导航态编辑态的切换方式:短按LV_KEY_ENTER进入编辑态,长按LV_KEY_ENTER会切换状态,之前是编辑态就切换到导航态,之前是导航态就切换到编辑态。也从LVGL源码中得到了验证。
static void indev_encoder_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*Release happened*/
else if(data->state == LV_INDEV_STATE_RELEASED && last_state == LV_INDEV_STATE_PRESSED) {
LV_LOG_INFO("released");
if(data->key == LV_KEY_ENTER) {
bool editable_or_scrollable = lv_obj_is_editable(indev_obj_act) ||
lv_obj_has_flag(indev_obj_act, LV_OBJ_FLAG_SCROLLABLE);
/*The button was released on a non-editable object. Just send enter*/
if(editable_or_scrollable == false) {
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;
}
/*An object is being edited and the button is released.*/
else if(lv_group_get_editing(g)) {
/*Ignore long pressed enter release because it comes from mode switch*/
if(!i->proc.long_pr_sent || lv_group_get_obj_count(g) <= 1) {
lv_event_send(indev_obj_act, LV_EVENT_RELEASED, indev_act);
if(indev_reset_check(&i->proc)) return;
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;
lv_group_send_data(g, LV_KEY_ENTER);
if(indev_reset_check(&i->proc)) return;
}
else {
lv_obj_clear_state(indev_obj_act, LV_STATE_PRESSED); /*Remove the pressed state manually*/
}
}
/*If the focused object is editable and now in navigate mode then on enter switch edit
mode*/
else if(!i->proc.long_pr_sent) {
LV_LOG_INFO("entering edit mode");
lv_group_set_editing(g, true); /*Set edit mode*/
}
}
i->proc.pr_timestamp = 0;
i->proc.long_pr_sent = 0;
}
indev_obj_act = NULL;
......
}
在 indev_encoder_proc函数最后还有一个判断,这个判断就决定了编码器模式下,处于导航态时,按下LV_KEY_LEFT或者LV_KEY_RIGHT就切换焦点,处于编辑态时按下LV_KEY_LEFT或者LV_KEY_RIGHT就发送LV_EVENT_KEY事件,传递键值LV_KEY_LEFT或者LV_KEY_RIGHT,从而可以改变控件数值。
static void indev_encoder_proc(lv_indev_t * i, lv_indev_data_t * data)
{
......
/*if encoder steps or simulated steps via left/right keys*/
if(data->enc_diff != 0) {
/*In edit mode send LEFT/RIGHT keys*/
if(lv_group_get_editing(g)) {
LV_LOG_INFO("rotated by %+d (edit)", data->enc_diff);
int32_t s;
if(data->enc_diff < 0) {
for(s = 0; s < -data->enc_diff; s++) {
lv_group_send_data(g, LV_KEY_LEFT);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->enc_diff > 0) {
for(s = 0; s < data->enc_diff; s++) {
lv_group_send_data(g, LV_KEY_RIGHT);
if(indev_reset_check(&i->proc)) return;
}
}
}
/*In navigate mode focus on the next/prev objects*/
else {
LV_LOG_INFO("rotated by %+d (nav)", data->enc_diff);
int32_t s;
if(data->enc_diff < 0) {
for(s = 0; s < -data->enc_diff; s++) {
lv_group_focus_prev(g);
if(indev_reset_check(&i->proc)) return;
}
}
else if(data->enc_diff > 0) {
for(s = 0; s < data->enc_diff; s++) {
lv_group_focus_next(g);
if(indev_reset_check(&i->proc)) return;
}
}
}
}
......
}
在看了indev_encoder_proc函数的处理逻辑后,我在之前的编码器模式驱动的基础上做了一些改动,实测解决了短按一次LV_KEY_LEFT或LV_KEY_RIGHT键改变一次焦点,长按则每过一次Long press repeated time切换一次焦点。新编码器模式驱动代码如下:
lv_port_indev.c:
#define encoder_num 3 //按钮数量,按钮id依次为0、1、2...
#define ENCODER_DEBOUNCE_COUNT 3 //编码器消抖次数
GPIO_TypeDef *encoder_ports[] = {KEY_BACK_GPIO_Port, KEY_DOWN_GPIO_Port, KEY_UP_GPIO_Port};
uint16_t encoder_pins[] = {KEY_BACK_Pin, KEY_DOWN_Pin, KEY_UP_Pin};
static uint8_t debounce_counter[encoder_num] = {0}; // 连续按下计数,每一个按钮都有一个独立计数值
static bool debounced_state[encoder_num] = {false}; // 当前稳定状态,每一个按钮都有一个
static void encoder_init(void);
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static void encoder_handler(void);
static uint32_t encoder_get_key(void);
static bool encoder_is_pressed(uint8_t id);
lv_indev_t * indev_encoder;
void lv_port_indev_init(void)
{
static lv_indev_drv_t indev_Encoder_drv;
/*------------------
* Encoder
* -----------------*/
/*Initialize your encoder if you have*/
encoder_init();
/*Register a encoder input device*/
lv_indev_drv_init(&indev_Encoder_drv);
indev_Encoder_drv.type = LV_INDEV_TYPE_ENCODER;
indev_Encoder_drv.read_cb = encoder_read;
indev_Encoder_drv.long_press_time = 1000; //自定义
indev_Encoder_drv.long_press_repeat_time = 300;
indev_encoder = lv_indev_drv_register(&indev_Encoder_drv);
}
/*------------------
* Encoder
* -----------------*/
/*Initialize your keypad*/
static void encoder_init(void)
{
/*Your code comes here*/
}
//int32_t encoder_diff;
//lv_indev_state_t encoder_state;
/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;
uint32_t act_key = encoder_get_key();
if(act_key != 0) {
data->state= LV_INDEV_STATE_PR;
switch(act_key) {
case 1:
act_key = LV_KEY_ENTER;
break;
case 2:
act_key = LV_KEY_LEFT;
break;
case 3:
act_key = LV_KEY_RIGHT;
break;
}
last_key = act_key;
}
else {
data->state = LV_INDEV_STATE_REL;
}
data->key = last_key;
}
/*Call this function in an interrupt to process encoder events (turn, press)*/
//使用中断方式检测编码器旋转和按下时,在中断中调用下列函数
static void encoder_handler(void)
{
/*Your code comes here*/
//encoder_diff += get_encoder_rotation(); //是一个用户自定义函数,返回编码器自上次调用后
的旋转格数。
//encoder_state = get_encoder_State(); //用户自定义函数,用于检查按键的状态。
}
/*Get the currently being pressed key. 0 if no key is pressed*/
static uint32_t encoder_get_key(void)
{
for (uint8_t i = 0; i < encoder_num ; i++) {
bool raw_pressed = encoder_is_pressed(i);
// 检测连续按下次数是否达到阈值
if (raw_pressed) {
if (debounce_counter[i] < ENCODER_DEBOUNCE_COUNT) {
debounce_counter[i]++;
}
} else {
if (debounce_counter[i] > 0) {
debounce_counter[i]--;
}
}
// 状态更新
if (debounce_counter[i] >= ENCODER_DEBOUNCE_COUNT) {
debounced_state[i] = true;
} else if (debounce_counter[i] == 0) {
debounced_state[i] = false;
}
// 如果去抖确认按下
if (debounced_state[i]) {
return i + 1;
}
}
return 0; // 没有任何按键稳定按下
}
static bool encoder_is_pressed(uint8_t id)
{
if(id >= encoder_num) {
return false; // 检查id是否在有效范围
}
return HAL_GPIO_ReadPin(encoder_ports[id], encoder_pins[id]) == GPIO_PIN_RESET;
}
至此我们结合下拉列表控件了解了导航态和编辑态这个概念在LVGL中的应用,这个概念主要还是用在编码器输入设备上,从而实现了三个按键实现丰富的UI交互效果,keypad输入设备关于导航态和编辑态的使用我没有看到太多相关代码;此外,在实际使用中我们还需要根据不同控件的基础事件去了解该控件的特性和对于不同输入设备的支持从而更好的去使用LVGL。