Rockchip · AI & Linux Hub

显示与音频

Rockchip Linux LVGL 与 OpenGL(ES) Gallery

Rockchip Linux LVGL 与 OpenGL(ES) Gallery — V1.2.1

· ArmSoM Editorial · Rockchip source documentation · 12 分钟阅读

ArmSoM 平台参考,依据对应中英文 Rockchip 原稿;具体板卡、SoC、SDK/BSP、内核及外设需逐项匹配,不代表所有 ArmSoM 板卡支持。原稿示例和测试结果不是 ArmSoM 实测,未执行命令或硬件验证。

对应 Buildroot LVGL 8.4.x / 9.1.x 与 Rockchip lv_gl 扩展;示例含原稿有意省略部分。

中英 V1.2.1;保留全部7章、API参数及版本9的lv_drivers合并脚注。中文13图取中文原稿,英俄德13图取英文原稿。原稿动画示例有意省略代码,不能当成完整可编译程序;透明度与可见度用语有混用,原意和疑点注明,代码不改。

系统 版本
Buildroot 8.4.x
Buildroot 9.1.x

1. LVGL 简介

LVGL 是一个较为流行的免费和开源的嵌入式图形库,可以在MCU、MPU以及各种屏幕上绘制美观的UI。LVGL 是完全开源的,除了部分扩展功能外没有外部依赖,这使得它的移植非常简单。它可与任何现代 MCU 或 MPU配合使用,并可与任何 (RT)OS 或裸机设置一起使用,以驱动电子纸、单色、OLED 或 TFT 显示器,甚至监视器。因此,甚至可以在Windows、Linux上调试UI界面,有一些基础的平台无关的UI调试,反复编译烧录固件是十分繁琐的,这时就可以在PC上搭建环境调试,再拷⻉到嵌入式设备上编译运行。

2. LVGL 源码

在通用Linux SDK中,LVGL有三个源码仓库,分别为lvgl,lv_drivers 1 ,lvgl_demo。其中lvgl仓库为LVGL框架源码,lv_drivers为一些比较常用的显示接口,比如drm,sdl,wayland等,lvgl_demo则是由RK编写的一些应用示例,比如如何调用一些基本的初始化,如何将官方的DEMO运行起来等。其中lvgl与lv_drivers源码均通过官方github仓库下载,并打上RK提供的一些补丁,详⻅与。lvgl_demo源码则在同步SDK时下buildroot/package/lvgl/lvgl/buildroot/package/lvgl/lv_drivers/载,路径为 。<SDK>/app/lvgl_demo

3. LVGL 配置

LVGL的配置主要区别在于渲染后端的选择,目前可选DRM直接送显,通过SDL送显,以及通过RKADK送显。其中DRM配置使用在没有GPU或RGA的平台如RK3308等,SDL配置使用在有GPU的平台如RK3568等,RKADK使用在有GPU或RGA的平台。

3.1 配置文件说明

常用的配置已保存在 目录下。<SDK>/buildroot/configs/rockchip/gui/lvgl/

<SDK>/buildroot/configs/rockchip/gui/lvgl/
├── lvgl_drm.config     # 软链接V8版本配置,兼容旧⽂件路径
├── lvgl_rkadk.config   # 软链接V8版本配置,兼容旧⽂件路径
├── lvgl_sdl.config     # 软链接V8版本配置,兼容旧⽂件路径
├── music.config            # LVGL官⽅⾳乐播放器⽰例
├── rk_demo.config          # Rockchip家电显控⽰例
├── v8                      # V8版本配置⽂件
│   ├── base.config         # 基础配置⽂件,配置LVGL的特性,功能等
│   ├── lvgl_drm.config     # DRM配置⽂件,使⽤DRM送显
│   ├── lvgl_rkadk.config   # RKADK配置⽂件,使⽤RKADK送显
│   └── lvgl_sdl.config     # SDL配置⽂件,使⽤SDL送显
├── v9                      # V9版本配置⽂件
│   ├── base.config
│   ├── lvgl_drm.config
│   ├── lvgl_rkadk.config
│   └── lvgl_sdl.config
└── widgets.config          # LVGL官⽅组件⽰例

3.2 配置文件使用说明

在板级配置文件中引用LVGL配置文件即可,以RK3308为例:

$ cat buildroot/configs/rockchip_rk3308_bs_32_ia_release_defconfig | grep lvgl
# 选择后端
#include "gui/lvgl/lvgl_drm.config"
# 选择应⽤
#include "gui/lvgl/rk_demo.config"

3.3 SDL配置补充说明

如果选择了SDL配置文件,则还需要手动开启SDL后端选项:

BR2_PACKAGE_SDL2=y
BR2_PACKAGE_SDL2_OPENGLES=y
# 如果是在WAYLAND环境下,则开启WAYLAND后端。依据具体环境选择
BR2_PACKAGE_SDL2_WAYLAND=y

在SDL配置环境下,如果需要额外的OpenGL(ES)接口支持,则可以开启:

BR2_PACKAGE_LV_DRIVERS_USE_OPENGL=y

4. LVGL 编译

cd buildroot
source envsetup.sh
# 选择板⼦对应配置
# V8 版本编译如下三个包
make lvgl lv_drivers lvgl_demo -j20
# V9 版本编译如下两个包
make lvgl lvgl_demo -j20

需要注意,如果进入buildroot menuconfig修改了LVGL相关的配置,建议将每个仓库都重新编译一遍,避免配置不一致导致链接或运行出错,如:

