The most efficient way to structure a multi screen LittlevGL project

As my project grows i am starting to see my ESP32 processor get stretched for the first time and it makes me wonder what is the most efficient way (memory, processing power etc) to structure my LittlevGL project.

I currently have ten different pages (you could call them screens), i have set this up running on tileview. Scrolling is turned off and buttons are used to navigate between the pages.

Is there a significantly better way to go about this?

1 Like

I’m working on a control panel for a 3D printer recently. It also needs a lot of screens to show different functions. I can tell you how I solved this problem.

When multiple screens exist, you need to consume a lot of memory. This is obviously unreasonable. So we need to do a good job of screen mutual exclusion. Only one screen can be instantiated and displayed at a time.
To do this, I designed a Screen class :
screen.h


/**
 * @brief 定义一个屏幕界面的基类
 * 屏幕管理
 *
 * 控制显示
 * 控制隐藏
 *
 * 管理与屏幕相关的任务
 *
 */
class Screen : public LVObject
{
    LV_OBJECT

    friend class MarlinUi;
    friend class ScreenTask;
    friend lv_res_t screenSignalFunc(struct _lv_obj_t * obj, lv_signal_t sign, void * param);
protected:

    ////////// 形成屏幕的显示链 ////////////////
    Screen * m_preScreen = nullptr; //!< 前一个屏幕
    Screen * m_nextScreen = nullptr; //!< 后一个屏幕

    static Screen * s_lastScreen; //!< 之前显示的屏幕
    static Screen * s_currScreen; //!< 当前显示的屏幕

    /////////// 任务列表 ////////////
    LVLinkList m_taskList; //!< 屏幕拥有的任务列表

    //////////// 外观属性 /////////////////
    LVColor m_screenColor; //!< 屏幕颜色
    bool m_inited = false; //!< 标识屏幕是否初始化
    bool m_deleteAfterHide = false; //!< 隐藏后清理屏幕数据
    bool m_clearAfterHide = true; //!< 隐藏后清理屏幕(子对象)数据
    int32_t m_memoryUsed = -1; //!< 统计内存消耗

public:
    Screen();

    ~Screen();

    /**
     * @brief 显示屏幕
     * @return true 显示成功 ; false 显示失败
     */
    bool show();

    /**
     * @brief 显示之前运行的函数
     * 可用来判断屏幕是否能够显示
     * @return true 可以显示 ; false 不能显示
     */
    virtual bool beforeShow();

    /**
     * @brief 显示之后
     */
    virtual void afterShow(){}

    /**
     * @brief 隐藏屏幕
     * 跳转到上一个屏幕或者主屏幕
     * @return true 显示成功 ; false 显示失败
     */
    bool hide();

    /**
     * @brief 隐藏之前
     * 可用来判断屏幕是否能够隐藏
     * @return true 可以显示 ; 不能隐藏
     */
    virtual bool beforeHide(){return true;}

    /**
     * @brief 隐藏之后
     */
    virtual void afterHide(){}

    /**
     * @brief 退回到上一个显示的屏幕
     */
    void backScreen();
    bool hasBackScreen();

    /**
     * @brief 显示前一个屏幕
     */
    void preScreen();
    bool hasPreScreen();
    void setPreScreen(Screen * screen);

    /**
     * @brief 显示后一个屏幕
     */
    void nextScreen();
    bool hasNextScreen();
    void setNextScreen(Screen * screen);

    /**
     * @brief 在屏幕上是否可见
     * @return
     */
    bool isVisible();

    /**
     * @brief 设置屏幕的显隐
     * @param value
     */
    void setVisible(bool value);

    /**
     * @brief 最后显示的一个屏幕界面
     * @return
     */
    static Screen * LastScreen();

    /**
     * @brief 当前显示的屏幕界面
     * @return
     */
    static Screen * CurrScreen();

    bool isDeleteAfterHide();
    void setDeleteAfterHide(bool value);

    bool isInited(){ return m_inited; }

    bool isClearAfterHide() const;
    void setClearAfterHide(bool isClearAfterHide);

    /**
     * @brief 清除屏幕
     */
    void cleanScreen();

protected:

    /**
     * @brief 屏幕清理前
     */
    virtual void beforeCleanScreen();

    /**
     * @brief 屏幕清理后
     */
    virtual void afterCleanScreen();

