Edit online

设计说明

Read time: 16 minute(s)
代码目录
luban-lite/packages/artinchip/lvgl-ui
├── aic_demo      // aic lvgl demo
├── lv_driver     // lvgl 显示和 2D 加速对接
├── lvgl          // lvgl 库
├── aic_ui.c      // aic demo 入口
├── aic_ui.h      // aic demo 头文件
├── lv_conf.h     // lvgl 配置文件
├── lv_demo.c     // lvgl demo 入口函数
└── SConscript

LVGL 整体流程

LVGL 的运行基于定时器 (Timer),系统需要给 LVGL 一个“心跳”,即定时调用 lv_tick_get() 函数来获取当前时间,才能正常运转。LVGL 运行流程中涉及以下关键函数:
  • lv_tick_get():获取以毫秒为单位的 tick 时间。

  • lv_timer_handler():在 while 循环中的基于定时器的任务处理

    函数 lv_task_handler() 会调用 lv_timer_handler()lv_tick_get() 决定了 lv_timer_handler() 基于定时器的任务处理的时间的准确性。


key_process1

1. 整体流程
lv_tick_get() 的实现代码
在文件 lv_hal_tick.c 中的 lv_tick_get() 的实现代码如下:
uint32_t lv_tick_get(void)
{
#if LV_TICK_CUSTOM == 0

    /*If `lv_tick_inc` is called from an interrupt while `sys_time` is read
    *the result might be corrupted.
    *This loop detects if `lv_tick_inc` was called while reading `sys_time`.
    *If `tick_irq_flag` was cleared in `lv_tick_inc` try to read again
    *until `tick_irq_flag` remains `1`.*/
    uint32_t result;
    do {
        tick_irq_flag = 1;
        result        = sys_time;
    } while(!tick_irq_flag); /*Continue until see a non interrupted cycle*/

    return result;
#else
    return LV_TICK_CUSTOM_SYS_TIME_EXPR;
#endif
}
在头文件 lv_rt_thread_conf.h 中定义了上述函数中的 LV_TICK_CUSTOM_SYS_TIME_EXPR
#define LV_TICK_CUSTOM 1
#define LV_TICK_CUSTOM_INCLUDE LV_RTTHREAD_INCLUDE
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (rt_tick_get_millisecond())    /*Expression evaluating to current system time in ms*/
创建 LVGL 线程的代码如下所示:
static void lvgl_thread_entry(void *parameter)
{
#if LV_USE_LOG
    lv_log_register_print_cb(lv_rt_log);
#endif /* LV_USE_LOG */
    lv_init();
    lv_port_disp_init();
    lv_port_indev_init();
    lv_user_gui_init();

    /* handle the tasks of LVGL */
    while(1)
    {
        lv_task_handler();
        rt_thread_mdelay(SLEEP_PERIOD);
    }
}

int lvgl_thread_init(void)
{
    rt_err_t err;

    err = rt_thread_init(&lvgl_thread, "LVGL", lvgl_thread_entry, RT_NULL,
        &lvgl_thread_stack[0], sizeof(lvgl_thread_stack), LPKG_LVGL_THREAD_PRIO, 0);
    if(err != RT_EOK)
    {
        LOG_E("Failed to create LVGL thread");
        return -1;
    }
    rt_thread_startup(&lvgl_thread);

    return 0;
}
LVGL 通过定时器机制来驱动其内部任务处理,用户只需实现下列显示、输入设备和 UI 界面相关的初始化函数,即可快速构建嵌入式系统的图形界面:
  • lv_port_disp_init():实现显示接口的对接以及硬件 2D 加速的对接

  • lv_port_indev_init():实现触摸屏的对接

  • lv_user_gui_init():适配不同 UI 界面的初始化

LVGL 层次结构


struct1.png