cd buildroot
# V8 版本编译如下三个包
make lvgl-reconfigure lv_drivers-reconfigure lvgl_demo-reconfigure -j20
# V9 版本编译如下两个包
make lvgl-reconfigure lvgl_demo-reconfigure -j20

5. LVGL_DEMO 代码说明

源码路径为 。<SDK>/app/lvgl_demo/

5.1 目录结构

app/lvgl_demo/
├── cJSON   # cJSON源码
├── common  # 通⽤组件
├── gallery # 图库应⽤
├── lv_demo # 基础⽰例程序,运⾏官⽅DEMO
├── lvgl8   # V8版本接⼝适配,包含显⽰,触摸等
├── lvgl9   # V9版本接⼝适配,包含显⽰,触摸等
├── rk_demo
│   ├── asr                 # ASR应⽤
│   ├── furniture_control   # 家电显控
│   ├── home                # DEMO⾸⻚
│   ├── include             # rkwifibt等头⽂件
│   ├── intercom_homepage   # 楼宇对讲
│   │   ├── intercom_call   # 对讲呼叫
│   │   └── video_monitor   # RTSP码流播放
│   ├── layout              # 布局组件
│   ├── resource            # 图⽚、字体资源
│   ├── rockit              # rockit相关适配
│   ├── setting             # 系统设置
│   ├── smart_home          # 智能家居
│   └── wifibt              # rkwifibt相关适配
├── sys     # 时间戳,soc信息,cpu信息等接⼝
└── tools   # 代码⻛格格式化⼯具

5.2 lv_demo 代码说明

源码路径为,主要作为一个示例程序,演示如何将官方的DEMO运行起来。以下说<SDK>/app/lvgl_demo/lv_demo明略过一些无关的代码,仅挑选需要关注的代码进行说明。

/* <SDK>/app/lvgl_demo/lv_demo/main.c */
...
int main(int argc, char **argv)
{
    lv_port_init();

    /* 根据配置选择对应的DEMO初始化,绘制对应UI */
#if LV_USE_DEMO_WIDGETS
    lv_demo_widgets();
#elif LV_USE_DEMO_BENCHMARK
    lv_demo_benchmark();
#elif LV_USE_DEMO_MUSIC
    lv_demo_music();
#endif
    while (!quit)
    {
        /* 调⽤LVGL任务处理函数,LVGL所有的事件、绘制、送显等都在该接⼝内完成 */
        lv_task_handler();
        usleep(100);
    }

    return 0;
}
/* <SDK>/app/lvgl_demo/lvgl8/lv_port_init.c */
/* 触摸旋转 */
/* 0, 90, 180, 270 */
static int g_indev_rotation = 0;
/* 显⽰旋转 */
/* 0, 90, 180, 270 */
static int g_disp_rotation = 0;

void lv_port_init(void)
{
    /* LVGL框架初始化 */
    lv_init();

    /* 显⽰初始化 */
    lv_port_disp_init(0, 0, g_disp_rotation);
    /* 触摸初始化 */
    lv_port_indev_init(g_indev_rotation);
}
/* <SDK>/app/lvgl_demo/lvgl9/lv_port_init.c */
/* 触摸旋转 */
/* 0, 90, 180, 270 */
static int g_indev_rotation = 0;
/* 显⽰旋转 */
/* 0, 90, 180, 270 */
static int g_disp_rotation = 0;
/* ⽇志等级 */
static int log_level = LV_LOG_LEVEL_WARN;
...
void lv_port_init(void)
{
    const char *buf;

    /* LVGL框架初始化 */
    lv_init();

    /* 通过环境变量获取⽇志等级 */
    buf = getenv("LV_DEBUG");
    if (buf)
        log_level = buf[0] - '0';
    /* 设置⽇志回调 */
    lv_log_register_print_cb(print_cb);
    /* 设置系统时钟回调 */
    lv_tick_set_cb(lv_systick);
    /* 设置延时回调 */
    lv_delay_set_cb(lv_sysdelay);

    /* 显⽰初始化 */
    lv_port_disp_init(0, 0, g_disp_rotation);
    /* 触摸初始化 */
    lv_port_indev_init(g_indev_rotation);
}

6. OpenGL(ES)接口说明

由于LVGL本身不支持OpenGL(ES)接口,且SDL不支持3D对象,因此针对一些需要使用到相关功能的应用,封装出一套lv_gl接口,使LVGL应用可以在原有的UI应用上直接叠加上3D图像效果。

6.1 文件说明

lv_drivers/sdl/gl/
├── gl.c                // 接⼝实现
├── gl.h                // 接⼝及外部结构体定义
├── mat.c               // 矩阵函数实现
├── mat.h               // 矩阵函数定义
└── shaders             // OpenGL着⾊器
    ├── bgra_cube.frag  // BGRA格式⽴⽅体⽚段着⾊器
    ├── bgra.frag       // BGRA格式图⽚⽚段着⾊器
    ├── common.vert     // 通⽤顶点着⾊器,包含旋转矩阵、缩放矩阵等
    ├── cube.vert       // ⽴⽅体顶点着⾊器
    ├── default.vert    // 图⽚顶点着⾊器
    ├── rgba_cube.frag  // RGBA格式⽴⽅体⽚段着⾊器
    └── rgba.frag       // RGBA格式图⽚⽚段着⾊器

6.2 枚举说明

6.2.1 颜色格式

目前支持的是LV_GL_FMT_RGBA和LV_GL_FMT_BGRA,后续考虑加入YUV格式支持。

