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 and Grid 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.

Gridnav assumes that the object to which gridnav is added is part of a group. 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

Basic grid navigation

C code  

 GitHub
#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, "%"LV_PRIu32"", 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

MicroPython code  

 GitHub Simulator
#
# Demonstrate a a basic grid navigation
#
# It's assumed that the default group is set and
# there is a keyboard indev

cont1 = lv.obj(lv.scr_act())
lv.gridnav_add(cont1, lv.GRIDNAV_CTRL.NONE)

# Use flex here, but works with grid or manually placed objects as well
cont1.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
cont1.set_style_bg_color(lv.palette_lighten(lv.PALETTE.BLUE, 5), lv.STATE.FOCUSED)
cont1.set_size(lv.pct(50), lv.pct(100))

# Only the container needs to be in a group
lv.group_get_default().add_obj(cont1)

label = lv.label(cont1)
label.set_text("No rollover")

for i in range(10):
    obj = lv.btn(cont1)
    obj.set_size(70, lv.SIZE_CONTENT)
    obj.add_flag(lv.obj.FLAG.CHECKABLE)
    lv.group_remove_obj(obj)   # Not needed, we use the gridnav instead

    label = lv.label(obj)
    label.set_text("{:d}".format(i))
    label.center()

# Create a second container with rollover grid nav mode.

cont2 = lv.obj(lv.scr_act())
lv.gridnav_add(cont2,lv.GRIDNAV_CTRL.ROLLOVER)
cont2.set_style_bg_color(lv.palette_lighten(lv.PALETTE.BLUE, 5), lv.STATE.FOCUSED)
cont2.set_size(lv.pct(50), lv.pct(100))
cont2.align(lv.ALIGN.RIGHT_MID, 0, 0)

label = lv.label(cont2)
label.set_width(lv.pct(100))
label.set_text("Rollover\nUse tab to focus the other container")

# Only the container needs to be in a group
lv.group_get_default().add_obj(cont2)

# Add and place some children manually
ta = lv.textarea(cont2)
ta.set_size(lv.pct(100), 80)
ta.set_pos(0, 80);
lv.group_remove_obj(ta)   # Not needed, we use the gridnav instead

cb = lv.checkbox(cont2)
cb.set_pos(0, 170)
lv.group_remove_obj(cb)   # Not needed, we use the gridnav instead

sw1 = lv.switch(cont2)
sw1.set_pos(0, 200);
lv.group_remove_obj(sw1)  # Not needed, we use the gridnav instead

sw2 = lv.switch(cont2)
sw2.set_pos(lv.pct(50), 200)
lv.group_remove_obj(sw2)  # Not needed, we use the gridnav instead


Grid navigation on a list

C code  

 GitHub
#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

MicroPython code  

 GitHub Simulator
#
# Grid navigation on a list
#
# It's assumed that the default group is set and
# there is a keyboard indev

list1 = lv.list(lv.scr_act())
lv.gridnav_add(list1, lv.GRIDNAV_CTRL.NONE)
list1.set_size(lv.pct(45), lv.pct(80))
list1.align(lv.ALIGN.LEFT_MID, 5, 0)
list1.set_style_bg_color(lv.palette_lighten(lv.PALETTE.BLUE, 5), lv.STATE.FOCUSED)
lv.group_get_default().add_obj(list1)

for i in range(15):
    item_text = "File {:d}".format(i)
    item = list1.add_btn(lv.SYMBOL.FILE, item_text)
    item.set_style_bg_opa(0, 0)
    lv.group_remove_obj(item)   # Not needed, we use the gridnav instead

list2 = lv.list(lv.scr_act())
lv.gridnav_add(list2, lv.GRIDNAV_CTRL.ROLLOVER)
list2.set_size(lv.pct(45), lv.pct(80))
list2.align(lv.ALIGN.RIGHT_MID, -5, 0)
list2.set_style_bg_color(lv.palette_lighten(lv.PALETTE.BLUE, 5), lv.STATE.FOCUSED)
lv.group_get_default().add_obj(list2)

for i in range(15):
    item_text = "Folder {:d}".format(i)
    item = list2.add_btn(lv.SYMBOL.DIRECTORY, item_text)
    item.set_style_bg_opa(0, 0)
    lv.group_remove_obj(item)


Nested grid navigations

C code  

 GitHub
#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

MicroPython code  

 GitHub Simulator
def cont_sub_event_cb(e):
    k = e.get_key()
    obj = e.get_current_target()
    if k == lv.KEY.ENTER:
        lv.group_focus_obj(obj)

    elif k == lv.KEY.ESC:
        obj.get_group().focus_next()

#
# Nested grid navigations
#
# It's assumed that the default group is set and
# there is a keyboard indev*/