2. LVGL 层次结构
LVGL 层次结构主要包括以下几个部分:
  • Display:显示设备,是对显示驱动的封装和抽象。

  • Screen:屏幕对象,表示不同的界面。

  • Layer:图层,表示屏幕上的不同绘制区域。

  • 一个 display 包含多个 screen,每个 screen 又包含多个 layer。layer 按照层级关系叠加在一起,形成最终的显示效果。具体来说:
    • Active Screen:当前活动的屏幕,承载主要的应用程序界面,是所有操作的主要界面,处于最底层。

      一般在 Active Screen 实现不同的 app 界面,用户可以创建多个 screen,但只能有一个 screen 设置为 Active Screen

    • Top layer:位于 Active Screen 之上的图层,通常用来创建弹出窗口。

      Top layer 永远在 Active Screen 之上。

    • System layer:位于最顶层,通常用于显示系统级别的信息,比如鼠标可以在 System layer,永远不会被遮挡

为了更好地理解 LVGL 的层次结构,可以参考以下图层叠加的示意图:

layer1

3. 图层叠加

父子结构

父子结构的设计是 LVGL 面向对象的核心设计之一,使得图形界面的层次关系和布局管理变得非常直观和灵活。父子结构的特点如下所示:
  • 每一个对象都包含一个父对象,screen 对象除外。
  • 一个父对象可以包含任意数量的子对象。
  • 创建对象时需要传入父对象的指针,如果父对象为 NULL,则表示创建的是 screen 对象。
    lv_obj_create(NULL);
  • 父对象和子对象一起移动。
    父子对象一起移动

    move1

    4. 父子对象移动
  • 子对象超出父对象部分不可见。

    outside1

    5. 子对象可见区域
Edit online

显示对接

Read time: 16 minute(s)

LVGL 显示对接主要包括以下步骤和流程:

  1. 绘制 buffer 初始化,用于存储当前帧的图像数据,并在需要时进行交换。
  2. flush_cb 对接,刷新回调函数,用于将绘制缓冲区的数据发送到显示屏。刷新回调函数又分为以下两种模式:
    • 全刷新。
    • 局部刷新。
  3. 2D 硬件加速对接。

    为了提高绘图性能,可以将部分绘图操作进行硬件加速。

  4. 将上述所有配置注册到 LVGL 中。

绘制 buffer 初始化

在 LVGL(Light and Versatile Graphics Library)中,绘制缓冲区 (draw buffer)的初始化是显示设备配置的重要步骤之一。双缓冲模式可以提高图形界面的刷新效率和流畅度。绘制 buffer 初始化函数如下:
void lv_disp_draw_buf_init(lv_disp_draw_buf_t * draw_buf, void * buf1, void * buf2, uint32_t size_in_px_cnt)
  • buf1:无论选择单缓冲或多缓冲,必须设置此 buffer。

  • buf2:选择双缓冲时,需要配置此 buffer,单缓冲不需要配置。

  • size_in_px_cnt:以像素为单位的 buf 大小,应该根据实际的分辨率来计算。


double_frame1

6. 双缓冲

flush_cb 对接

以双缓冲为例,flush_cb 回调函数的处理流程分别如下所示,包括 full_refresh 和 direct_mode 两种绘制模式:

  1. full_refresh:即全刷新模式,每一帧都刷新整个显示屏。流程如下:
    1. 直接通过 pan_display 接口将当前绘制缓冲区 (draw buffer)发送到显示设备。
    2. 等待垂直同步 (vsync)中断,确保数据已经显示在屏幕上。
    3. 调用 lv_disp_flush_ready 通知 LVGL 框架刷新结束。
    4. LVGL 框架会自动交换绘制缓冲区。

    full_flush_cb1

    7. 全刷新模式下 flush_cb 回调函数的处理流程
  2. direct_mode:即局部刷新,每一帧只刷新需要更新的无效区域 (可以有多个无效区域)。流程如下:
    1. 遍历所有无效区域,例如 invalid area0invalid area1,将每个无效区域的数据从绘制缓冲区发送到显示设备。
    2. 最后一个无效区域处理完毕后,调用 lv_disp_flush_ready 通知 LVGL 框架刷新结束。
    3. LVGL 框架会自动交换绘制缓冲区。

    invalid_area1.png

    8. 无效区域

    direct_flush_cb1

    9. 局部刷新模式下 flush_cb 回调函数的处理流程
    flush_cb 的实现代码 fbdev_flush 如下:
    static void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t *color_p)
    {
        int index = 0;
        lv_disp_t * disp = _lv_refr_get_disp_refreshing();
        lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp);
    
        if (!disp->driver->direct_mode || draw_buf->flushing_last) {
            if (disp->driver->direct_mode)
                aicos_dcache_clean_invalid_range((unsigned long *)info.framebuffer, (unsigned long)info.smem_len * 2);
            else
                aicos_dcache_clean_invalid_range((unsigned long *)color_p, (unsigned long)info.smem_len);
    
            if ((void *)color_p == (void *)info.framebuffer)
                index = 0;
            else
                index = 1;
    
            mpp_fb_ioctl(g_fb, AICFB_PAN_DISPLAY , &index);
            mpp_fb_ioctl(g_fb, AICFB_WAIT_FOR_VSYNC, 0);
    
            if (drv->direct_mode == 1) {
                for (int i = 0; i < disp->inv_p; i++) {
                    if (disp->inv_area_joined[i] == 0) {
                        sync_disp_buf(drv, color_p, &disp->inv_areas[i]);
                    }
                }
            }
    
            lv_disp_flush_ready(drv);
        }
        else {
            lv_disp_flush_ready(drv);
        }
    }