enum {
    LV_GL_FMT_ALPHA,
    LV_GL_FMT_LUMINANCE,
    LV_GL_FMT_LUMINANCE_ALPHA,
    LV_GL_FMT_INTENSITY,
    LV_GL_FMT_RGB,
    LV_GL_FMT_RGBA,
    LV_GL_FMT_BGRA,
};

6.2.2 纹理类型

用于区分平面图形和立方体图形,使用不同的渲染方式

enum {
    GL_TEX_TYPE_2D,
    GL_TEX_TYPE_CUBE,
};

6.2.3 立方体平面

用于选择立方体平面,在导入图像,或使用立方体对象作为渲染目标时选择目标面。

enum {
    CUBE_LEFT,
    CUBE_RIGHT,
    CUBE_TOP,
    CUBE_BOTTOM,
    CUBE_FRONT,
    CUBE_BACK,
};

在OpenGL的坐标中,屏幕中间为原点(0, 0),X坐标向右为正,Y坐标向上为正,Z坐标垂直于屏幕向外为正,因此,当一个立方体被创建,无旋转的情况下,面向用戶的方向为正(CUBE_FRONT),其余方向类推,如下图所示(虚线表示不可⻅部分):

中文原稿技术配图

6.3 结构体说明

应用上使用,主要关注lv_gl_img_t,lv_gl_tex_t,lv_gl_obj_t三个结构体。lv_gl_img_t用于将外部图片导入为GPU纹理对象

typedef struct {
    /* 图⽚像素数据地址 */
    const void *pixels;
    /* 图⽚格式 */
    int format;
    /* 宽 */
    int w;
    /* ⾼ */
    int h;
} lv_gl_img_t;

lv_gl_tex_t储存GPU纹理对象及相关信息

typedef struct {
    /* 宽 */
    int w;
    /* ⾼ */
    int h;
} lv_gl_size_t;

typedef struct {
    /* GPU纹理对象 */
    GLuint gl_tex;
    /* GPU FBO帧缓冲对象,⽤于将该纹理作为FB时使⽤ */
    GLuint FBO;
    /* 纹理⼤⼩,普通2D纹理仅有第⼀组size有效,⽴⽅体有6个⾯则有6组size信息 */
    lv_gl_size_t size[6];
    /* 纹理格式 */
    int format;
    /* 纹理类型,⽤于区分2D和⽴⽅体 */
    int type;
    /* 引⽤计数 */
    int ref_cnt;
} lv_gl_tex_t;

lv_gl_obj_t储存GPU渲染对象及相关信息,如顶点、变换参数等

typedef struct {
    float x, y, z;
} lv_gl_vec_t;

typedef struct {
    /* 宽 */
    int w;
    /* ⾼ */
    int h;
    /* Y⽅向纹理翻转,应⽤可以忽略,按默认0即可 */
    int reverse_y;
    /* 绑定的纹理 */
    lv_gl_tex_t *tex;
    /* 渲染区域(位置及⼤⼩) */
    SDL_Rect r;
    /* 视图区域(位置及⼤⼩) */
    SDL_Rect view;
    /* 裁剪区域(位置及⼤⼩) */
    SDL_Rect crop;
    /* 视图区域需要更新,内部使⽤,应⽤可忽略 */
    int view_dirty;
    /* 裁剪区域需要更新,内部使⽤,应⽤可忽略 */
    int crop_dirty;
    /* 裁剪区域使能 */
    int crop_en;
} lv_gl_base_t;

typedef struct {
    lv_gl_base_t base;
    /* GPU VAO,顶点数组对象,会将VBO及⼀些属性配置缓存,⽅便快速切换 */
    GLuint VAO;
    /* GPU VBO,顶点缓冲对象,存储坐标、纹理顶点信息 */
    GLuint VBO;
    /* 输出⽬标,默认GL_TEXTURE_2D,如果需要在⽴⽅体上渲染,则通过该变量指定渲染的⾯ */
    int out_type;
    /*
     * ⼿动指定2D对象的四个顶点坐标
     * 默认顶点为全屏(注意是framebuffer的视图⼤⼩,⽽不是屏幕的全屏,两者可以不相等)
     * 再通过设置scale, offset等属性进⾏矩阵变换。也可以通过该属性⾃由指定顶点坐标
     * 但不建议两种⽅式共同使⽤,涉及的变换⽐较复杂,⼈⼒计算不好估算最终的效果
     * x, y, z范围均为[-1, 1]
     */
    lv_gl_vec_t p[4];
    /* ⼿动指定纹理的引⽤范围,在只需要使⽤到纹理的部分切⽚时使⽤ */
    SDL_Rect tp;

    /* 全局透明度 */
    float alpha;
    /*
     * 以下变换的顺序为:
     * 默认顶点坐标pos * offset * scale * self_rot * view_rot * perspective * move
     * 从左到右依次计算,其中perspective为视图转换,实现近⼤远⼩效果
     */
    /* 三轴⽅向的缩放 */
    lv_gl_vec_t scale;
    /* 三轴⽅向的偏移 */
    lv_gl_vec_t offset;
    /* 三轴⽅向的旋转 */
    lv_gl_vec_t self_rot;
    /* 三轴⽅向的视⻆旋转 */
    lv_gl_vec_t view_rot;
    /* 三轴⽅向的位移 */
    lv_gl_vec_t move;
} lv_gl_obj_t;

6.4 函数说明

void lv_gl_set_render_cb(lv_gl_render_cb_t cb);

注册应用层渲染回调,对象的渲染只能在回调里完成cb:渲染回调函数指针

void lv_gl_set_fb(lv_gl_obj_t *obj);

设置framebufferobj:不为空时则后续的渲染会将图像合成在该对象的纹理内存中,为空则表示恢复到默认的framebuffer,直接渲染到屏幕上

