1
0
mirror of https://github.com/lvgl/lvgl.git synced 2025-01-14 06:42:58 +08:00

feat(gridnav): add lv_gridnav (#2911)

* add first implememtation

* Update src/extra/others/gridnav/lv_gridnav.c

Co-authored-by: embeddedt <42941056+embeddedt@users.noreply.github.com>

* minor fix

* add example and minor fixes

* add more examples

* add more examples

* code formatting

* add LV_GRIDNAC_CTRL_SCROLL_FIRST

* code formatting

* add example for list

* add docs

* Misc:  improvements to gridnav docs (#2994)

Co-authored-by: embeddedt <42941056+embeddedt@users.noreply.github.com>
Co-authored-by: Ken Carpenter <62639971+FoundationKen@users.noreply.github.com>
This commit is contained in:
Gabor Kiss-Vamosi 2022-01-20 10:28:49 +01:00
parent 731ef5a75e
commit 62fc7123f1
18 changed files with 948 additions and 53 deletions

60
docs/others/gridnav.md Normal file
View File

@ -0,0 +1,60 @@
```eval_rst
.. include:: /header.rst
:github_url: |github_link_base|/others/gridnav.md
```
# Grid navigation
Grid navigation (gridnav for short) is a feature that changes the currently focused child object as arrow keys are pressed.
If the children are arranged into a grid-like layout then the up, down, left and right arrows move focus to the nearest sibling
in the respective direction.
It doesn't matter how the children are positioned, as only the current x and y coordinates are considered.
This means that gridnav works with manually positioned children, as well as [Flex](/layouts/flex.html) and [Grid](/layouts/grid.html) layouts.
Gridnav also works if the children are arranged into a single row or column.
That makes it useful, for example, to simplify navigation on a [List widget](/widgets/extra/list.html).
Gridnav assumes that the object to which gridnav is added is part of a [group](/overview/indev.html#groups).
This way, if the object with gridnav is focused, the arrow key presses are automatically forwarded to the object
so that gridnav can process the arrow keys.
To move the focus to the next widget of the group use `LV_KEY_NEXT/PREV` or `lv_group_focus_next/prev()` or the `TAB` key on keyboard as usual.
If the container is scrollable and the focused child is out of the view, gridnav will automatically scroll the child into view.
## Usage
To add the gridnav feature to an object use `lv_gridnav_add(cont, flags)`.
`flags` control the behavior of gridnav:
- `LV_GRIDNAV_CTRL_NONE` Default settings
- `LV_GRIDNAV_CTRL_ROLLOVER` If there is no next/previous object in a direction,
the focus goes to the object in the next/previous row (on left/right keys) or first/last row (on up/down keys
- `LV_GRIDNAV_CTRL_SCROLL_FIRST` If an arrow is pressed and the focused object can be scrolled in that direction
then it will be scrolled instead of going to the next/previous object. If there is no more room for scrolling the next/previous object will be focused normally
`lv_gridnav_remove(cont)` Removes gridnav from an object.
## Focusable objects
An object needs to be clickable or click focusable (`LV_OBJ_FLAG_CLICKABLE` or `LV_OBJ_FLAG_CLICK_FOCUSABLE`)
and not hidden (`LV_OBJ_FLAG_HIDDEN`) to be focusable by gridnav.
## Example
```eval_rst
.. include:: ../../examples/others/gridnav/index.rst
```
## API
```eval_rst
.. doxygenfile:: lv_gridnav.h
:project: lvgl
```

View File

@ -12,5 +12,6 @@
snapshot
monkey
gridnav
```

View File

@ -21,6 +21,7 @@ void lv_example_flex_2(void)
for(i = 0; i < 8; i++) {
lv_obj_t * obj = lv_obj_create(cont);
lv_obj_set_size(obj, 70, LV_SIZE_CONTENT);
lv_obj_add_flag(obj, LV_OBJ_FLAG_CHECKABLE);
lv_obj_t * label = lv_label_create(obj);
lv_label_set_text_fmt(label, "%"LV_PRIu32, i);

View File

@ -0,0 +1,18 @@
Basic grid navigation
"""""""""""""""""""""
.. lv_example:: others/monkey/lv_example_gridnav_1
:language: c
Grid navigation on a list
""""""""""""""""""""""""
.. lv_example:: others/monkey/lv_example_gridnav_2
:language: c
Nested grid navigations
"""""""""""""""""""""""
.. lv_example:: others/monkey/lv_example_gridanav_3
:language: c

View File

@ -0,0 +1,40 @@
/**
* @file lv_example_gridnav.h
*
*/
#ifndef LV_EXAMPLE_GRIDNAV_H
#define LV_EXAMPLE_GRIDNAV_H
#ifdef __cplusplus
extern "C" {
#endif
/*********************
* INCLUDES
*********************/
/*********************
* DEFINES
*********************/
/**********************
* TYPEDEFS
**********************/
/**********************
* GLOBAL PROTOTYPES
**********************/
void lv_example_gridnav_1(void);
void lv_example_gridnav_2(void);
void lv_example_gridnav_3(void);
/**********************
* MACROS
**********************/
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /*LV_EXAMPLE_GRIDNAV_H*/

View File

@ -0,0 +1,72 @@
#include "../../lv_examples.h"
#if LV_USE_GRIDNAV && LV_USE_FLEX && LV_BUILD_EXAMPLES
/**
* Demonstrate a a basic grid navigation
*/
void lv_example_gridnav_1(void)
{
/*It's assumed that the default group is set and
*there is a keyboard indev*/
lv_obj_t * cont1 = lv_obj_create(lv_scr_act());
lv_gridnav_add(cont1, LV_GRIDNAV_CTRL_NONE);
/*Use flex here, but works with grid or manually placed objects as well*/
lv_obj_set_flex_flow(cont1, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_bg_color(cont1, lv_palette_lighten(LV_PALETTE_BLUE, 5), LV_STATE_FOCUSED);
lv_obj_set_size(cont1, lv_pct(50), lv_pct(100));
/*Only the container needs to be in a group*/
lv_group_add_obj(lv_group_get_default(), cont1);
lv_obj_t * label = lv_label_create(cont1);
lv_label_set_text_fmt(label, "No rollover");
uint32_t i;
for(i = 0; i < 10; i++) {
lv_obj_t * obj = lv_btn_create(cont1);
lv_obj_set_size(obj, 70, LV_SIZE_CONTENT);
lv_obj_add_flag(obj, LV_OBJ_FLAG_CHECKABLE);
lv_group_remove_obj(obj); /*Not needed, we use the gridnav instead*/
lv_obj_t * label = lv_label_create(obj);
lv_label_set_text_fmt(label, "%d", i);
lv_obj_center(label);
}
/* Create a second container with rollover grid nav mode.*/
lv_obj_t * cont2 = lv_obj_create(lv_scr_act());
lv_gridnav_add(cont2, LV_GRIDNAV_CTRL_ROLLOVER);
lv_obj_set_style_bg_color(cont2, lv_palette_lighten(LV_PALETTE_BLUE, 5), LV_STATE_FOCUSED);
lv_obj_set_size(cont2, lv_pct(50), lv_pct(100));
lv_obj_align(cont2, LV_ALIGN_RIGHT_MID, 0, 0);
label = lv_label_create(cont2);
lv_obj_set_width(label, lv_pct(100));
lv_label_set_text_fmt(label, "Rollover\nUse tab to focus the other container");
/*Only the container needs to be in a group*/
lv_group_add_obj(lv_group_get_default(), cont2);
/*Add and place some children manually*/
lv_obj_t * ta = lv_textarea_create(cont2);
lv_obj_set_size(ta, lv_pct(100), 80);
lv_obj_set_pos(ta, 0, 80);
lv_group_remove_obj(ta); /*Not needed, we use the gridnav instead*/
lv_obj_t * cb = lv_checkbox_create(cont2);
lv_obj_set_pos(cb, 0, 170);
lv_group_remove_obj(cb); /*Not needed, we use the gridnav instead*/
lv_obj_t * sw1 = lv_switch_create(cont2);
lv_obj_set_pos(sw1, 0, 200);
lv_group_remove_obj(sw1); /*Not needed, we use the gridnav instead*/
lv_obj_t * sw2 = lv_switch_create(cont2);
lv_obj_set_pos(sw2, lv_pct(50), 200);
lv_group_remove_obj(sw2); /*Not needed, we use the gridnav instead*/
}
#endif

View File

@ -0,0 +1,44 @@
#include "../../lv_examples.h"
#if LV_USE_GRIDNAV && LV_USE_LIST && LV_BUILD_EXAMPLES
/**
* Grid navigation on a list
*/
void lv_example_gridnav_2(void)
{
/*It's assumed that the default group is set and
*there is a keyboard indev*/
lv_obj_t * list1 = lv_list_create(lv_scr_act());
lv_gridnav_add(list1, LV_GRIDNAV_CTRL_NONE);
lv_obj_set_size(list1, lv_pct(45), lv_pct(80));
lv_obj_align(list1, LV_ALIGN_LEFT_MID, 5, 0);
lv_obj_set_style_bg_color(list1, lv_palette_lighten(LV_PALETTE_BLUE, 5), LV_STATE_FOCUSED);
lv_group_add_obj(lv_group_get_default(), list1);
char buf[32];
uint32_t i;
for(i = 0; i < 15; i++) {
lv_snprintf(buf, sizeof(buf), "File %d", i + 1);
lv_obj_t * item = lv_list_add_btn(list1, LV_SYMBOL_FILE, buf);
lv_obj_set_style_bg_opa(item, 0, 0);
lv_group_remove_obj(item); /*Not needed, we use the gridnav instead*/
}
lv_obj_t * list2 = lv_list_create(lv_scr_act());
lv_gridnav_add(list2, LV_GRIDNAV_CTRL_ROLLOVER);
lv_obj_set_size(list2, lv_pct(45), lv_pct(80));
lv_obj_align(list2, LV_ALIGN_RIGHT_MID, -5, 0);
lv_obj_set_style_bg_color(list2, lv_palette_lighten(LV_PALETTE_BLUE, 5), LV_STATE_FOCUSED);
lv_group_add_obj(lv_group_get_default(), list2);
for(i = 0; i < 15; i++) {
lv_snprintf(buf, sizeof(buf), "Folder %d", i + 1);
lv_obj_t * item = lv_list_add_btn(list2, LV_SYMBOL_DIRECTORY, buf);
lv_obj_set_style_bg_opa(item, 0, 0);
lv_group_remove_obj(item);
}
}
#endif

View File

@ -0,0 +1,101 @@
#include "../../lv_examples.h"
#if LV_USE_GRIDNAV && LV_USE_FLEX && LV_BUILD_EXAMPLES
static void cont_sub_event_cb(lv_event_t * e)
{
uint32_t k = lv_event_get_key(e);
lv_obj_t * obj = lv_event_get_current_target(e);
if(k == LV_KEY_ENTER) {
lv_group_focus_obj(obj);
}
else if(k == LV_KEY_ESC) {
lv_group_focus_next(lv_obj_get_group(obj));
}
}
/**
* Nested grid navigations
*/
void lv_example_gridnav_3(void)
{
/*It's assumed that the default group is set and
*there is a keyboard indev*/
lv_obj_t * cont_main = lv_obj_create(lv_scr_act());
lv_gridnav_add(cont_main, LV_GRIDNAV_CTRL_ROLLOVER | LV_GRIDNAV_CTRL_SCROLL_FIRST);
/*Only the container needs to be in a group*/
lv_group_add_obj(lv_group_get_default(), cont_main);
/*Use flex here, but works with grid or manually placed objects as well*/
lv_obj_set_flex_flow(cont_main, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_bg_color(cont_main, lv_palette_lighten(LV_PALETTE_BLUE, 5), LV_STATE_FOCUSED);
lv_obj_set_size(cont_main, lv_pct(80), LV_SIZE_CONTENT);
lv_obj_t * btn;
lv_obj_t * label;
btn = lv_btn_create(cont_main);
lv_group_remove_obj(btn);
label = lv_label_create(btn);
lv_label_set_text(label, "Button 1");
btn = lv_btn_create(cont_main);
lv_group_remove_obj(btn);
label = lv_label_create(btn);
lv_label_set_text(label, "Button 2");
/*Create an other container with long text to show how LV_GRIDNAV_CTRL_SCROLL_FIRST works*/
lv_obj_t * cont_sub1 = lv_obj_create(cont_main);
lv_obj_set_size(cont_sub1, lv_pct(100), 100);
label = lv_label_create(cont_sub1);
lv_obj_set_style_bg_color(cont_sub1, lv_palette_lighten(LV_PALETTE_RED, 5), LV_STATE_FOCUSED);
lv_obj_set_width(label, lv_pct(100));
lv_label_set_text(label,
"I'm a very long text which is makes my container scrollable. "
"As LV_GRIDNAV_FLAG_SCROLL_FIRST is enabled arrow will scroll me first "
"and a new objects will be focused only when an edge is reached with the scrolling.\n\n"
"This is only some placeholder text to be sure the parent will be scrollable. \n\n"
"Hello world!\n"
"Hello world!\n"
"Hello world!\n"
"Hello world!\n"
"Hello world!\n"
"Hello world!");
/*Create a third container that can be focused with ENTER and contains an other grid nav*/
lv_obj_t * cont_sub2 = lv_obj_create(cont_main);
lv_gridnav_add(cont_sub2, LV_GRIDNAV_CTRL_ROLLOVER);
/*Only the container needs to be in a group*/
lv_group_add_obj(lv_group_get_default(), cont_sub2);
lv_obj_add_event_cb(cont_sub2, cont_sub_event_cb, LV_EVENT_KEY, NULL);
/*Use flex here, but works with grid or manually placed objects as well*/
lv_obj_set_flex_flow(cont_sub2, LV_FLEX_FLOW_ROW_WRAP);
lv_obj_set_style_bg_color(cont_sub2, lv_palette_lighten(LV_PALETTE_RED, 5), LV_STATE_FOCUSED);
lv_obj_set_size(cont_sub2, lv_pct(100), LV_SIZE_CONTENT);
label = lv_label_create(cont_sub2);
lv_label_set_text(label, "Use ENTER/ESC to focus/defocus this container");
lv_obj_set_width(label, lv_pct(100));
btn = lv_btn_create(cont_sub2);
lv_group_remove_obj(btn);
label = lv_label_create(btn);
lv_label_set_text(label, "Button 3");
btn = lv_btn_create(cont_sub2);
lv_group_remove_obj(btn);
label = lv_label_create(btn);
lv_label_set_text(label, "Button 4");
}
#endif

View File

@ -0,0 +1,47 @@
#include "../../lv_examples.h"
#if LV_USE_GRIDNAV && LV_USE_FLEX && LV_BUILD_EXAMPLES
static void event_handler(lv_event_t * e)
{
lv_obj_t * obj = lv_event_get_target(e);
lv_obj_t * list = lv_obj_get_parent(obj);
LV_LOG_USER("Clicked: %s", lv_list_get_btn_text(list, obj));
}
/**
* Simple navigation on a list widget
*/
void lv_example_gridnav_4(void)
{
/*It's assumed that the default group is set and
*there is a keyboard indev*/
lv_obj_t * list = lv_list_create(lv_scr_act());
lv_gridnav_add(list, LV_GRIDNAV_CTRL_ROLLOVER);
lv_obj_align(list, LV_ALIGN_LEFT_MID, 0, 10);
lv_group_add_obj(lv_group_get_default(), list);
uint32_t i;
for(i = 0; i < 20; i++) {
char buf[32];
/*Add some separators too, they are not focusable by gridnav*/
if((i % 5) == 0) {
lv_snprintf(buf, sizeof(buf), "Section %d", i / 5 + 1);
lv_list_add_text(list, buf);
}
lv_snprintf(buf, sizeof(buf), "File %d", i + 1);
lv_obj_t * item = lv_list_add_btn(list, LV_SYMBOL_FILE, buf);
lv_obj_add_event_cb(item, event_handler, LV_EVENT_CLICKED, NULL);
lv_group_remove_obj(item); /*The default group adds it automatically*/
}
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_align(btn, LV_ALIGN_RIGHT_MID, 0, -10);
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "Button");
}
#endif

View File

@ -15,6 +15,7 @@ extern "C" {
*********************/
#include "snapshot/lv_example_snapshot.h"
#include "monkey/lv_example_monkey.h"
#include "gridnav/lv_example_gridnav.h"
/*********************
* DEFINES

View File

@ -10,6 +10,7 @@ static void event_handler(lv_event_t * e)
LV_LOG_USER("Clicked: %s", lv_list_get_btn_text(list1, obj));
}
}
void lv_example_list_1(void)
{
/*Create a list*/

View File

@ -621,11 +621,14 @@
*----------*/
/*1: Enable API to take snapshot for object*/
#define LV_USE_SNAPSHOT 1
#define LV_USE_SNAPSHOT 0
/*1: Enable Monkey test*/
#define LV_USE_MONKEY 0
/*1: Enable grid navigation*/
#define LV_USE_GRIDNAV 0
/*==================
* EXAMPLES
*==================*/

View File

@ -252,9 +252,58 @@ void lv_obj_get_scroll_end(struct _lv_obj_t * obj, lv_point_t * end)
* Other functions
*====================*/
void lv_obj_scroll_by(lv_obj_t * obj, lv_coord_t x, lv_coord_t y, lv_anim_enable_t anim_en)
void lv_obj_scroll_by_bounded(lv_obj_t * obj, lv_coord_t dx, lv_coord_t dy, lv_anim_enable_t anim_en)
{
if(x == 0 && y == 0) return;
if(dx == 0 && dy == 0) return;
/*We need to know the final sizes for bound check*/
lv_obj_update_layout(obj);
/*Don't let scroll more then naturally possible by the size of the content*/
lv_coord_t x_current = -lv_obj_get_scroll_x(obj);
lv_coord_t x_bounded = x_current + dx;
if(lv_obj_get_style_base_dir(obj, LV_PART_MAIN) != LV_BASE_DIR_RTL) {
if(x_bounded > 0) x_bounded = 0;
if(x_bounded < 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_left(obj) + lv_obj_get_scroll_right(obj);
if(scroll_max < 0) scroll_max = 0;
if(x_bounded < -scroll_max) x_bounded = -scroll_max;
}
}
else {
if(x_bounded < 0) x_bounded = 0;
if(x_bounded > 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_left(obj) + lv_obj_get_scroll_right(obj);
if(scroll_max < 0) scroll_max = 0;
if(x_bounded > scroll_max) x_bounded = scroll_max;
}
}
/*Don't let scroll more then naturally possible by the size of the content*/
lv_coord_t y_current = -lv_obj_get_scroll_y(obj);
lv_coord_t y_bounded = y_current + dy;
if(y_bounded > 0) y_bounded = 0;
if(y_bounded < 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
if(scroll_max < 0) scroll_max = 0;
if(y_bounded < -scroll_max) y_bounded = -scroll_max;
}
dx = x_bounded - x_current;
dy = y_bounded - y_current;
if(dx || dy) {
lv_obj_scroll_by(obj, dx, dy, anim_en);
}
}
void lv_obj_scroll_by(lv_obj_t * obj, lv_coord_t dx, lv_coord_t dy, lv_anim_enable_t anim_en)
{
if(dx == 0 && dy == 0) return;
if(anim_en == LV_ANIM_ON) {
lv_disp_t * d = lv_obj_get_disp(obj);
lv_anim_t a;
@ -262,13 +311,13 @@ void lv_obj_scroll_by(lv_obj_t * obj, lv_coord_t x, lv_coord_t y, lv_anim_enable
lv_anim_set_var(&a, obj);
lv_anim_set_ready_cb(&a, scroll_anim_ready_cb);
if(x) {
uint32_t t = lv_anim_speed_to_time((lv_disp_get_hor_res(d) * 2) >> 2, 0, x);
if(dx) {
uint32_t t = lv_anim_speed_to_time((lv_disp_get_hor_res(d) * 2) >> 2, 0, dx);
if(t < SCROLL_ANIM_TIME_MIN) t = SCROLL_ANIM_TIME_MIN;
if(t > SCROLL_ANIM_TIME_MAX) t = SCROLL_ANIM_TIME_MAX;
lv_anim_set_time(&a, t);
lv_coord_t sx = lv_obj_get_scroll_x(obj);
lv_anim_set_values(&a, -sx, -sx + x);
lv_anim_set_values(&a, -sx, -sx + dx);
lv_anim_set_exec_cb(&a, scroll_x_anim);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
@ -278,13 +327,13 @@ void lv_obj_scroll_by(lv_obj_t * obj, lv_coord_t x, lv_coord_t y, lv_anim_enable
lv_anim_start(&a);
}
if(y) {
uint32_t t = lv_anim_speed_to_time((lv_disp_get_ver_res(d) * 2) >> 2, 0, y);
if(dy) {
uint32_t t = lv_anim_speed_to_time((lv_disp_get_ver_res(d) * 2) >> 2, 0, dy);
if(t < SCROLL_ANIM_TIME_MIN) t = SCROLL_ANIM_TIME_MIN;
if(t > SCROLL_ANIM_TIME_MAX) t = SCROLL_ANIM_TIME_MAX;
lv_anim_set_time(&a, t);
lv_coord_t sy = lv_obj_get_scroll_y(obj);
lv_anim_set_values(&a, -sy, -sy + y);
lv_anim_set_values(&a, -sy, -sy + dy);
lv_anim_set_exec_cb(&a, scroll_y_anim);
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
@ -298,7 +347,7 @@ void lv_obj_scroll_by(lv_obj_t * obj, lv_coord_t x, lv_coord_t y, lv_anim_enable
/*Remove pending animations*/
bool y_del = lv_anim_del(obj, scroll_y_anim);
bool x_del = lv_anim_del(obj, scroll_x_anim);
scroll_by_raw(obj, x, y);
scroll_by_raw(obj, dx, dy);
if(y_del || x_del) {
lv_res_t res;
res = lv_event_send(obj, LV_EVENT_SCROLL_END, NULL);
@ -317,48 +366,20 @@ void lv_obj_scroll_to_x(lv_obj_t * obj, lv_coord_t x, lv_anim_enable_t anim_en)
{
lv_anim_del(obj, scroll_x_anim);
/*Don't let scroll more then naturally possible by the size of the content*/
if(lv_obj_get_style_base_dir(obj, LV_PART_MAIN) != LV_BASE_DIR_RTL) {
if(x < 0) x = 0;
if(x > 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_left(obj) + lv_obj_get_scroll_right(obj);
if(scroll_max < 0) scroll_max = 0;
if(x > scroll_max) x = scroll_max;
}
}
else {
if(x > 0) x = 0;
if(x < 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_left(obj) + lv_obj_get_scroll_right(obj);
if(scroll_max < 0) scroll_max = 0;
if(x < -scroll_max) x = -scroll_max;
}
}
lv_coord_t scroll_x = lv_obj_get_scroll_x(obj);
lv_coord_t diff = -x + scroll_x;
lv_obj_scroll_by(obj, diff, 0, anim_en);
lv_obj_scroll_by_bounded(obj, diff, 0, anim_en);
}
void lv_obj_scroll_to_y(lv_obj_t * obj, lv_coord_t y, lv_anim_enable_t anim_en)
{
lv_anim_del(obj, scroll_y_anim);
/*Don't let scroll more then naturally possible by the size of the content*/
if(y < 0) y = 0;
if(y > 0) {
lv_coord_t scroll_max = lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
if(scroll_max < 0) scroll_max = 0;
if(y > scroll_max) y = scroll_max;
}
lv_coord_t scroll_y = lv_obj_get_scroll_y(obj);
lv_coord_t diff = -y + scroll_y;
lv_obj_scroll_by(obj, 0, diff, anim_en);
lv_obj_scroll_by_bounded(obj, 0, diff, anim_en);
}
void lv_obj_scroll_to_view(lv_obj_t * obj, lv_anim_enable_t anim_en)

View File

@ -183,17 +183,27 @@ void lv_obj_get_scroll_end(struct _lv_obj_t * obj, lv_point_t * end);
*====================*/
/**
*
* Scroll by a given amount of pixels
* @param obj pointer to an object to scroll
* @param x pixels to scroll horizontally
* @param y pixels to scroll vertically
* @param dx pixels to scroll horizontally
* @param dy pixels to scroll vertically
* @param anim_en LV_ANIM_ON: scroll with animation; LV_ANIM_OFF: scroll immediately
* @note > 0 value means scroll right/bottom (show the more content on the right/bottom)
* @note
* @note e.g. dy = -20 means scroll down 20 px
*/
void lv_obj_scroll_by(struct _lv_obj_t * obj, lv_coord_t x, lv_coord_t y, lv_anim_enable_t anim_en);
/**
* Scroll by a given amount of pixels.
* `dx` and `dy` will be limited internally to allow scrolling only on the content area.
* @param obj pointer to an object to scroll
* @param dx pixels to scroll horizontally
* @param dy pixels to scroll vertically
* @param anim_en LV_ANIM_ON: scroll with animation; LV_ANIM_OFF: scroll immediately
* @note e.g. dy = -20 means scroll down 20 px
*/
void lv_obj_scroll_by_bounded(struct _lv_obj_t * obj, lv_coord_t dx, lv_coord_t dy, lv_anim_enable_t anim_en);
/**
* Scroll to a given coordinate on an object.
* `x` and `y` will be limited internally to allow scrolling only on the content area.

View File

@ -0,0 +1,354 @@
/**
* @file lv_gridnav.c
*
*/
/*********************
* INCLUDES
*********************/
#include "lv_gridnav.h"
#if LV_USE_GRIDNAV
#include "../../../misc/lv_assert.h"
#include "../../../misc/lv_math.h"
#include "../../../core/lv_indev.h"
/*********************
* DEFINES
*********************/
/**********************
* TYPEDEFS
**********************/
typedef struct {
lv_gridnav_ctrl_t ctrl;
lv_obj_t * focused_obj;
} lv_gridnav_dsc_t;
typedef enum {
FIND_LEFT,
FIND_RIGHT,
FIND_TOP,
FIND_BOTTOM,
FIND_NEXT_ROW_FIRST_ITEM,
FIND_PREV_ROW_LAST_ITEM,
FIND_FIRST_ROW,
FIND_LAST_ROW,
} find_mode_t;
/**********************
* STATIC PROTOTYPES
**********************/
static void gridnav_event_cb(lv_event_t * e);
static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode);
static lv_obj_t * find_first_focusable(lv_obj_t * obj);
static lv_obj_t * find_last_focusable(lv_obj_t * obj);
static bool obj_is_focuable(lv_obj_t * obj);
static lv_coord_t get_x_center(lv_obj_t * obj);
static lv_coord_t get_y_center(lv_obj_t * obj);
/**********************
* STATIC VARIABLES
**********************/
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
void lv_gridnav_add(lv_obj_t * obj, lv_gridnav_ctrl_t ctrl)
{
lv_gridnav_remove(obj); /*Be sure to not add gridnav twice*/
lv_gridnav_dsc_t * dsc = lv_mem_alloc(sizeof(lv_gridnav_dsc_t));
LV_ASSERT_MALLOC(dsc);
dsc->ctrl = ctrl;
dsc->focused_obj = NULL;
lv_obj_add_event_cb(obj, gridnav_event_cb, LV_EVENT_ALL, dsc);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLL_WITH_ARROW);
}
void lv_gridnav_remove(lv_obj_t * obj)
{
lv_gridnav_dsc_t * dsc = lv_obj_get_event_user_data(obj, gridnav_event_cb);
if(dsc == NULL) return; /* no gridnav on this object */
lv_mem_free(dsc);
lv_obj_remove_event_cb(obj, gridnav_event_cb);
}
/**********************
* STATIC FUNCTIONS
**********************/
static void gridnav_event_cb(lv_event_t * e)
{
lv_obj_t * obj = lv_event_get_current_target(e);
lv_gridnav_dsc_t * dsc = lv_event_get_user_data(e);
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_KEY) {
uint32_t child_cnt = lv_obj_get_child_cnt(obj);
if(child_cnt == 0) return;
if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj);
if(dsc->focused_obj == NULL) return;
uint32_t key = lv_indev_get_key(lv_indev_get_act());
lv_obj_t * guess = NULL;
if(key == LV_KEY_RIGHT) {
if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
lv_obj_get_scroll_right(dsc->focused_obj) > 0) {
lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
if(d <= 0) d = 1;
lv_obj_scroll_by_bounded(dsc->focused_obj, -d, 0, LV_ANIM_ON);
}
else {
guess = find_chid(obj, dsc->focused_obj, FIND_RIGHT);
if(guess == NULL) {
if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
guess = find_chid(obj, dsc->focused_obj, FIND_NEXT_ROW_FIRST_ITEM);
if(guess == NULL) guess = find_first_focusable(obj);
}
else {
lv_group_focus_next(lv_obj_get_group(obj));
}
}
}
}
else if(key == LV_KEY_LEFT) {
if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
lv_obj_get_scroll_left(dsc->focused_obj) > 0) {
lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
if(d <= 0) d = 1;
lv_obj_scroll_by_bounded(dsc->focused_obj, d, 0, LV_ANIM_ON);
}
else {
guess = find_chid(obj, dsc->focused_obj, FIND_LEFT);
if(guess == NULL) {
if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
guess = find_chid(obj, dsc->focused_obj, FIND_PREV_ROW_LAST_ITEM);
if(guess == NULL) guess = find_last_focusable(obj);
}
else {
lv_group_focus_prev(lv_obj_get_group(obj));
}
}
}
}
else if(key == LV_KEY_DOWN) {
if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
lv_obj_get_scroll_bottom(dsc->focused_obj) > 0) {
lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
if(d <= 0) d = 1;
lv_obj_scroll_by_bounded(dsc->focused_obj, 0, -d, LV_ANIM_ON);
}
else {
guess = find_chid(obj, dsc->focused_obj, FIND_BOTTOM);
if(guess == NULL) {
if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
guess = find_chid(obj, dsc->focused_obj, FIND_FIRST_ROW);
}
else {
lv_group_focus_next(lv_obj_get_group(obj));
}
}
}
}
else if(key == LV_KEY_UP) {
if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
lv_obj_get_scroll_top(dsc->focused_obj) > 0) {
lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
if(d <= 0) d = 1;
lv_obj_scroll_by_bounded(dsc->focused_obj, 0, d, LV_ANIM_ON);
}
else {
guess = find_chid(obj, dsc->focused_obj, FIND_TOP);
if(guess == NULL) {
if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
guess = find_chid(obj, dsc->focused_obj, FIND_LAST_ROW);
}
else {
lv_group_focus_prev(lv_obj_get_group(obj));
}
}
}
}
else {
if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
lv_event_send(dsc->focused_obj, LV_EVENT_KEY, &key);
}
}
if(guess && guess != dsc->focused_obj) {
lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_add_state(guess, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_scroll_to_view(guess, LV_ANIM_ON);
dsc->focused_obj = guess;
}
}
else if(code == LV_EVENT_FOCUSED) {
if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj);
if(dsc->focused_obj) {
lv_obj_add_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_scroll_to_view(dsc->focused_obj, LV_ANIM_OFF);
}
}
else if(code == LV_EVENT_DEFOCUSED) {
if(dsc->focused_obj) {
lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
}
}
else if(code == LV_EVENT_CHILD_CREATED) {
lv_obj_t * child = lv_event_get_target(e);
if(lv_obj_get_parent(child) == obj) {
if(dsc->focused_obj == NULL) {
dsc->focused_obj = child;
if(lv_obj_has_state(obj, LV_STATE_FOCUSED)) {
lv_obj_add_state(child, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
lv_obj_scroll_to_view(child, LV_ANIM_OFF);
}
}
}
}
else if(code == LV_EVENT_CHILD_DELETED) {
/*This event bubble, so be sure this object's child was deleted.
*As we don't know which object was deleted we can't make the next focused.
*So make the first object focused*/
lv_obj_t * target = lv_event_get_target(e);
if(target == obj) {
dsc->focused_obj = find_first_focusable(obj);
}
}
else if(code == LV_EVENT_DELETE) {
lv_gridnav_remove(obj);
}
else if(code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_PRESS_LOST ||
code == LV_EVENT_LONG_PRESSED || code == LV_EVENT_LONG_PRESSED_REPEAT ||
code == LV_EVENT_CLICKED || code == LV_EVENT_RELEASED) {
if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
/*Forward press/release related event too*/
lv_indev_type_t t = lv_indev_get_type(lv_indev_get_act());
if(t == LV_INDEV_TYPE_ENCODER || t == LV_INDEV_TYPE_KEYPAD) {
lv_event_send(dsc->focused_obj, code, lv_indev_get_act());
}
}
}
}
static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode)
{
lv_coord_t x_start = get_x_center(start_child);
lv_coord_t y_start = get_y_center(start_child);
uint32_t child_cnt = lv_obj_get_child_cnt(obj);
lv_obj_t * guess = NULL;
lv_coord_t x_err_guess = LV_COORD_MAX;
lv_coord_t y_err_guess = LV_COORD_MAX;
lv_coord_t h_half = lv_obj_get_height(start_child) / 2;
lv_coord_t h_max = lv_obj_get_height(obj) + lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
uint32_t i;
for(i = 0; i < child_cnt; i++) {
lv_obj_t * child = lv_obj_get_child(obj, i);
if(child == start_child) continue;
if(obj_is_focuable(child) == false) continue;
lv_coord_t x_err;
lv_coord_t y_err;
switch(mode) {
case FIND_LEFT:
x_err = get_x_center(child) - x_start;
y_err = get_y_center(child) - y_start;
if(x_err >= 0) continue; /*It's on the right*/
if(LV_ABS(y_err) > h_half) continue; /*Too far*/
break;
case FIND_RIGHT:
x_err = get_x_center(child) - x_start;
y_err = get_y_center(child) - y_start;
if(x_err <= 0) continue; /*It's on the left*/
if(LV_ABS(y_err) > h_half) continue; /*Too far*/
break;
case FIND_TOP:
x_err = get_x_center(child) - x_start;
y_err = get_y_center(child) - y_start;
if(y_err >= 0) continue; /*It's on the bottom*/
break;
case FIND_BOTTOM:
x_err = get_x_center(child) - x_start;
y_err = get_y_center(child) - y_start;
if(y_err <= 0) continue; /*It's on the top*/
break;
case FIND_NEXT_ROW_FIRST_ITEM:
y_err = get_y_center(child) - y_start;
if(y_err <= 0) continue; /*It's on the top*/
x_err = lv_obj_get_x(child);
break;
case FIND_PREV_ROW_LAST_ITEM:
y_err = get_y_center(child) - y_start;
if(y_err >= 0) continue; /*It's on the bottom*/
x_err = obj->coords.x2 - child->coords.x2;
break;
case FIND_FIRST_ROW:
x_err = get_x_center(child) - x_start;
y_err = lv_obj_get_y(child);
break;
case FIND_LAST_ROW:
x_err = get_x_center(child) - x_start;
y_err = h_max - lv_obj_get_y(child);
}
if(guess == NULL ||
(y_err * y_err + x_err * x_err < y_err_guess * y_err_guess + x_err_guess * x_err_guess)) {
guess = child;
x_err_guess = x_err;
y_err_guess = y_err;
}
}
return guess;
}
static lv_obj_t * find_first_focusable(lv_obj_t * obj)
{
uint32_t child_cnt = lv_obj_get_child_cnt(obj);
uint32_t i;
for(i = 0; i < child_cnt; i++) {
lv_obj_t * child = lv_obj_get_child(obj, i);
if(obj_is_focuable(child)) return child;
}
return NULL;
}
static lv_obj_t * find_last_focusable(lv_obj_t * obj)
{
uint32_t child_cnt = lv_obj_get_child_cnt(obj);
int32_t i;
for(i = child_cnt - 1; i >= 0; i++) {
lv_obj_t * child = lv_obj_get_child(obj, i);
if(obj_is_focuable(child)) return child;
}
return NULL;
}
static bool obj_is_focuable(lv_obj_t * obj)
{
if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return false;
if(lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)) return true;
else return false;
}
static lv_coord_t get_x_center(lv_obj_t * obj)
{
return obj->coords.x1 + lv_area_get_width(&obj->coords) / 2;
}
static lv_coord_t get_y_center(lv_obj_t * obj)
{
return obj->coords.y1 + lv_area_get_height(&obj->coords) / 2;
}
#endif /*LV_USE_GRIDNAV*/

View File

@ -0,0 +1,115 @@
/**
* @file lv_templ.c
*
*/
/*********************
* INCLUDES
*********************/
/*********************
* DEFINES
*********************/
/**********************
* TYPEDEFS
**********************/
/*This typedef exists purely to keep -Wpedantic happy when the file is empty.*/
/*It can be removed.*/
typedef int _keep_pedantic_happy;
/**********************
* STATIC PROTOTYPES
**********************/
/**********************
* STATIC VARIABLES
**********************/
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
/**********************
* STATIC FUNCTIONS
**********************/
/**
* @file lv_gridnav.h
*
*/
#ifndef LV_GRIDFOCUS_H
#define LV_GRIDFOCUS_H
#ifdef __cplusplus
extern "C" {
#endif
/*********************
* INCLUDES
*********************/
#include "../../../core/lv_obj.h"
#if LV_USE_GRIDNAV
/*********************
* DEFINES
*********************/
/**********************
* TYPEDEFS
**********************/
typedef enum {
LV_GRIDNAV_CTRL_NONE = 0x0,
/**
* If there is no next/previous object in a direction,
* the focus goes to the object in the next/previous row (on left/right keys)
* or first/last row (on up/down keys)
*/
LV_GRIDNAV_CTRL_ROLLOVER = 0x1,
/**
* If an arrow is pressed and the focused object can be scrolled in that direction
* then it will be scrolled instead of going to the next/previous object.
* If there is no more room for scrolling the next/previous object will be focused normally */
LV_GRIDNAV_CTRL_SCROLL_FIRST = 0x2,
} lv_gridnav_ctrl_t;
/**********************
* GLOBAL PROTOTYPES
**********************/
/**
* Add grid navigation feature to an object. It expects the children to be arranged
* into a grid-like layout. Although it's not required to have pixel perfect alignment.
* This feature makes possible to use keys to navigate among the children and focus them.
* The keys other than arrows and press/release related events
* are forwarded to the focused child.
* @param obj pointer to an object on which navigation should be applied.
* @param ctrl control flags from `lv_gridnav_ctrl_t`.
*/
void lv_gridnav_add(lv_obj_t * obj, lv_gridnav_ctrl_t ctrl);
/**
* Remove the grid navigation support from an object
* @param obj pointer to an object
*/
void lv_gridnav_remove(lv_obj_t * obj);
/**********************
* MACROS
**********************/
#endif /*LV_USE_GRIDNAV*/
#ifdef __cplusplus
} /*extern "C"*/
#endif
#endif /*LV_GRIDFOCUS_H*/

View File

@ -15,6 +15,7 @@ extern "C" {
*********************/
#include "snapshot/lv_snapshot.h"
#include "monkey/lv_monkey.h"
#include "gridnav/lv_gridnav.h"
/*********************
* DEFINES

View File

@ -1991,15 +1991,11 @@
/*1: Enable API to take snapshot for object*/
#ifndef LV_USE_SNAPSHOT
#ifdef _LV_KCONFIG_PRESENT
#ifdef CONFIG_LV_USE_SNAPSHOT
#define LV_USE_SNAPSHOT CONFIG_LV_USE_SNAPSHOT
#else
#define LV_USE_SNAPSHOT 0
#endif
#else
#define LV_USE_SNAPSHOT 1
#endif
#endif
/*1: Enable Monkey test*/
@ -2011,6 +2007,15 @@
#endif
#endif
/*1: Enable grid navigation*/
#ifndef LV_USE_GRIDNAV
#ifdef CONFIG_LV_USE_GRIDNAV
#define LV_USE_GRIDNAV CONFIG_LV_USE_GRIDNAV
#else
#define LV_USE_GRIDNAV 0
#endif
#endif
/*==================
* EXAMPLES
*==================*/