2D 硬件加速对接

在 LVGL 中,硬件加速的实现可以显著提高图形绘制的性能。2D 加速主要对接 lv_draw_ctx_t 中的绘制函数,并对接相应的硬件加速接口。以下是详细的步骤:
  1. lv_draw_aic_ctx_t 结构体中包含 lv_draw_ctx_tblend 函数。

    lv_draw_aic_ctx_t 是一个包含了硬件加速功能的上下文结构体,继承自 lv_draw_sw_ctx_t 并添加了硬件加速所需的成员函数。关于详细绘制函数的说明,可查看 lv_draw_ctx_t 中的绘制函数

    typedef struct {
        lv_draw_ctx_t base_draw;
    
        /** Fill an area of the destination buffer with a color*/
        void (*blend)(lv_draw_ctx_t * draw_ctx, const lv_draw_sw_blend_dsc_t * dsc);
    } lv_draw_sw_ctx_t;
  2. 初始化 lv_draw_aic_ctx_t 硬件加速上下文结构体,并将软件实现的绘制函数替换为硬件加速版本的函数;
    void lv_draw_aic_ctx_init(lv_disp_drv_t * drv, lv_draw_ctx_t * draw_ctx)
    {
        lv_draw_sw_init_ctx(drv, draw_ctx);
        lv_draw_aic_ctx_t * aic_draw_ctx = (lv_draw_aic_ctx_t *)draw_ctx;
    
        aic_draw_ctx->blend = lv_draw_aic_blend;
        aic_draw_ctx->base_draw.draw_img = lv_draw_aic_draw_img;
        aic_draw_ctx->base_draw.draw_img_decoded = lv_draw_aic_img_decoded;
    
        return;
    }

    draw_rect、draw_line 等接口操作的功能由多个步骤组成,虽然未进行硬件加速,但是操作的部分实现会调用到 blend,而 blend 接口进行了硬件加速对接。

  3. 实现硬件加速的 blend 函数,该函数会调用底层硬件加速 API 来实现高效的混合操作。

    先调用 lv_draw_sw_init_ctx 函数把所有绘制操作都初始化为软件实现,对可以硬件加速的接口重新实现, 覆盖原来的软件实现。

  4. 继续实现其它硬件加速的绘制函数,例如 draw_imgdraw_img_decoded
1. lv_draw_ctx_t 中的绘制函数

成员

说明

是否硬件加速

void *buf

当前要绘制的 buffer

-

const lv_area_t * clip_area

绘制区域裁剪 (以屏幕为参考的绝对坐标)

-

void (*draw_rect)()

绘制矩形 (包括圆角、阴影、渐变等)

void (*draw_arc)()

绘制弧形

void (*draw_img_decoded)()

绘制已经解码后的图像

lv_res_t (*draw_img)()

绘制图像 (包括图片解码)

void (*draw_letter)()

绘制文字

void (*draw_line)()