    /**
     * @brief 初始化屏幕
     * 初始化屏幕显示和任务
     * @return
     */
    virtual bool initScreen();

};

Some important functions
screen.cpp

void Screen::beforeCleanScreen()
{
    //NOTE: 子类中完成屏幕清理动作
}

void Screen::afterCleanScreen()
{
    //NOTE: 子类中完成屏幕清理动作
}

bool Screen::initScreen()
{
    LV_LOG_INFO("Screen::initScreen() empty function");
    return false;
}

bool Screen::show()
{
    bool ret = false;

    //如果存在其他的屏幕界面正在显示,需要先隐藏当前的界面
    if(CurrScreen())
        CurrScreen()->hide();

    if(beforeShow())
    {
        screenLoad();
        setCurrScreen(this);
        //开启相关任务
        startScreenTask();
        afterShow();
        ret = true;
    }

    //安装界面的公共元素
    installPublicComponents(this);

    return ret;
}

bool Screen::beforeShow()
{

    //初始化界面
    //将初始化放到这理执行,有助于减少内存占用情况
    //没有用到的界面就不会消耗内存了
    if(!m_inited)
    {

        //统计初始化屏幕用了多少内存
        //方便在屏幕清理的时候发现内存泄露
        memory_monitor(nullptr);
        m_memoryUsed = getUsedMemorySize();
        m_inited = initScreen();
        m_memoryUsed = getUsedMemorySize() - m_memoryUsed;
        memory_monitor(nullptr);

        {
            char str[40];
            //警告内存有泄露
            sprintf(str,"Memory used : %d Bytes!", m_memoryUsed);
            LV_LOG_WARN(str);
        }

        if(!m_inited)
            LV_LOG_INFO("Screen::beforeShow() initScreen false");
    }

    return m_inited;
}

bool Screen::hide()
{
    bool ret = false;
    if(beforeHide())
    {
        //关闭相关任务
        stopScreenTask();
        //if(s_lastScreen != nullptr)
        //    s_lastScreen->show();
        //else if(((Screen*) MarlinUi::getInstance()->homeScreen()) != this )
        //    //BUG:存在主屏幕无法显示的情况
        //    MarlinUi::getInstance()->showHomeScreen();

        setCurrScreen(nullptr);
        setLastScreen(this);

        //卸载界面公共元素,避免被删除
        uninstallPublicComponents();

        //隐藏后清理屏幕
        if(isClearAfterHide())
        {
            int32_t memoryRecovery = getUsedMemorySize();
            cleanScreen();
            memoryRecovery = memoryRecovery - getUsedMemorySize();

            if(m_memoryUsed != memoryRecovery)
            {
                char str[40];
                //警告内存有泄露
                sprintf(str,"Memory leak deteted : %d Bytes!", m_memoryUsed - memoryRecovery);
                LV_LOG_WARN(str);
            }
        }

        afterHide();

        //隐藏后删除屏幕
        if(isDeleteAfterHide())
        {
            setLastScreen(nullptr);
            //延后清理对象,防止对象此时正在被使用
            //deleteLater();

            delete this;
        }

        ret = true;
    }

    return ret;
}

As you can see, screen has several important virtual functions:

virtual bool Screen::hide();
virtual bool Screen::beforeHide();
virtual bool Screen::afterHide();
virtual bool Screen::show();
virtual bool Screen::beforeShow();
virtual bool Screen::afterShow();
virtual bool Screen::initScreen();

I use these functions to guarantee the absolute mutual exclusion between the screens.
Every time I need to show a screen, show will check whether other interfaces are showing. If so, let other screen hide.Then show will call initScreen to initialize new interface. Like this:

bool HomeScreen::initScreen()
{

    /*
     * 实现主界面
     *
    ==============================================
    =                                            =
    =  000000000000   11111111111   22222222222  =
    =  000000000000   11111111111   22222222222  =
    =  000000000000   11111111111   22222222222  =
    =  000000000000   11111111111   22222222222  =
    =  000000000000   11111111111   22222222222  =
    =                                            =
    =  333333333333   4444444444444444444444444  =
    =  333333333333   4444444444444444444444444  =
    =  333333333333   4444444444444444444444444  =
    =  333333333333   4444444444444444444444444  =
    =  333333333333   4444444444444444444444444  =
    =                                            =
    = ..........................5555..6666..7777 =
    = ..........................5555..6666..7777 =
    ==============================================
    */

    //构建主界面

    int screenWidth = this->width();
    int screenHeight = this->height();
    int screenDpi = this->dpi();

    //计算出各区域大小
#if SCREEN_SIZE == 28
    int16_t safeMargin = 6;
    int16_t spacing = 4;
    int16_t barHeight = 26;

    int16_t butAreaWidth = screenWidth;
    int16_t butAreaHeight = screenHeight - barHeight;

    int16_t butWidth = 100;
    int16_t butHeight = 100;

    int16_t fontSize = 16;
    int16_t iconSize = 24;
#elif SCREEN_SIZE == 35
    // scale 120%
    int16_t safeMargin = 7;
    int16_t spacing = 5;
    int16_t barHeight = 32;

    int16_t butAreaWidth = screenWidth;
    int16_t butAreaHeight = screenHeight - barHeight;

    int16_t butWidth = 152;
    int16_t butHeight = 138;

    int16_t fontSize = 20;
    int16_t iconSize = 28;
#endif

    //主界面按钮区域
    {
        LVObject * butArea = new LVObject(this,nullptr);
        butArea->setPos(0,0);
        butArea->setSize(butAreaWidth,butAreaHeight);
        butArea->setStyle(THEME_STYLE_GET(Theme_screen_ButtonArea));

        //移动按钮
        LVButton * butMove = new LVButton(butArea,nullptr);
        butMove->setSize(butWidth,butHeight);
        butMove->alignOrigo(LV_ALIGN_IN_TOP_MID,0,safeMargin + butHeight / 2);
        butMove->setStyle(LV_BTN_STYLE_PR,THEME_STYLE_GET(Theme_Screen_ButtonPr));
        butMove->setStyle(LV_BTN_STYLE_REL,THEME_STYLE_GET(Theme_Screen_ButtonRel));
        butMove->setAction(LV_BTN_ACTION_CLICK,onMoveButClocked);
        //移动按钮图标
        LVImage * imgMove = new LVImage(butMove,nullptr);
        imgMove->setSize(iconSize,iconSize);
        imgMove->align(LV_ALIGN_CENTER,0,0);
        imgMove->setSrc(&move_24);
        //移动按钮文本
        labMove = new LVLabel(butMove,nullptr);
        labMove->setText(Lang_HomeScreen_ButMove); //"Move"


        //预热按钮
        LVButton * butPreheat = new LVButton(butArea,butMove);
        butPreheat->align(butMove,LV_ALIGN_OUT_LEFT_TOP,-spacing,0);
        butPreheat->setAction(LV_BTN_ACTION_CLICK,onTempButClocked);
        //预热按钮图标
        LVImage * imgPreheat = new LVImage(butPreheat,nullptr);
        imgPreheat->setSize(iconSize,iconSize);
        imgPreheat->align(LV_ALIGN_CENTER,0,0);
        imgPreheat->setSrc(&temp_24);
        //预热按钮文本
        labPreheat = new LVLabel(butPreheat,nullptr);
        labPreheat->setText(Lang_HomeScreen_ButPreHeat);//"Preheat"


        //挤出按钮
        LVButton * butExtrude = new LVButton(butArea,butMove);
        butExtrude->align(butMove,LV_ALIGN_OUT_RIGHT_TOP,spacing,0);
        butExtrude->setAction(LV_BTN_ACTION_CLICK,onExtrudeButClocked);
        //挤出按钮图标
        LVImage * imgExtrude = new LVImage(butExtrude,nullptr);
        imgExtrude->setSize(iconSize,iconSize);
        imgExtrude->align(LV_ALIGN_CENTER,0,0);
        imgExtrude->setSrc(&extrude_24);
        //挤出按钮文本
        labExtrude = new LVLabel(butExtrude,nullptr);
        labExtrude->setText(Lang_HomeScreen_ButExtrude); //"Extrude"

        //工具按钮
        LVButton * butTool = new LVButton(butArea,butMove);
        butTool->align(butPreheat,LV_ALIGN_OUT_BOTTOM_LEFT,0,spacing);
        butTool->setAction(LV_BTN_ACTION_CLICK,onToolButClocked);
        //工具按钮图标
        LVImage * imgTool = new LVImage(butTool,nullptr);
        imgTool->setSize(iconSize,iconSize);
        imgTool->align(LV_ALIGN_CENTER,0,0);
        imgTool->setSrc(&tool_24);
        //工具按钮文本
        labTool = new LVLabel(butTool,nullptr);
        labTool->setText(Lang_HomeScreen_ButTool);//"Tool"

        //打印按钮
        LVButton * butPrint = new LVButton(butArea,butMove);
        butPrint->setSize(butWidth*2 + spacing , butHeight);
        butPrint->align(butTool,LV_ALIGN_OUT_RIGHT_TOP,spacing,0);
        butPrint->setAction(LV_BTN_ACTION_CLICK,onPrintButClocked);
        //打印按钮图标
        LVImage * imgPrint = new LVImage(butPrint,nullptr);
        imgPrint->setSize(iconSize,iconSize);
        imgPrint->align(LV_ALIGN_CENTER,0,0);
        imgPrint->setSrc(&print_24);
        //打印按钮文本
        labPrint = new LVLabel(butPrint,nullptr);
        labPrint->setText(Lang_HomeScreen_ButPrint);//"Print"

    }

    //主界面消息区
    {
        //状态栏
        LVObject * stateBar = new LVObject(this,nullptr);
        stateBar->setSize(screenWidth,barHeight);
        stateBar->align(LV_ALIGN_IN_BOTTOM_LEFT,0,0);
        stateBar->setStyle(THEME_STYLE_GET(Theme_Screen_StateBar));

        //状态栏信息
        stateMesg = new LVLabel(stateBar,nullptr);
        stateMesg->align(LV_ALIGN_IN_LEFT_MID,safeMargin,0);
        stateMesg->setText(Lang_HomeScreen_MsgConnected); //"Connected!"
        stateMesg->align(LV_ALIGN_IN_LEFT_MID,safeMargin,0);

        //热床温度
        labHeatbed = new LVLabel(stateBar,nullptr);
        labHeatbed->setText("999");
        labHeatbed->align(LV_ALIGN_IN_RIGHT_MID,-safeMargin,0);
        labHeatbed->setValue((int16_t)ExtUI::getActualTemp_celsius(ExtUI::BED));

        //热床温度图标
        imgHeatbed = new LVImage(stateBar,nullptr);
        imgHeatbed->setSrc(&heatbed_20_12);
        imgHeatbed->align(labHeatbed,LV_ALIGN_OUT_LEFT_MID,-spacing,0);

        //喷嘴温度
        labHeatend = new LVLabel(stateBar,nullptr);
        labHeatend->setText("999");
        labHeatend->align(imgHeatbed,LV_ALIGN_OUT_LEFT_MID,-safeMargin*2,0);
        labHeatend->setValue((int16_t)ExtUI::getActualTemp_celsius(ExtUI::E0));

        //喷嘴温度图标
        imgHeatend = new LVImage(stateBar,nullptr);
        imgHeatend->setSrc(&heatend_20_12);
        imgHeatend->align(labHeatend,LV_ALIGN_OUT_LEFT_MID,-spacing,0);

    }

    LV_LOG_INFO(__FUNCTION__);

    return true;
}