void lv_gl_read_pixels(void *ptr, SDL_Rect *r, int type);

将当前的framebuffer的指定区域拷⻉到内存中ptr:目标内存r:目标区域,起始点及大小type:目标类型,传0则默认GL_TEXTURE_2D,如果需要读取立方体的面,则通过该变量指定

lv_gl_tex_t *lv_gl_tex_create(int type, int w, int h, lv_gl_img_t *img);

创建纹理对象type:纹理类型,GL_TEX_TYPE_2D或GL_TEX_TYPE_CUBEw:纹理宽度,img不为空时会使用img中的宽度值,该值被忽略h:纹理高度,img不为空时会使用img中的高度值,该值被忽略

img:导入的图像,为空时则会只会在GPU内申请对应空间(默认值全为0.0),不为空时则会将对应图像也导入到GPU内。如果纹理类型为GL_TEX_TYPE_CUBE,则该参数需要传入一个大小至少为6的数组返回值:lv_gl_tex_t *,纹理对象指针

void lv_gl_tex_del(lv_gl_tex_t *tex);

删除纹理对象tex:纹理对象指针

void lv_gl_tex_import_img(lv_gl_tex_t *tex, lv_gl_img_t *img);

纹理对象导入外部图像,如果纹理对象在创建时已经传入过图像指针,则该接口不需要再次被调用。如果需要更换图像,或者创建时仅创建空内存,则使用该接口导入图像tex:纹理对象指针img:导入的图像地址。如果纹理类型为GL_TEX_TYPE_CUBE,则该参数需要传入一个大小至少为6的数组

void lv_gl_tex_clear(lv_gl_tex_t *tex, float r, float g, float b, float a);

将纹理对象使用指定颜色填充tex:纹理对象指针r:红色通道g:绿色通道b:蓝色通道a:透明度通道

lv_gl_obj_t *lv_gl_obj_create(int w, int h);

创建渲染对象。创建后的对象无法直接渲染,需要绑定纹理后才可渲染w:宽度,用于缩放计算h:高度,用于缩放计算返回值:lv_gl_obj_t *,渲染对象指针

void lv_gl_obj_del(lv_gl_obj_t *obj);

删除渲染对象obj:渲染对象

void lv_gl_obj_resize(lv_gl_obj_t *obj, lv_gl_obj_t *parent);

根据渲染目标重设渲染缩放大小。默认创建的渲染对象为全屏显示,因此需要使用该接口重置scale参数。需要注意的是,该接口使用obj的宽高和parent的视图宽高进行计算,而非和parent的宽高进行计算obj:渲染对象parent:渲染目标。为空时则选为默认值,即屏幕

void lv_gl_obj_move(lv_gl_obj_t *obj, lv_gl_obj_t *parent);

根据渲染目标重设渲染位置。根据scale和base.r的值与parent的视图宽高进行计算,由于使用了scale的值,该接口建议在resize之后调用,且scale有手动修改时,均建议重新调用下该接口obj:渲染对象parent:渲染目标。为空时则选为默认值,即屏幕

void lv_gl_obj_reset_points(lv_gl_obj_t *obj);

将渲染对象的顶点参数重置,即全屏显示

obj:渲染对象

void lv_gl_obj_reset_tex_points(lv_gl_obj_t *obj);

将渲染对象的纹理顶点参数重置,即使用全图obj:渲染对象

void lv_gl_obj_update_vao(lv_gl_obj_t *obj);

根据当前的顶点参数和纹理顶点参数更新渲染对象的VAO和VBO。因此如果手动修改了顶点参数和纹理顶点参数后,需要调用该接口才会生效。如未创建过则会创建新的VAO和VBO。obj:渲染对象

void lv_gl_obj_release_vao(lv_gl_obj_t *obj);

释放VAO和VBO,使用默认的参数obj:渲染对象

void lv_gl_obj_bind_tex(lv_gl_obj_t *obj, lv_gl_tex_t *tex);

绑定纹理对象,纹理对象引用计数会加1。如果渲染对象已经有绑定的纹理对象,则会对原有的纹理对象减引用,当引用小于等于0时会自动释放。因此需要注意,该接口可能触发内存释放,应用上需要避免外部引用纹理对象和重复释放obj:渲染对象tex:纹理对象

void lv_gl_obj_set_crop(lv_gl_obj_t *obj, SDL_Rect *r, int en);

设置裁剪范围,以屏幕左下⻆为原点坐标,向上为Y正方向,向右为X正方向,与LVGL默认的左上⻆为原点不同,需要特别注意obj:渲染对象,为空时表示屏幕r:裁剪范围,起始坐标和大小,为空时起始坐标重置为原点,大小重置为渲染对象大小en:为1使能裁剪,为0关闭裁剪

void lv_gl_obj_get_crop(lv_gl_obj_t *obj, SDL_Rect *r);

获取裁剪范围obj:渲染对象,为空时表示屏幕r:裁剪范围,起始坐标和大小

void lv_gl_obj_set_viewport(lv_gl_obj_t *obj, SDL_Rect *r);

设置视图范围,以屏幕左下⻆为原点坐标,向上为Y正方向,向右为X正方向,与LVGL默认的左上⻆为原点不同,需要特别注意obj:渲染对象,为空时表示屏幕r:视图范围,起始坐标和大小,为空时起始坐标重置为原点,大小重置为渲染对象大小

void lv_gl_obj_get_viewport(lv_gl_obj_t *obj, SDL_Rect *r);

获取视图范围obj:渲染对象,为空时表示屏幕r:视图范围,起始坐标和大小