绘制直线

void (*draw_polygon)()

绘制多边形

显示驱动注册

  1. 通过 lv_disp_drv_init 来初始化 lv_disp_drv_t 结构体。
    注: lv_disp_drv_t包含了所有与显示相关的配置信息,详情可查看lv_disp_drv_t 结构体代码
  2. 通过 lv_disp_draw_buf_init 初始化绘制 buffer。
  3. 通过回调 flush_cb 来注册显示接口。
  4. 通过 lv_draw_aic_ctx_init 来注册 2D 硬件加速相关接口。
  5. 通过 lv_disp_drv_register 来注册 lv_disp_drv_t。
上述代码中表示目前是局部刷新模式:
disp_drv.full_refresh = 0;
disp_drv.direct_mode = 1;
全刷新模式参数配置如下:
disp_drv.full_refresh = 1;
disp_drv.direct_mode = 0;
lv_disp_drv_t 结构体代码示例如下所示:
static lv_disp_drv_t disp_drv;
void lv_port_disp_init(void)
{
    void *buf1 = RT_NULL;
    void *buf2 = RT_NULL;
    uint32_t fb_Size;
    rt_err_t result;

    g_fb = mpp_fb_open();
    if (g_fb == 0) {
        LOG_E("can't find aic framebuffer device!");
        return;
    }

    result = mpp_fb_ioctl(g_fb, AICFB_GET_SCREENINFO, &info);
    if (result != RT_EOK) {
        LOG_E("get device fb info failed!");
        return;
    }

    g_ge = mpp_ge_open();
    if (!g_ge) {
        LOG_E("ge open fail\n");
        return;
    }

    fb_Size = info.height * info.stride;
    buf1 = (void *)info.framebuffer;
    buf2 = (void *)((uint8_t *)info.framebuffer + fb_Size);
    lv_disp_draw_buf_init(&disp_buf, buf2, buf1,
                        info.width * info.height);
    lv_disp_drv_init(&disp_drv);

    /*Set a display buffer*/
    disp_drv.draw_buf = &disp_buf;

    /*Set the resolution of the display*/
    disp_drv.hor_res = info.width;
    disp_drv.ver_res = info.height;
    disp_drv.full_refresh = 0;
    disp_drv.direct_mode = 1;
    disp_drv.flush_cb = fbdev_flush;
    disp_drv.draw_ctx_init = lv_draw_aic_ctx_init;
    disp_drv.draw_ctx_deinit = lv_draw_aic_ctx_deinit;
    disp_drv.draw_ctx_size = sizeof(lv_draw_aic_ctx_t);

    /*Finally register the driver*/
    lv_disp_drv_register(&disp_drv);
}
Edit online

硬件解码对接

Read time: 16 minute(s)

通过 lv_img_decoder_t 可以注册硬件解码器接口,从而提升图像加载和渲染的效率。

