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

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