void lv_gl_obj_render(lv_gl_obj_t *obj);

将渲染对象渲染在framebuffer上,只能在回调中使用

obj:渲染对象

针对相册,图库类应用,新增Gallery DEMO,包含多个动画效果,如滑入,淡入,立方体旋转,立方体翻转,立方体文字显示,滚筒图片预览,照片流等

7.1 目录结构

gallery/
├── anims                   // 动画实现
│   ├── cube_flip.c         // ⽴⽅体翻转动画
│   ├── cube_flip.h
│   ├── cube_lyric.c        // ⽴⽅体字符渲染
│   ├── cube_lyric.h
│   ├── cube_rotate.c       // ⽴⽅体旋转
│   ├── cube_rotate.h
│   ├── fade_out.c          // 淡出
│   ├── fade_out.h
│   ├── fade_slide_out.c    // 滑动淡出
│   ├── fade_slide_out.h
│   ├── fold.c              // 照⽚折叠
│   ├── fold.h
│   ├── photo_stream.c      // 照⽚流
│   ├── photo_stream.h
│   ├── roller.c            // 滚桶图⽚预览
│   ├── roller.h
│   ├── slide_out.c         // 滑动退出
│   ├── slide_out.h
│   ├── stiker.c            // 贴图测试
│   └── stiker.h
├── CMakeLists.txt
├── gallery.c               // UI⼊⼝。渲染对象,纹理初始化等
├── gallery.h
├── main.c                  // 应⽤⼊⼝。lvgl,屏幕,触摸初始化等
├── main.h
└── pics                    // 图⽚资源

7.2 UI入口函数

main.c内函数与lv_demo 代码说明一致,不做过多介绍,主要介绍UI相关代码。

// gallery.c

/* 定义在⽴⽅体字符渲染动画中⽤到的字符,每两⾏渲染到⼀个⾯上,共12⾏ */
static char *lyric[]...

/* 各个动画中⽤到的纹理对象 */
lv_gl_tex_t *tex_cube;
lv_gl_tex_t *tex_2d[6];
lv_gl_tex_t *tex_roller;
lv_gl_tex_t *tex_fb;

/* 各个动画中⽤到的渲染对象 */
lv_gl_obj_t *obj_fb;
lv_gl_obj_t *obj_img0;
lv_gl_obj_t *obj_img1;
lv_gl_obj_t *obj_cube;
lv_gl_obj_t *obj_fold[4];
lv_gl_obj_t *obj_roller_items[6];
lv_gl_obj_t *obj_roller;
/*
 * ⽴⽅体字符渲染动画中⽤到的结构体,共12⾏则会创建⼀个12个成员的lyric_row数组
 * 每个lyric_row内都包含了当前⾏的所有单个字符
 */
lyric_row *obj_lyrics;

/*
 * 视图⼤⼩,为屏幕⼤⼩减去按钮矩阵的⼤⼩
 * 即屏幕分为两部分,下半部分为按钮,⽤于交互触发动画,上半部分为动画渲染区域
 */
SDL_Rect view;

/* 屏幕完整⼤⼩ */
SDL_Rect screen;

/* 指⽰当前动画是否在播放中,⼀次只能等待⼀个动画播放完成 */
int animing = 0;

/* ttf⽂件导⼊的字体 */
lv_ft_info_t ttf_main;

/* lvgl对象,屏幕 */
lv_obj_t *scr;
/* lvgl对象,图⽚,⽤于图⽚切换动画 */
lv_obj_t *img1;
/* lvgl对象,图⽚,⽤于图⽚切换动画  */
lv_obj_t *img2;
/* lvgl对象,动画区域容器 */
lv_obj_t *anim_area;
/* lvgl对象,按钮矩阵 */
lv_obj_t *btn_mat;
/* lvgl对象,进度条,显⽰动画进度 */
lv_obj_t *slider;
/* lvgl对象,照⽚流容器 */
lv_obj_t *photo_box;
/* lvgl对象,照⽚流图⽚ */
lv_obj_t *photos[6];
/* 定义了所有的动画,各个动画都通过宏定义在各⾃的头⽂件中,并在此处引⽤ */
static lv_anim_t anims[]...

/* 定义按钮矩阵的⽂字,与动画顺序⼀⼀对应 */
static const char *btnm_map[]...

/* 按钮矩阵回调,⽤于触发动画 */
static void event_handler(lv_event_t * e)

/* 字体初始化,⽤于中⽂字符渲染 */
static void font_init(void)

/* 创建渲染画布,⽤于后续将⽂字渲染为图⽚ */
static label_canvas *create_canvas(lv_color_t color, lv_font_t *font)

/* 将⽂字渲染为图⽚,并创建GPU渲染对象,⾃动绑定纹理后返回 */
static lv_gl_obj_t *utf8_to_obj(lv_gl_obj_t *parent, label_canvas *lc, char *text)

/* 通⽤动画起始回调,⽤于重置⼀些动画参数 */
void common_anim_start(void)

/* 纹理对象初始化 */
static void tex_init(void)

/* UI⼊⼝ */
void gallery(void)
// gallery.h
typedef struct
{
    /* 指定字体及渲染颜⾊ */
    lv_draw_label_dsc_t label_dsc;
    /* 画布buffer,⽤于反复利⽤,减少申请内存次数 */
    lv_img_dsc_t *img_dsc;
    /* 画布对象 */
    lv_obj_t *canvas;
} label_canvas;

typedef struct {
    /* 字符转出的GPU渲染对象 */
    lv_gl_obj_t **objs;
    /* 当前⾏字符⻓度 */
    int len;
} lyric_row;