注册解码器过程如下所示;
  1. 定义一个解码器结构体,并初始化:
    void aic_dec_create()
    {
        lv_img_decoder_t *aic_dec = lv_img_decoder_create();
    
        /* init frame info lists */
        mpp_list_init(&buf_list);
        lv_img_decoder_set_info_cb(aic_dec, aic_decoder_info);
        lv_img_decoder_set_open_cb(aic_dec, aic_decoder_open);
        lv_img_decoder_set_close_cb(aic_dec, aic_decoder_close);
    }
  2. 实现 aic_decoder_info 回调函数,用于获取图片的宽度、高度和格式信息。
  3. 实现 aic_decoder_open 回调函数,用于申请解码输出 buffer。
  4. 实现 aic_decoder_close 回调函数,用于释放硬件解码资源。
  5. 在绘制函数 draw_img_decoded中,通过注册解码器回调去获取需要的解码后数据,默认的图片处理流程如下所示:

    draw_img_decoded

    10. draw_img_decoded
    • 采用此流程需要额外申请一块解码 buffer,占用内存增加。

    • 缓存解码后的 buffer,下次再显示同样的 image,不用重复解码,加快 UI 加载速度。

    当绘制函数为 draw_img 时,硬件解码在函数 draw_img 内部,无需注册解码回调函数,默认不采用此方法。在内存受限的场景下,可以评估此方法是否可满足场景需求。


    draw_img

    11. draw_img
    • 采用此流程无需额外申请解码 buffer,直接解码到绘制 buffer。

    • 当需要进行 alpha blending 的时候,此方法不可行。

    • 每次都要重新对 image 解码,速度不如 draw_img_decoded。

    • 当硬件解码不支持裁剪的时进行局部更新,此方法不可行。

  6. 通过 lv_img_cache_set_size 设置缓存的图片数量,以优化 UI 流畅性。

    如果图片的读取时间或者解码时间比较长,采用图片缓存机制可以提升 UI 流畅性:

    1. 打开或关闭图片缓冲机制
      lv_conf.h 文件中,采用 lv_img_decoder_t 提供的接口注册的解码器,控制是否启用 LVGL 内部的图片缓冲机制:
      • 宏定义 LV_IMG_CACHE_DEF_SIZE 为 1 表示打开图片缓冲机制。
      • 宏定义 LV_IMG_CACHE_DEF_SIZE 为 0 表示关闭图片缓冲机制。
    2. 设置缓存的图片张数

      通过 void lv_img_cache_set_size(uint16_t entry_cnt) 来设置缓冲的图片张数

      图以张数为单位进行缓存。例如,设置缓存的图片张数为 10。

    3. 图片缓存价值判断

      当图片缓存到设置的最大张数时,如果需要新的缓存,图片缓存机制内部会进行图片缓存价值的判断。 例如,某张图片解码的时间比较久或使用频繁高,则其缓存价值打分会更高,需要优先缓存这类图片。

2. lv_img_decoder_t 注册接口说明

函数

说明

aic_decoder_info

获取图片宽、高、图片格式信息

aic_decoder_open

申请解码输出 buffer,硬件解码输出

aic_decoder_close

释放硬件解码资源 (包括输出 buffer)

Edit online

第三方 FreeType 库支持

Read time: 16 minute(s)

如需在 LVGL 中使用 FreeType 字体,执行以下配置流程:

  1. 使用 scons --menuconfigme 进入配置菜单,选中 freetype 包:
      Local packages options  --->
        Third-party packages options  --->
        [*] freetype  ---
  2. 打开头文件 luban-lite/packages/artinchip/lvgl-ui/lv_conf.h ,并定义宏 LV_USE_FREETYPE
    #define LV_USE_FREETYPE 1
  3. 编写 FreeType 字体调用代码,并将其添加在项目中。
    FreeType 字体调用示例如下:
    void lv_example_freetype_1(void)
    {
        /*Create a font*/
        static lv_ft_info_t info;
        /*FreeType uses C standard file system, so no driver letter is required.*/
        // 需要设置字库文件在系统目录下的路径
        info.name = "/rodata/lvgl_data/font/Lato-Regular.ttf";
        info.weight = 24;
        info.style = FT_FONT_STYLE_NORMAL;
        info.mem = NULL;
        if(!lv_ft_font_init(&info)) {
            LV_LOG_ERROR("create failed.");
        }
    
        /*Create style with the new font*/
        static lv_style_t style;
        lv_style_init(&style);
        lv_style_set_text_font(&style, info.font);
        lv_style_set_text_align(&style, LV_TEXT_ALIGN_CENTER);
    
        /*Create a label with the new style*/
        lv_obj_t * label = lv_label_create(lv_scr_act());
        lv_obj_add_style(label, &style, 0); // 添加字库 style 到 label 控件
        lv_label_set_text(label, "Hello world\nI'm a font created with FreeType");
        lv_obj_center(label);
    }
  4. 编译项目。

    确保所有依赖项都已正确安装。编译完成后,可在屏幕上看到使用 FreeType 字体渲染的文本。

Edit online

LVGL Demo