It works like this:
%E6%89%93%E5%8D%B0%E6%9C%BA%E9%A6%96%E9%A1%B5%20-%20320240

Every time screen need to hide, hide will call cleanScreen to release all subobjects . Free up memory.

I only need 25KB of memory to implement more than 50 interfaces. All interfaces can switch to each other.

1 Like

Interesting…
so to make i’m understanding correctly… you have found it more efficient to have one screen with hundreds (thousands?!) of objects and implement code to show/hide the required screen components on this one screen as required?
Giving the user the experience of 50 screens where it’s just 1 screen with a 90+% of objects hidden at any given time…

I had the same problem, so I created a framework to manage all the screens I need.

Take a look at this thread,

and this repo,

I have run this complete application containing just about all the example LV applications, demo’s and tests as well as some other apps which demonstrate some of the more advanced features of the framework on an embedded system with recources similar to that of the ESP32. Would be happy to fill in the blanks if you have queries.

There are basically two main ways I can think of/have seen of structuring a LittlevGL application:

  1. Create every screen and object at the beginning, and hide everything you don’t need. This will give you high graphics performance, but will obviously use a lot of memory. There would be almost no delay while switching screens. (For my projects, I’ve generally used this approach, because I have plenty of memory available.)
  2. Dynamically create/remove a screen at a time. This approach works better in memory-constrained environments, but depending on the resources you have available, you might notice extra lag when switching screens. Obviously, the extent to which you have to apply this idea depends on the abilities of your target device.