7.3 动画

由于lv_gl无法自动触发渲染,需要在动画回调中触发LVGL框架渲染流程,目前是在所有的动画回调中使用lv_obj_invalidate(lv_layer_top())触发渲染,同时由于top layer没有子对象,不会影响到正常的渲染性能。

7.3.1 立方体翻转

效果说明动画效果如图所示,通过立方体的旋转,实现两张照片的切换,旋转过程中立方体适当缩小

中文原稿技术配图

通过X轴旋转15度视⻆可以更好的观察这个动画

中文原稿技术配图

代码说明

/* anims/cube_flip.c */
/*
 * 定义额外的三个⼦动画。应⽤中将完整的动画分为4步,每步旋转90度
 * 第⼀个动画定义在anims/cube_flip.h中,从0度转到90度
 * 第⼀个⼦动画从90度旋转到180度
 * 第⼆个⼦动画从180度转到270度
 * 第三个⼦动画从270度转到360度
 * 每个⼦动画都设定了act_time = -1000,从⽽实现每个动作之间间隔1s
 */
static lv_anim_t sub_anims[]...
/* 设置⽴⽅体动画视图范围,所有⽴⽅体动画通⽤ */
SDL_Rect cube_view;
/* 根据当前的动画进度设置缩放效果 */
#define ZOOM_IN ...
/* ⽴⽅体动画渲染回调,所有⽴⽅体动画通⽤ */
void anim_cube_render(void)
/*
 * ⽴⽅体动画起始回调,所有⽴⽅体动画通⽤
 * 特别注意,在起始回调中将视图修改为⼀个特别的值
 * ⾸先取默认视图中宽⾼较⼩的⼀边(⽐如宽⽐⾼⼩,则取宽)
 * 并将该值除0.57 + 2作为新的视图的宽⾼值(视图需要为正⽅形,否则后续变换可能异常)
 * 由于正⽅体的中⼼到任意⼀⻆的距离d=sqrt(3)/2*a
 * a为边⻓,且等于2.0(坐标系为[-1,-1,-1]-[1, 1, 1])
 * 因此为了保证⽆论如何旋转,⽴⽅体的任意⻆都不会超出坐标系
 * 即d应该始终⼩于1.0
 * 则选择缩放⽐例为0.57。sqrt(3)/2*a*0.57=0.9873
 * +2仅为经验值,保证⽴⽅体略微超出屏幕,不让左右留有缝隙,根据实际应⽤情况可去除
 */
void anim_cube_start(lv_anim_t *a)
/* 起始回调 */
void anim_cube_flip_start(lv_anim_t *a)
/* 动画回调,根据当前动画进度,设置Y轴旋转⻆度和缩放⽐例 */
void anim_cube_flip(void *var, int32_t v)
/* 结束回调,根据a->var的值判断当前是第⼏段动画,并执⾏下⼀段动画或结束 */
void anim_cube_flip_end(lv_anim_t *a)

7.3.2 立方体旋转

效果说明通过简单的360度旋转展示不同面上的照片

中文原稿技术配图

代码说明

/* anims/cube_rotate.c */
/* 起始回调,直接调⽤anim_cube_start,说明⻅前⼀⼩节 */
void anim_cube_rotate_start(lv_anim_t *a)
/* 动画回调,设置缩放为0.57(原因⻅前⼀节),并根据动画进度设置X,Y轴的旋转⻆度 */
void anim_cube_rotate(void *var, int32_t v)
/* 结束回调,恢复动画标志位,恢复进度条 */
void anim_cube_rotate_end(lv_anim_t *a)

7.3.3 立方体字符渲染

效果说明依次旋转立方体6个面,并在当前显示的面上绘制字符,将字符替换为歌词即可配合音乐实现3D歌词展示

中文原稿技术配图

代码说明

/* anims/cube_lyric.c */
/*
 * 定义额外的两个⼦动画。应⽤中将每次旋转,每⾏⽂字的渲染都分为⼦动画去完成
 * 动画从左⾯开始,主动画完成左⾯的两⾏绘制
 * 主动画完成后使能第⼀个旋转⼦动画,从左⾯旋转到右⾯
 * 旋转完成后使能第⼀个渲染⼦动画,完成右⾯的两⾏绘制
 * 绘制完成后使能第⼆个旋转⼦动画,如此循环直⾄6个⾯绘制完成
 */
static lv_anim_t sub_anims[]...
/* 表⽰当前绘制进⾏到第⼏⾏的第⼏个字 */
static int line;
static int row;
static int col;
/*
 * 渲染回调,和普通的⽴⽅体不同,多了⼀步在⽴⽅体上绘制,因此独⽴⼀个回调
 * 在这个回调中
 * 第⼀步先将⽴⽅体设置为framebuffer,表⽰后续的渲染都会绘制到⽴⽅体上
 * 第⼆步绘制当前字符
 * 第三步将framebuffer重置,表⽰后续的渲染直接显⽰到屏幕上
 * 第四步将⽴⽅体渲染到屏幕上
 */
void anim_cube_lyric_render(void)
/*
 * 起始回调,先调⽤⽴⽅体通⽤起始回调,并将渲染回调设置为anim_cube_lyric_render
 * 再将⽴⽅体旋转到左⾯,⽅便后续动画进⾏
 */