Read time: 16 minute(s)
LVGL demo 示例目前支持下列常见的示例:
  • Base Demo:LVGL 的基础演示示例,展示了如何使用 LVGL 创建基本的 UI 元素,通常用于展示 LVGL 的基本功能和使用方法,包括:
    • 创建按钮并响应点击事件。
    • 显示文本标签。
    • 使用滑块调整数值。
    • 简单的动画效果。
  • Meter Demo:演示如何使用 LVGL 创建仪表盘 (meter), 常用于需要显示数据变化的场景,如速度表、温度计等。功能包括:
    • 创建仪表盘并添加指针。
    • 动态更新指针位置。
    • 显示刻度和标签。
    • 支持多种样式和颜色。
  • Widgets demo:展示 LVGL 提供的各种 UI 控件,可以帮助开发者了解如何使用不同的控件来构建复杂的用户界面。UI 控件包括:
    • 列表视图。
    • 下拉菜单。
    • 复选框和单选按钮。
    • 开关按钮。
    • 日期选择器。
  • Benchmark demo:展示 LVGL 在不同硬件平台上的表现,用于测试和评估 LVGL 的性能,包括渲染速度、内存占用指标以及帧率统计。

本节主要介绍 Base DemoMeter Demo

Base Demo

Base Demo 演示了如何使用 LVGL 创建基本的 UI 元素,包括 PNG 和 JPG 图片的硬件解码,内置图片的使用方式以及通过滑动操作切换页面。UI 界面示例如下:

base_demo1

12. Base Demo 界面示例
Base demo 一共有四个页面, 分别为仪表演示、音乐播放演示、菜单演示以及播放器演示。播放器演示页面需要打开 base_ui.c 中的宏定义 VIDEO_PLAYER。以下是 base demo 页面的基本布局和特性说明:
  1. 每个页面都可以通过滑动操作切换。页面滑动使用了 tabview 控件,每个页面都是一个 tab:
    lv_obj_set_size(main_tabview, 1024, 600);
    lv_obj_set_pos(main_tabview, 0, 0);
    lv_obj_set_style_bg_opa(main_tabview, LV_OPA_0, 0);
    
    lv_obj_t *main_tab0 = lv_tabview_add_tab(main_tabview, "main page 0");
    lv_obj_t *main_tab1 = lv_tabview_add_tab(main_tabview, "main page 1");
    
    lv_obj_set_style_bg_opa(main_tab0, LV_OPA_0, 0);
    lv_obj_set_style_bg_opa(main_tab1, LV_OPA_0, 0);
    lv_obj_set_size(main_tab0, 1024, 600);
    lv_obj_set_size(main_tab1, 1024, 600);
    
    lv_obj_set_pos(main_tab0, 0, 0);
    lv_obj_set_pos(main_tab1, 0, 0);
  2. 背景图片通过 image 控件来创建,是一个名字为 global_bg 的 PNG 图片。此图片会采用注册的硬件解码器进行解码:
    static lv_obj_t *img_bg = NULL;
    img_bg = lv_img_create(lv_scr_act());
    lv_img_set_src(img_bg, LVGL_PATH(global_bg));
    lv_obj_set_pos(img_bg, 0, 0);
  3. 菜单图片也通过 image 控件来创建,也是一个 PNG 图片,且此图片也会采用注册的硬件解码器进行解码:
    lv_obj_t *sub_image00 = lv_img_create(sub_tab0);
    lv_img_set_src(sub_image00, LVGL_PATH(cook_0.jpg));
    lv_obj_set_pos(sub_image00, 36, 100);
  4. Fake image
    Fake image 不是一个真实的图片,通过此方式可以方便的对一个矩形区域进行填充,包括 alpha、red、green、blue:
    static lv_obj_t *img_bg = NULL;
    FAKE_IMAGE_DECLARE(bg_dark)  // 声明 (bg_dark 名字可修改)
    
    /* 最后一个参数为要设置的颜色值:bit31:24 为 alpha */
    FAKE_IMAGE_INIT(bg_dark, 1024, 600, 0, 0x00000000);
    
    lv_img_set_src(img_bg, FAKE_IMAGE_NAME(bg_dark)); // 设置 fake image 数据源
  5. Build-in image 是通过数组变量在程序中表示图像。
    图片转换成 .c 文件的工具,参考官网:http://lvgl.io/tools/imageconverter
    uint8_t circle_white_map[] = {
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x50, 0xff, 0x7f, 0xff,
            ........................................................};
    
    const lv_img_dsc_t circle_white = {
        .header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA,
        .header.always_zero = 0,
        .header.reserved = 0,
        .header.w = 20,
        .header.h = 20,
        .data_size = 400 * LV_IMG_PX_SIZE_ALPHA_BYTE,
        .data = circle_white_map,
    };
    
    static lv_obj_t * circle_0 = lv_img_create(img_bg);
    lv_img_set_src(circle_0, &circle_white);
    lv_obj_align(circle_0, LV_ALIGN_BOTTOM_MID, -16, -28);