It sounds like thus far you’ve been using Method 1. In this case, I would ask what constraints you are running up against. Are you running out of memory? Is the UI unresponsive?

2 Likes

Thanks for all your input . I’ve done some basic experimenting and here is what i have found…

I reduced from ten tiles (and 170 objects) in tileview to one screen (and 100 objects). I show and hide objects as needed dring navigation.

In my setup the I love how easy it is to share common page objects (headers, menu buttons etc).
I have noticed that in one of my more taxing searches my program can return more results without giving me a Guru Meditation Error. Which is also brilliant!!!

My only gripe is that with tileview a page navigation was totally smooth seamless and fast, with the “hide all objects then show some objects” method the screen refresh looks clunky. (see this video https://youtu.be/sheMPaYVPI0). It is subtle but as a user this jarring page refresh feels very different from the smoothness and tileview and makes the interface feel a bit yuck…

I’ve tried putting a small millis() delay between my hide and show to see if this helped but that doesn’t change it… both the hide and the show appear to update the screen in a non uniform way over a number of frames…

Anyone else experienced this?

Is there a way for me to make this smoother?
Could i “freeze” the screen, carry out my updates then have it all show in one single smooth update?

One possibility is that LittlevGL can now optimize how the screen is drawn with this new method and avoid redrawing unchanged areas. In your case, the display update appears to be slow enough that this ends up looking worse.

If you add lv_obj_invalidate(lv_scr_act()) in your page transition code this should force LittlevGL to redraw everything, but that might not look any more pleasant.

1 Like

Brilliant!
lv_obj_invalidate(lv_scr_act())
is exactly what i needed.

It’s back to being buttery smooth again.
Thanks so much!!

This exercise in memory management and object reuse has me wondering if creating just a few objects and then fully restyling them on each screen load is the a more memory efficient method than having many similar objects and hiding many of them…

For example, most of my pages have buttons, I could do this by just reusing the same button object on each screen, something like…

  //Create objects on startup
  ButtoneOne = lv_btn_create(lv_scr_act(), NULL);
  ButtoneOneLabel = lv_label_create(ButtoneOne, NULL);

voidNavigateToScreen1(){
  lv_obj_set_event_cb(ButtoneOne, event_handler_ClickNavigation);
  lv_obj_align(ButtoneOne, NULL, LV_ALIGN_IN_TOP_LEFT, 10, 90);
  lv_obj_set_size(ButtoneOne, 100, 100);
  lv_btn_set_style(ButtoneOne, LV_BTN_STYLE_REL, &NavButtonRel);
  lv_btn_set_style(ButtoneOne, LV_BTN_STYLE_PR, &NavButtonPr);
  lv_label_set_text(ButtoneOneLabel, "    " LV_SYMBOL_HOME "\nSearch");
lv_obj_set_hidden(ButtoneOne, false);
}

voidNavigateToScreen2(){
  lv_obj_set_event_cb(ButtoneOne, event_handler_SomewhereDifferent);
  lv_obj_align(ButtoneOne, NULL, LV_ALIGN_IN_TOP_LEFT, 122, 5);
  lv_obj_set_size(ButtoneOne, 50, 20);
  lv_btn_set_style(ButtoneOne, LV_BTN_STYLE_REL, &NavBrightButtonRel);
  lv_btn_set_style(ButtoneOne, LV_BTN_STYLE_PR, &NavBrightButtonPr);
  lv_label_set_text(ButtoneOneLabel, "    " LV_SYMBOL_SETTINGS "\nSettings");
lv_obj_set_hidden(ButtoneOne, false);
}

voidNavigateToScreen3(){
lv_obj_set_hidden(ButtoneOne, true);
}

This would be faster because you don’t have to tear down screens, but it takes more work to implement because you then have to make sure the right objects get modified.

If there’s still RAM left over and it’s fast enough right now, I would personally leave it the way it is.