void anim_cube_lyric_start(lv_anim_t *a)
/* 绘制动画回调,根据当前进度,更新字符索引,触发渲染 */
void anim_cube_lyric(void *var, int32_t v)
/* 旋转动画回调,根据当前进度,旋转⽴⽅体到合适的⾯ */
void anim_cube_lyric_sub(void *var, int32_t v)
/* 旋转动画结束回调,根据下⼀⾏⽂字的⻓度,更新绘制动画的时⻓(按每字300ms计算),并启动绘制动画 */
void anim_cube_lyric_sub_end(lv_anim_t *a)
/*
 * 绘制动画结束回调,每次绘制完⾏数加⼀
 * 如果⾏数是奇数,说明当前⾯还有⼀⾏未显⽰,根据下⼀⾏⽂字的⻓度
 * 更新绘制动画的时⻓(按每字300ms计算),并启动绘制动画
 * 如果⾏数是偶数,说明当前⾯已绘制完成,启动旋转动画
 * 如果所有12⾏都绘制完成,则结束动画
 * DEMO中共12⾏,每⾯2⾏,如果是其他⾏数规划,则逻辑会有不同
 */
void anim_cube_lyric_end(lv_anim_t *a)

7.3.4 图片折叠

效果说明图一向内折叠退出,图二分两部分从画面外向内位移

中文原稿技术配图

代码说明

/* anims/fold.c */
/* 渲染回调。应⽤将两张图分为四个部分(每张都分为左右两部分),因此在渲染回调中调⽤了四次渲染 */
static void anim_fold_render(void)
/*
 * 起始回调
 * 1 将视图设置为480x480(图⽚⼤⼩,可选,根据实际情况调整)
 * 2 根据新视图重置4个渲染对象的缩放参数
 * 3 将图⽚切分为4块,obj_fold[0]为图⼀左半,obj_fold[1]为图⼀右半,以此类推
 * 4 设置渲染回调
 */
void anim_fold_start(lv_anim_t *a)
/*
 * 动画回调
 * 由于该需求使⽤旋转变换相对复杂,因为旋转轴在不停发⽣变化,且需要根据偏移值反推旋转⻆度等
 * 因此通过直接设置顶点坐标来实现,只需要在动画回调中不停更新四个对象各个顶点的X轴坐标即可
 * ⽽obj_fold[0]的右边两点和obj_fold[1]的左边两点是固定向内折且不变的
 * 因此这4个点的X坐标不需要改变,仅需将Z坐标设置为负值即可(⽬前是设定为0~-0.2,根据效果调整)
 * 顶点着⾊器内的透视变换会⾃动变换出近⼤远⼩的效果
 */
void anim_fold(void *var, int32_t v)
/* 结束回调 */
void anim_fold_end(lv_anim_t *a)

7.3.5 滚桶图片预览

效果说明六张图片构成一个桶形,旋转展示每张图片

中文原稿技术配图

代码说明

/* anims/roller.c */
/* 渲染回调,将6张图⽚依次渲染 */
void anim_roller_render(void)
/*
 * 起始回调
 * 1 将视图设置为2000x2000,因为涉及到旋转,因此为了避免图⽚超出坐标造成裁切丢失画⾯,设为⼀个较⼤值
 * 2 将渲染对象根据新视图重新计算⼤⼩,并且将Z轴偏移设为0.20785,使图⽚距离中⼼有⼀定距离,
 *   从⽽形成桶形。Z轴计算为简单的勾股定理,sqrt(240^2 - 120^2) / 2000 * 2.0 = 0.20785
 *   图⽚宽240(⻅下图),由于正好是六边形,因此简单的sqrt(240^2 - 120^2) 即可计算出每个⾯到
 *   中⼼的距离,2000是视图⼤⼩,由此算出缩放⽐例,再乘上范围2.0(坐标-1.0~1.0)即可得到偏移值
 *   如果不是六边形,则需要通过其他⽅式计算距离,计算缩放⽐例和偏移值的⽅式不变
 * 3 将X轴旋转设为30,Z轴旋转设为15,为了有⼀个倾斜的效果,根据需要随意设置
 */
void anim_roller_start(lv_anim_t *a)
/* 动画回调。为每张图⽚Y轴加上旋转值,第⼀张图为0度加上旋转值,第⼆张图为60度加上旋转值,依次类推 */
void anim_roller(void *var, int32_t v)
/* 结束回调 */
void anim_roller_end(lv_anim_t *a)

中文原稿技术配图

7.3.6 贴图测试

在一个背景图上,每次按一小格步进一段距离,贴上一张小图

中文原稿技术配图

代码说明

/* anims/stiker.c */
/*
 * 渲染回调
 * 1 将obj_fb设为framebuffer
 * 2 将obj_fb清除为(0.0, 0.0, 0.0, 1.0),因为混合⽅式中,最终的alpha为d.a,即底图的alpha,
 *   因此这边需要设为1.0,否则图⽚将透明或不可⻅
 * 3 将obj_img1渲染到obj_fb上作为背景图
 * 4 将obj_img0渲染到obj_fb上作为贴图
 * 5 重置framebuffer,将obj_fb渲染到屏幕上
 * 实际使⽤中,可以将两张图⽚直接渲染到屏幕上,DEMO只是为了演⽰合成功能及framebuffer功能
 */
static void anim_stiker_render(void)
/* 起始回调。将贴图的坐标设为0,0,并调⽤lv_gl_obj_move更新参数值 */
void anim_stiker_start(lv_anim_t *a)
/* 动画回调。根据当前动画进度,设置贴图坐标,并调⽤lv_gl_obj_move更新参数值 */
void anim_stiker(void *var, int32_t v)
/* 结束回调 */
void anim_stiker_end(lv_anim_t *a)

7.3.7 滑动退出