Meter Demo

Meter demo 演示了硬件旋转,以及仪表盘的设计参考。


meter_demo

13. Meter Demo 界面示例

Meter demo 的详细说明及特点如下:

  1. 各种控件的动作通过 timer 来实现,每间隔一定的时间执行相应的回调函数:
    lv_timer_create(point_callback, 10, 0);
    lv_timer_create(fps_callback, 1000, 0);
    lv_timer_create(speed_callback, 100, 0);
    lv_timer_create(time_callback, 1000 * 60, 0);
  2. 指针和光影通过贴图和硬件任意角度旋转来实现。其中,前 74 张指针通过切换 74 张图片来实现, 从第 75 张开始的红色指针,采用硬件任意角度旋转来实现:
    static void point_callback(lv_timer_t *tmr)
    {
        char data_str[64];
        (void)tmr;
    
        static bool first = true;
        static int id = 1;
        static int direct = 0;
        static int mode_id = 0;
        static int mode_num = sizeof(rot_mode_list) / sizeof(rot_mode_list[0]);
        static int start_id = 0;
        static int end_id = 0;
    
        if (first) {
            first = false;
            start_id = rot_mode_list[mode_id].start_id;
            end_id = rot_mode_list[mode_id].end_id;
        }
    
        direct = start_id < end_id ? 0: 1;
    
        if (id < 75) {
        lv_img_set_src(img_circle, LVGL_PATH(bg/small_blue));
        lv_obj_clear_flag(img_circle, LV_OBJ_FLAG_HIDDEN);
        } else {
        lv_obj_add_flag(img_circle, LV_OBJ_FLAG_HIDDEN);
        }
    
        if (id < 75) {
            sprintf(data_str, "%spoint/point_%05d", LVGL_DIR, id);
            lv_img_set_src(img_point, data_str);
            lv_img_set_angle(img_point, 0);
    
        } else {
            // id to angle
            float rot_angle = ((float)(id - 75) * 2 * 10) * 0.84;
            sprintf(data_str, "%spoint/point_%05d", LVGL_DIR, 75);
            lv_img_set_src(img_point, data_str);
            lv_img_set_pivot(img_point, 210, 210);
            lv_img_set_angle(img_point, (int16_t)rot_angle);
        }
    
        if (direct == 0) {
            id++;
        } else {
            id--;
        }
    
        if ((!direct && (id > end_id) ) ||
            (direct && (id < end_id))) {
            id = end_id;
            mode_id++;
            mode_id %= mode_num;
            start_id = rot_mode_list[mode_id].start_id;
            end_id = rot_mode_list[mode_id].end_id;
        }
    
        return;
    }
  3. UI 设计方案比较。

    针对本 demo 场景,有以下 UI 设计方案可供选择。其中 UI 设计方案 2 更高效,实现同样的界面效果,简化流程速度可以提升一倍以上:

    1. UI 设计方案 1

      meter_draw_1

      14. UI 设计方案 1 示例
      • 准备光条、指针、光圈、底图四张图片。

      • 按照先后顺序,依次对光圈和背景、光条和背景以及指针和背景进行 alpha blending。

      • 每一个角度的旋转都需要进行三次 alpha blending。

    2. UI 设计方案 2

      meter_draw_2

      15. UI 设计方案 2 示例
      • 光条和指针合并为一张图片。

      • 光圈合并到背景图中。

      • 准备光条和指针图片和底图。

      • 每一个角度的旋转都只需要对光条和指针图片与背景图进行一次 alpha blending。