cont_main = lv.obj(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_get_default().add_obj(cont_main)

# Use flex here, but works with grid or manually placed objects as well
cont_main.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
cont_main.set_style_bg_color(lv.palette_lighten(lv.PALETTE.BLUE, 5), lv.STATE.FOCUSED)
cont_main.set_size(lv.pct(80), lv.SIZE_CONTENT)

btn = lv.btn(cont_main)
lv.group_remove_obj(btn)
label = lv.label(btn)
label.set_text("Button 1")

btn = lv.btn(cont_main)
lv.group_remove_obj(btn)
label = lv.label(btn)
label.set_text("Button 2");


# Create an other container with long text to show how LV_GRIDNAV_CTRL_SCROLL_FIRST works
cont_sub1 = lv.obj(cont_main)
cont_sub1.set_size(lv.pct(100), 100)

label = lv.label(cont_sub1)
cont_sub1.set_style_bg_color(lv.palette_lighten(lv.PALETTE.RED, 5), lv.STATE.FOCUSED)
label.set_width(lv.pct(100));
label.set_text(
    """I'm a very long text which makes my container scrollable. 
    As LV_GRIDNAV_FLAG_SCROLL_FIRST is enabled arrows will scroll me first 
    and a new objects will be focused only when an edge is reached with the scrolling.\n
    This is only some placeholder text to be sure the parent will be scrollable. \n
    Hello world!
    Hello world!
    Hello world!
    Hello world!
    Hello world!
    Hello world!
    """)

# Create a third container that can be focused with ENTER and contains an other grid nav
cont_sub2 = lv.obj(cont_main)
lv.gridnav_add(cont_sub2, lv.GRIDNAV_CTRL.ROLLOVER)
#Only the container needs to be in a group
lv.group_get_default().add_obj(cont_sub2)

cont_sub2.add_event_cb(cont_sub_event_cb, lv.EVENT.KEY, None)

# Use flex here, but works with grid or manually placed objects as well
cont_sub2.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP)
cont_sub2.set_style_bg_color(lv.palette_lighten(lv.PALETTE.RED, 5), lv.STATE.FOCUSED)
cont_sub2.set_size(lv.pct(100), lv.SIZE_CONTENT)

label = lv.label(cont_sub2)
label.set_text("Use ENTER/ESC to focus/defocus this container")
label.set_width(lv.pct(100))

btn = lv.btn(cont_sub2)
lv.group_remove_obj(btn)
label = lv.label(btn)
label.set_text("Button 3")

btn = lv.btn(cont_sub2)
lv.group_remove_obj(btn)
label = lv.label(btn)
label.set_text("Button 4")


Simple navigation on a list widget

C code  

 GitHub
#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

MicroPython code  

 GitHub Simulator
def event_handler(e):
    obj = e.get_target()
    list = obj.get_parent()
    print("Clicked: " + list.get_btn_text(obj))


#
# Simple navigation on a list widget
#
# It's assumed that the default group is set and
# there is a keyboard indev

list = lv.list(lv.scr_act())
lv.gridnav_add(list, lv.GRIDNAV_CTRL.ROLLOVER)
list.align(lv.ALIGN.LEFT_MID, 0, 10)
lv.group_get_default().add_obj(list)

for i in range(20):

    # Add some separators too, they are not focusable by gridnav
    if i % 5 == 0:
        txt = "Section {:d}".format(i // 5 + 1)
        # lv_snprintf(buf, sizeof(buf), "Section %d", i / 5 + 1);
        list.add_text(txt)

    txt = "File {:d}".format(i + 1)
    #lv_snprintf(buf, sizeof(buf), "File %d", i + 1);
    item = list.add_btn(lv.SYMBOL.FILE, txt)
    item.add_event_cb(event_handler, lv.EVENT.CLICKED, None)
    lv.group_remove_obj(item)  # The default group adds it automatically

btn = lv.btn(lv.scr_act())
btn.align(lv.ALIGN.RIGHT_MID, 0, -10)
label = lv.label(btn)
label.set_text("Button")


API

Typedefs

typedef int _keep_pedantic_happy

Enums

enum lv_gridnav_ctrl_t

Values:

enumerator LV_GRIDNAV_CTRL_NONE
enumerator 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)

enumerator 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

Functions

void lv_gridnav_add(lv_obj_t *obj, lv_gridnav_ctrl_t ctrl)

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.

Parameters
  • obj -- pointer to an object on which navigation should be applied.

  • ctrl -- control flags from lv_gridnav_ctrl_t.

void lv_gridnav_remove(lv_obj_t *obj)

Remove the grid navigation support from an object

Parameters

obj -- pointer to an object

void lv_gridnav_set_focused(lv_obj_t *cont, lv_obj_t *to_focus, lv_anim_enable_t anim_en)

Manually focus an object on gridnav container

Parameters
  • cont -- pointer to a gridnav container

  • to_focus -- pointer to an object to focus

  • anim_en -- LV_ANIM_ON/OFF