最近在做的项目用到了LVGL,基于实际产品使用的特殊情况,屏幕没有接触摸屏,而是使用物理按键来控制所有的object,而且硬件上只有四个按键,功能分别是:返回、左/上、右/下和确定,在切换界面的过程中,也引出了一个焦点切换问题。
既然项目中我们是使用按键来做界面切换的,也就是说我们是没有触摸屏的,所以按键还需要充当切换焦点的作用。但是对于前面的隐藏当前界面再创建新界面来说,隐藏界面并不会删除隐藏界面的焦点,而如果我们删掉之前界面的所有焦点肯定也不行。在这种情况下,在切换焦点时还会切换到隐藏界面中一些对象的焦点,即使你看不到隐藏的对象,这肯定是不能接受的。
解决方案:
1、通过lv_group_get_focused()
获得当前界面的焦点并记录,然后切换界面时就采用删除界面的方式,再后续返回时,通过之前记录的焦点使用函数lv_group_focus_obj
进行focus。
2、隐藏当前界面,但是要记录当前聚焦的对象,然后删除当前界面的所有可聚焦的对象。这样在新界面中按键切换时就不会切换到隐藏的界面中。最后在返回窗口时恢复所有可聚焦的对象和当前聚焦的对象即可。
对于第一种方法来说,如果要判断是哪个对象,需要遍历界面中的每一个对象,这就比较困难了,因为一个界面的所有对象的指针并不在一个数组中,而是可能定义在当前界面的一个结构体中,当然理论上我们也可以遍历结构体,因为一个任意类型指针的大小都是固定的,一般为4字节。接着在获取了这个对象后,由于后续还要删除掉界面,所以不能记录焦点对象的指针,而是需要给一个界面的每个焦点进行编号,所以第一种方法的实现是比较复杂的。
另外,无论对于第一种还是第二种方案来说,还有一个问题需要解决,就是界面中聚焦的焦点可能还会在某个对象的子对象中。经过以上分析,现在我们就来实现上面说的第二种保存对象焦点的方案。
LVGL中的焦点切换是通过组来实现的,切换焦点时,就是按照组中焦点的顺序进行切换的。所以如果要切换焦点,首先要通过函数lv_group_add_obj
把对象加入组中:
void lv_group_add_obj(lv_group_t * group, lv_obj_t * obj)
有的对象在创建的时候是自动加入组的,有的则需要手动加入。以lv_btn.c
和lv_imgbtn.c
为例,它们的lv_obj_class_t
的定义如下
const lv_obj_class_t lv_btn_class = {
.constructor_cb = lv_btn_constructor,
.width_def = LV_SIZE_CONTENT,
.height_def = LV_SIZE_CONTENT,
.group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE,
.instance_size = sizeof(lv_btn_t),
.base_class = &lv_obj_class
};
const lv_obj_class_t lv_imgbtn_class = {
.base_class = &lv_obj_class,
.instance_size = sizeof(lv_imgbtn_t),
.constructor_cb = lv_imgbtn_constructor,
.event_cb = lv_imgbtn_event,
};
可以发现对于lv_btn_t
来说,它的group_def
定义为LV_OBJ_CLASS_GROUP_DEF_TRUE
,通过查看代码可以发现,在对象创建的过程中有判断group_def
字段:
bool lv_obj_is_group_def(lv_obj_t * obj)
{
const lv_obj_class_t * class_p = obj->class_p;
/*Find a base in which group_def is set*/
while(class_p && class_p->group_def == LV_OBJ_CLASS_GROUP_DEF_INHERIT) class_p = class_p->base_class;
if(class_p == NULL) return false;
return class_p->group_def == LV_OBJ_CLASS_GROUP_DEF_TRUE ? true : false;
}
void lv_obj_class_init_obj(lv_obj_t * obj)
{
...
lv_group_t * def_group = lv_group_get_default();
if(def_group && lv_obj_is_group_def(obj)) {
lv_group_add_obj(def_group, obj);
}
...
}
也就是说group_def = LV_OBJ_CLASS_GROUP_DEF_TRUE
的对象创建时将自动加入当前的默认组中。
首先我们需要知道在LVGL中是使用什么结构将各个对象保存到group中的。以lv_group_add_obj
函数为例我们看看是怎么实现的(省略与我们需要的无关代码):
void lv_group_add_obj(lv_group_t * group, lv_obj_t * obj)
{
...
lv_obj_t ** next = _lv_ll_ins_tail(&group->obj_ll);
LV_ASSERT_MALLOC(next);
if(next == NULL) return;
*next = obj;
...
可以看到,在每个组中有一个obj_ll
来保存组中的对象,它的数据类型是lv_ll_t
,不难看出它是一个链表。如果要详细分析的话,篇幅有些过长,所以单独写了一篇博客,参考:LVGL源码分析(1):lv_ll链表的实现
假设我们所有页面使用一个group,且它为默认组,首先在每一个界面的结构体中我们都声明一个lv_ll_t
并初始化,用于记录焦点。
lv_ll_t focus_ll;
_lv_ll_init(&page->focus_ll, sizeof(lv_obj_t *));
在切换窗口时,我们就可以通过下面的函数获取当前的焦点对应的对象然后插入到链表的最后。最后要从group中删除当前窗口的所有焦点。
lv_obj_t *tmp_focus = lv_group_get_focused(lv_group_get_default());
lv_obj_t ** next = _lv_ll_ins_tail(&page->focus_ll);
LV_ASSERT_MALLOC(next);
*next = tmp_focus;
lv_group_remove_all_objs_self(lv_group_get_default(), &page->focus_ll);
这里我小小地修改了lv_group_remove_all_objs
函数为lv_group_remove_all_objs_self
,就是将要移除的焦点先调用_lv_ll_ins_tail
加入focus_ll
再删除。然后链表的第一个元素为当前焦点所在的对象。
我们需要在切换回界面时将刚刚保存的焦点恢复:
/* 恢复之前记录的焦点 */
lv_obj_t ** obj;
_LV_LL_READ(&page->focus_ll, obj) {
lv_group_add_obj(lv_group_get_default(), *obj);
}
obj = _lv_ll_get_head(&page->focus_ll);
lv_group_focus_obj(*obj);
所以上面的代码首先恢复所有的焦点,最后再取出链表中的第一个元素聚焦即可。前面我们把当前焦点所在的对象先加入到了链表中的第一个,实际上是通过阅读lv_group_add_obj
源码可以知道,加入一个已经存在的对象,会将之前的删除掉然后再加入到链表的最后面。如果源码中没有替换的话,就是加入焦点所在对象到链表最后一个。
本文实现了页面中焦点的保存和恢复,了解了LVGL中对于对象及焦点的保存。而在我写博客的过程中,又想到一个方案:在每一个界面创建时都创建一个group,并设置为默认组,然后在删除界面时删除这个group。最后我们需要在每次创建或切换界面的同时,调用lv_indev_set_group
来修改输入设备所对应的group。这种方式似乎比我上面实现的方式要更简洁高效。所以啊,办法都是想出来的,遇到了问题还是可以全方位地思考一下所有可能的解决方式。