Page navigation framework for ESP-IDF + LVGL: lifecycle, swipe gestures, slide/fade/slide-over transitions, theme abstraction, and a display base class.
- Page lifecycle —
OnCreate/OnEnter/OnLeave/OnDestroyonPageBase - Navigation —
UIManagersingleton: page stack, gestures,NavigateTo/NavigateBack - Transitions — Slide, SlideOver, Fade, None
- Page caching — Configurable inactive page limit for memory-constrained devices
- Async lifecycle guards — Bind late callbacks to the page activation that started them
- Theme —
ui_theme_tfor fonts, colors, spacing; optionalui::Displaybase for status bar / notifications
- ESP-IDF 5.x
- LVGL 9.x (via idf_component or managed_components)
Local: Copy components/lvgl_nav_kit into your project components/, then:
idf_component_register(...
REQUIRES lvgl_nav_kit
)Registry: In your project's idf_component.yml:
dependencies:
txp666/lvgl-nav-kit: "^1.0.0"After LVGL and display are initialized (lv_scr_act() valid):
#include "lvgl_nav_kit/ui_manager.h"
#include "lvgl_nav_kit/page_base.h"
#include "lvgl_nav_kit/page_registry.h"
// Init (nullptr = default theme)
auto &mgr = ui::UIManager::GetInstance();
mgr.Initialize(lv_scr_act(), nullptr);
auto ® = mgr.GetRegistry();
reg.RegisterPage(new HomePage());
reg.RegisterPage(new SettingsPage());
// e.g. swipe left from "home" -> "settings"; swipe right from "settings" -> "home"
reg.SetNavigation("home", {{{"settings", ui::Direction::Right}, {}, {}, {}}});
reg.SetNavigation("settings", {{{}, {"home", ui::Direction::Left}, {}, {}}});
// Optional: limit inactive pages in memory (default -1 = unlimited)
mgr.SetMaxCachedPages(3);
mgr.NavigateTo("home");Implement pages by subclassing PageBase, overriding OnCreate(lv_obj_t *parent) and using helpers like CreateLabel, CreateButton, SetPageBackground, GetStatusBarHeight().
| UIManager | PageBase (override) |
|---|---|
Initialize(screen, theme) |
OnCreate(parent) required |
GetRegistry() → RegisterPage, SetNavigation |
OnEnter, OnLeave, OnDestroy |
NavigateTo(id, dir, type), NavigateBack() |
CreateLabel, CreateButton, CreateCard, … |
SetTransitionDuration(ms), EnableGesture(bool) |
GetStatusBarHeight(), GetTheme() |
SetMaxCachedPages(n) — page memory management |
ShowLoading(), HideLoading() |
Transitions: Slide (both pages slide), SlideOver (new page slides over, old stays), Fade, None. Set per-navigation via NavTarget(page, dir, type) or per-call via NavigateTo(id, dir, type).
NavigateBack: Automatically reverses the animation direction and uses the same transition type as the forward navigation. Navigation requests are ignored while a transition is active, preventing rapid input from corrupting history.
Display: Subclass ui::Display in your app for status bar/notifications; use ui::NoDisplay when headless. Theme's status_bar_height (0 = none) is used by GetStatusBarHeight(). DisplayLockGuard::IsLocked() reports whether the lock was acquired; a failed acquisition is never unlocked.
Touch: Display/touch hardware init stays in the app. For custom pointer input (e.g. FT6236), use lvgl_nav_kit_add_pointer_indev(disp, read_cb, user_data) from lvgl_nav_kit/display.h.
Thread safety: All UIManager public methods must be called from the LVGL task (or while holding the LVGL lock when using esp_lvgl_port).
Page timers: Timers created with CreateTimer() are paused while their page is inactive. Their prior state is preserved: a timer paused before OnLeave() remains paused after re-entry. PageBase owns and deletes these timers; do not call lv_timer_delete() on them directly.
Loading overlays: ShowLoading() creates a page-owned modal on LVGL's top layer. It is automatically removed when the page leaves or is destroyed, and its pointer is cleared if LVGL deletes the object externally. Calling HideLoading() remains safe and optional during lifecycle cleanup.
Asynchronous callbacks: Wrap callbacks that touch page UI with BindToActivation(...). The callback is skipped if the page has left, entered again, or been deleted, so a late worker result cannot write into stale LVGL objects.
For work that should also complete while a page is cached and inactive, use BindToLifetime(...). It remains valid across leave/enter, but is invalidated when the page container is destroyed or recreated. LVGL access must still be marshalled onto the LVGL thread.
- examples/minimal — Multi-page UI (Home / Settings / List / Detail) with SlideOver demo. Copy
main/into your project; ensure LVGL + display are inited first. - examples/esp32_lcd_touch — Full runnable: ESP32-S3 + ST7789 LCD + FT6236 touch registration, then same UI. Copy
main/and add deps (see example README).
MIT.