效果说明一个简单的滑动退出的DEMO,如图所示,图一向左逐渐滑出画面,同时图二向左滑入画面

中文原稿技术配图

代码说明

/* anims/slide_out.c */
/* 调⽤通⽤的起始回调,并将动画区域设为可⻅ */
void anim_slide_out_start(lv_anim_t *a)
/* 动画回调,根据当前动画进度更新两张图⽚的X坐标 */
void anim_slide_out(void *var, int32_t v)
/* 结束回调 */
void anim_slide_out_end(lv_anim_t *a)

7.3.8 图片淡出

效果说明一个简单的淡入淡出DEMO,如图所示,图一透明度降低逐渐淡出画面,同时图二增加透明度逐渐淡入画面

中文原稿技术配图

代码说明

/* anims/fade_out.c */
/* 调⽤通⽤的起始回调,并将动画区域设为可⻅ */
void anim_fade_out_start(lv_anim_t *a)
/* 动画回调,根据当前动画进度更新两张图⽚的透明度 */
void anim_fade_out(void *var, int32_t v)
/* 结束回调 */
void anim_fade_out_end(lv_anim_t *a)

7.3.9 滑动淡出

效果说明将前两种动画结合,图一滑出的同时降低透明度,同时图二滑入画面

中文原稿技术配图

代码说明

/* anims/fade_slide_out.c */
/* 调⽤通⽤的起始回调,并将动画区域设为可⻅ */
void anim_fade_slide_out_start(lv_anim_t *a)
/* 动画回调,根据当前动画进度更新两张图⽚的X坐标及图⼀的透明度 */
void anim_fade_slide_out(void *var, int32_t v)
/* 结束回调 */
void anim_fade_slide_out_end(lv_anim_t *a)

7.3.10 照片流

效果说明6张图片由上往下滚动播放,触摸或鼠标点按时暂停动画并随鼠标移动,放开后继续动画

中文原稿技术配图

代码说明

/* anims/photo_stream.c */
/* 图⽚⾼度 */
static int32_t img_h = 480;
/* 图⽚间隔 */
static int32_t gap_h = 20;
/*
 * 根据图⽚⾼度、图⽚间隔、图⽚数量计算出最⼤的Y坐标
 * max_y = n * img_h + (n - 1) * gap_h
 */
static int32_t max_y;
/*
 * 边界值,与max_y类似,⽤于在图⽚到达max_y后重置到照⽚流顶部继续循环
 * boundary = n * img_h + n * gap_h
 * 由于图⽚循环后需要回到顶部继续轮转,且与第⼀张图还需要有⼀个间隔,因此boundary⽐max_y多⼀个gap_h
 */
static int32_t boundary;
/* 定时更新图⽚坐标 */
static lv_timer_t *timer;
/* 定时器定时增加的坐标偏移 */
static int32_t timer_ofs = 0;
/* 上⼀次触摸增加的坐标偏移 */
static int32_t touch_ofs_s = 0;
/* 本次触摸增加的坐标偏移 */
static int32_t touch_ofs = 0;
/* 更新并检查图⽚Y坐标,实现动画效果 */
static void update_y(void)
/* 定时器回调,timer_ofs⾃增 */
static void lv_timer_cb(lv_timer_t *timer)
/*
 * 触摸回调
 * 在LV_EVENT_PRESSED回调中暂停定时器,并获取按下的坐标
 * 在LV_EVENT_PRESSING回调中获取当前触摸坐标,和按下坐标对⽐加上touch_ofs_s计算出偏移值并更新坐标
 * 在LV_EVENT_RELEASED回调中重新启⽤定时器,并将此次触摸偏移量记录到touch_ofs_s
 * 之所以分了三个偏移量,是为了保证触摸按下和触摸释放时的动画坐标连续,避免出现坐标瞬移
 */
static void touch_handler(lv_event_t * e)
/* 停⽌照⽚流 */
void anim_photo_stream_stop(void)
/* 照⽚流初始化,创建定时器,计算出max_y,boundary等 */
void anim_photo_stream_start(lv_anim_t *a)
/* 动画回调
 * 实际上照⽚流是使⽤定时器实现的,与动画⽆关,这边使⽤动画接⼝只是为了接⼝统⼀
 * ⽅便嵌⼊到DEMO框架中,实际应⽤开发使⽤定时器即可
 */
void anim_photo_stream(void *var, int32_t v)
void anim_photo_stream_end(lv_anim_t *a)
  1. 在V9版本中,官方已将lv_drivers合并至lvgl主仓库。

中文原稿补充说明

中英文代码注释详略不同,保留各自原码。中文说明补充:furniture_control 是家电显控,intercom_homepage 是楼宇对讲,intercom_call 是对讲呼叫,video_monitor 播放 RTSP,smart_home 是智能家居,wifibt 适配 rkwifibt。VAO 缓存 VBO 和属性配置以便切换,VBO 保存坐标和纹理顶点,FBO 用于将纹理作为 framebuffer。Gallery 的歌词共12行,每个立方体面两行,lyric_row 数组有12个成员,每项包含该行的字符;屏幕下部是交互按钮,上部为动画区域,一次等待一个动画结束。贴图示例的最终 alpha 为目标底图的 d.a,因此清除颜色的 alpha 设为1.0,否则可能透明或不可见。

中文§1进一步限定:除部分扩展功能外无需外部依赖。可在Windows或Linux PC搭建环境,调试与平台无关的UI,避免反复编译烧写,完成后将代码拷到嵌入式设备编译运行;实际扩展仍需对应依赖。

相关原稿链接

ArmSoM Docs

浏览此主题 → · 入门路线 →