news 2026/9/9 22:58:43

ESP32+LVGL动画实战:从核心机制到三种效果实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32+LVGL动画实战:从核心机制到三种效果实现

做嵌入式界面开发时,最常遇到的一个问题就是:UI 控件能显示,但一动起来就发虚、卡顿、闪屏,甚至直接白屏死机。尤其是给 ESP32 这类资源不算宽裕的 MCU 做动画,网上资料零散,照着抄又经常因为 LVGL 版本差异编译不过。这篇文章从 ESP32 + LVGL 的实际开发视角出发,梳理动画相关的核心机制,并给出一套可以直接运行的实战示例,覆盖加载动画、仪表盘指针、图标平移三种常见效果。无论是刚接触嵌入式图形开发的新手,还是已经在用 LVGL 做项目但想优化动画的开发者,都能从中找到可复用的代码和排查思路。

1. 背景与核心概念

1.1 为什么要在嵌入式设备上做动画

很多人觉得嵌入式屏幕显示只要“能看见”就行,动画是锦上添花。但在实际产品里,动画的角色往往不是装饰,而是交互反馈的一部分。比如:

  • 设备开机时需要一个加载进度条,否则用户无法判断系统是否死机;
  • 温度、湿度、电压等传感器数值变化时,用平滑的进度条或指针动画代替数字跳变,读取体验更直观;
  • 页面切换、弹窗出现时,使用淡入淡出或位移动画,会让整个界面显得更像现代消费电子产品;
  • 状态提示(如 Wi-Fi 连接中、蓝牙配对中)用呼吸灯效果或旋转图标,能减少等待焦虑。

LVGL(Light and Versatile Graphics Library)是目前嵌入式领域使用非常广泛的开源图形库。它提供控件、样式、布局、事件系统,以及一套统一的动画机制。和直接在裸机代码里操作像素、自己写帧循环相比,LVGL 动画的最大优势是:动画参数与渲染逻辑分离,开发人员只需要定义变量变化范围和变化曲线,LVGL 内部会自动完成插值、逐帧刷新和资源释放。

1.2 LVGL 动画适合在什么场景下使用

LVGL 动画适合用在屏幕分辨率在 240x320、320x480 左右,主控具备一定算力(例如 ESP32、STM32F4/F7、IMXRT 等)的设备上。ESP32 虽然有双核处理器,但屏幕刷新走 SPI 总线,带宽有限,因此动画设计更偏重“短小精悍”,而不是追求复杂粒子特效。

动画可以作用于几乎所有可以被函数设置的属性,常见包括:

  • 坐标位置:lv_obj_set_xlv_obj_set_y
  • 尺寸大小:lv_obj_set_widthlv_obj_set_height
  • 角度旋转:lv_arc_set_valuelv_meter_set_indicator_value
  • 透明度:lv_obj_set_style_opa
  • 样式颜色:lv_obj_set_style_bg_color

只要知道属性怎么设置,就能用lv_anim让它在指定时间内平滑变化。

1.3 本文适合哪些读者

本文默认你已经能正常点亮一块 SPI 屏幕,并在 ESP32 上跑通 LVGL 的基本控件显示。如果不是,可以先按官方文档或网上教程完成环境搭建再回来。需要的基础如下:

  • 了解 Arduino IDE 或 ESP-IDF 的基本使用;
  • 能在 ESP32 上编译烧录程序;
  • 会创建 LVGL 控件并修改样式;
  • 知道lv_timer_handler是 LVGL 的心跳函数。

本篇文章重点不是 LVGL 环境搭建,而是动画机制和实战技巧。

2. 环境准备与版本说明

2.1 硬件准备

做这套示例,我用的是非常常见的一套配置,你不需要完全一致:

  • ESP32 开发板(ESP32-WROOM-32 或 ESP32-S3 均可,建议选带 PSRAM 的型号,图片和帧缓冲会更从容);
  • SPI 接口 TFT 屏幕,常见驱动 IC 为 ILI9341 或 ST7789,分辨率 240x320;
  • 可选:触摸屏。本文示例不依赖触摸,没有触摸也能跑;
  • 杜邦线或开发板排针连接。

接线方面,注意 ESP32 与屏幕的 SPI 引脚不要接反,尤其是 CS、DC(D/C)、RST 这几个控制引脚,一旦接错就会出现白屏或花屏。

2.2 软件环境

软件环境按大家最常用的组合来:

  • Arduino IDE(2.x 版本);
  • ESP32 开发板扩展包(通过 Arduino IDE 的 Boards Manager 安装,或者使用离线安装包离线安装);
  • TFT_eSPI 库(用于驱动 SPI 屏幕);
  • LVGL 库(建议选择 8.x 稳定版本)。

本文示例代码基于 LVGL v8 编写。LVGL v9 发布后,某些动画 API 命名有调整,如果你使用的是 v9,需要参考官方迁移文档对函数名和部分结构体字段做适配。版本问题在第三节后还会单独说明。

2.3 项目结构

Arduino IDE 项目中,Libraries 目录下会放置 TFT_eSPI 和 lvgl 两个库。TFT_eSPI 需要修改User_Setup.h配置文件,设置屏幕驱动类型、分辨率、SPI 引脚。LVGL 库则需要添加lv_conf.h配置文件,并确保其被正确引入。

一个典型的代码结构如下:

ArduinoSketch/ ├── ArduinoSketch.ino └── lv_conf.h(放在库目录外的 lib 文件夹或项目根目录,按 Arudino LVGL 配置方式导入)

如果你使用 PlatformIO,项目结构会更清晰:

project/ ├── platformio.ini ├── include/ │ └── lv_conf.h ├── lib/ │ ├── lvgl/ │ └── TFT_eSPI/ └── src/ └── main.cpp

无论哪种方式,核心代码逻辑是一样的。

3. LVGL 动画核心机制拆解

3.1 动画三要素:变量、执行函数、时间

LVGL 动画的基本原理可以这样理解:

  1. 设置一个动画对象lv_anim_t
  2. 指定要操作的控件或变量;
  3. 指定一个回调函数,负责把当前帧的数值应用到控件上;
  4. 指定起始值、结束值、持续时间;
  5. 调用lv_anim_start,LVGL 就会在每次刷新时计算当前帧应取的插值,并调用回调函数。

一个最简单的动画代码如下:

static void anim_value_cb(void *var, int32_t val) { lv_bar_set_value((lv_obj_t *)var, val, LV_ANIM_OFF); } void create_simple_anim(lv_obj_t *bar) { lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, bar); lv_anim_set_exec_cb(&a, anim_value_cb); lv_anim_set_values(&a, 0, 100); lv_anim_set_time(&a, 2000); lv_anim_start(&a); }

这里的anim_value_cb就是执行回调。lv_anim_set_var指定要操作的对象,lv_anim_set_exec_cb指定如何操作。lv_anim_set_values指定从 0 到 100,lv_anim_set_time指定 2000 毫秒完成。

3.2 动画路径:让过渡不单调

如果不做任何设置,动画值会按照线性方式从起点到终点,也就是匀速变化。匀速动画在 UI 上并不总是最好看,例如进度条加载到 80% 以后,如果一直是匀速,用户很难判断是卡住还是仍在加载。引入缓动函数后,动画会呈现加速、减速、回弹等效果。

LVGL 提供的路径函数比较多,常见的几个:

路径函数效果适用场景
lv_anim_path_linear匀速简单的数值进度
lv_anim_path_ease_in加速物体离开屏幕
lv_anim_path_ease_out减速物体进入屏幕、加载条
lv_anim_path_ease_in_out先加速后减速弹窗出现、仪表指针
lv_anim_path_bounce落地回弹图标移动、提示气泡

设置方式是在lv_anim_init之后调用:

lv_anim_set_path_cb(&a, lv_anim_path_ease_out);

路径函数本质上也是一个回调,它接收当前时间与总时间的比例,返回当前进展值。如果你有特殊需求,也可以自定义路径函数,这在后续部分会提到。

3.3 动画重复、延迟与反转

实际项目中,例如等待图标,通常需要无限循环播放。LVGL 支持三个关键参数:

  • lv_anim_set_delay:动画开始前的等待时间;
  • lv_anim_set_repeat_count:重复次数,LV_ANIM_REPEAT_INFINITE表示无限循环;
  • lv_anim_set_playback_time:反向播放时间,可以实现“来回摆动”的效果。

例如一个呼吸灯效果,透明度从 80 逐渐降到 30,然后反向播放再回升到 80,可以写成:

lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_set_playback_time(&a, 1000);

这样动画会在 forward 和 playback 之间来回切换,看起来非常自然。

3.4 动画完成回调与串行动画

有些场景需要动画结束后再触发另一个动画,比如一个图标先放大再移动。LVGL 提供了ready_cb,在当前动画播放完成后调用。通过ready_cb再启动新动画,就能实现串行动画。

需要注意的是,ready_cb的原型是void (*lv_anim_ready_cb_t)(struct _lv_anim_t *),在回调里要从lv_anim_t *参数中获取用户数据,可以使用lv_anim_get_user_data(a)得到之前设置的用户数据。

示例:

static void scale_ready_cb(lv_anim_t *a) { lv_obj_t *obj = lv_anim_get_user_data(a); // 启动下一个动画 start_move_anim(obj); } void start_scale_anim(lv_obj_t *obj) { lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, obj); lv_anim_set_exec_cb(&a, set_scale_cb); lv_anim_set_values(&a, 0, 100); lv_anim_set_time(&a, 500); lv_anim_set_ready_cb(&a, scale_ready_cb); lv_anim_set_user_data(&a, obj); lv_anim_start(&a); }

3.5 动画与 LVGL 心跳的关系

LVGL 动画不是独立的线程,它的驱动力来自lv_timer_handler。这个函数每秒会被调用多次,每次 LVGL 内部会检查当前时间、更新所有活动动画的插值、调用执行回调,并触发屏幕刷新。因此,如果lv_timer_handler没有被频繁调用,或者调用的时间间隔不稳定,动画就会出现明显掉帧或卡顿。

在 Arduino 环境中,loop函数通常这样写:

uint32_t last_tick = 0; void loop() { uint32_t now = millis(); lv_tick_inc(now - last_tick); last_tick = now; lv_timer_handler(); delay(5); }

lv_tick_inc用来告诉 LVGL 已经过去了多少毫秒。如果遗漏,动画会完全不动,这也是新手最容易忽略的坑。

4. 完整实战:ESP32 + LVGL 实现三种动画效果

接下来实现一个稍完整的示例。屏幕分辨率是 240x320,页面包含三个动画:

  1. 顶部一个加载进度条,从 0 增长到 100,完成后重置;
  2. 中部一个仪表盘圆弧,模拟指针从 0 转到 260;
  3. 底部一个图标,在水平方向来回移动。

4.1 创建项目结构

我用 Arduino IDE 来实现。新建一个 Sketch,命名为lvgl_anim_demo,目录下放入主要代码。如果你用 PlatformIO,也可以采用相同的源文件结构。

主要文件:

  • lvgl_anim_demo.ino:Arduino 入口,包含 setup 和 loop;
  • lv_conf.h:LVGL 配置文件,需要手动启用;
  • User_Setup.h:TFT_eSPI 屏幕配置。

这里不要求你一定把lv_conf.h放在项目根目录,取决于你的库加载方式。只要确保lv_conf.h在编译器包含路径中即可。

4.2 添加依赖与 lv_conf.h 配置

首先需要确保 LVGL 被正确初始化。在lv_conf.h中,至少需要关注几个关键配置项:

#define LV_COLOR_DEPTH 16 #define LV_MEM_CUSTOM 1 #define LV_MEM_SIZE (64U * 1024U) #define LV_USE_ANIMATION 1 #define LV_USE_ARC 1 #define LV_USE_BAR 1 #define LV_USE_IMG 1

如果LV_MEM_CUSTOM设置为 0,LVGL 会使用内部静态内存池。ESP32 的 RAM 比较有限,我建议保留自定义内存管理,并让 LVGL 使用标准堆内存。

如果需要显示图片素材,还需要在lv_conf.h中启用解码器相关配置,例如LV_USE_PNGLV_USE_BMP等。本文示例使用内置图片或纯控件,不涉及图片文件系统加载。

4.3 编写屏幕驱动与 LVGL 初始化

setup函数中,需要完成屏幕初始化、LVGL 初始化、显示驱动注册。

#include <lvgl.h> #include <TFT_eSPI.h> static lv_disp_draw_buf_t draw_buf; static lv_color_t buf[240 * 20]; static lv_disp_drv_t disp_drv; TFT_eSPI tft = TFT_eSPI(); void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); tft.startWrite(); tft.setAddrWindow(area->x1, area->y1, w, h); tft.pushColors((uint16_t *)&color_p->full, w * h, true); tft.endWrite(); lv_disp_flush_ready(disp); } void lvgl_init() { lv_init(); lv_disp_draw_buf_init(&draw_buf, buf, NULL, 240 * 20); lv_disp_drv_init(&disp_drv); disp_drv.hor_res = 240; disp_drv.ver_res = 320; disp_drv.flush_cb = my_disp_flush; disp_drv.draw_buf = &draw_buf; lv_disp_drv_register(&disp_drv); } void setup() { Serial.begin(115200); tft.begin(); tft.setRotation(1); // 根据屏幕方向调整 lvgl_init(); create_ui(); }

这里使用了单缓冲,缓冲大小是240 * 20,也就是 20 行像素。如果 ESP32 内存充足,建议把这个缓冲提到240 * 40或更多,能够明显减少撕裂感。

4.4 实现加载进度条动画

进度条使用lv_bar,通过动画修改它的值。

static void bar_anim_cb(void *bar, int32_t val) { lv_bar_set_value((lv_obj_t *)bar, val, LV_ANIM_OFF); } void create_loading_bar(lv_obj_t *parent) { lv_obj_t *bar = lv_bar_create(parent); lv_obj_set_size(bar, 180, 12); lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 20); lv_bar_set_range(bar, 0, 100); lv_bar_set_value(bar, 0, LV_ANIM_OFF); lv_obj_set_style_bg_color(bar, lv_color_hex(0x333333), LV_STATE_DEFAULT); lv_obj_set_style_bg_color(bar, lv_color_hex(0x00AEEF), LV_PART_INDICATOR); lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, bar); lv_anim_set_exec_cb(&a, bar_anim_cb); lv_anim_set_values(&a, 0, 100); lv_anim_set_time(&a, 2000); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_start(&a); }

为什么这里用LV_ANIM_OFF?因为我们在动画回调中逐帧设置数值,如果这里设置为LV_ANIM_ON,LVGL 内部还会再启动一个“值变化动画”,与外部动画叠加,反而会出现奇怪的数值抖动。

4.5 实现仪表盘圆弧指针动画

仪表盘可以用lv_arc来做。lv_arc本身支持设置角度,非常适合做仪表效果。

static void arc_anim_cb(void *arc, int32_t val) { lv_arc_set_value((lv_obj_t *)arc, val); } void create_meter_arc(lv_obj_t *parent) { lv_obj_t *arc = lv_arc_create(parent); lv_obj_set_size(arc, 180, 180); lv_obj_align(arc, LV_ALIGN_CENTER, 0, -10); lv_arc_set_rotation(arc, 135); lv_arc_set_bg_angles(arc, 0, 270); lv_arc_set_value(arc, 0); lv_obj_remove_style(arc, NULL, LV_PART_KNOB); lv_obj_set_style_arc_width(arc, 8, LV_PART_MAIN); lv_obj_set_style_arc_width(arc, 8, LV_PART_INDICATOR); lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, arc); lv_anim_set_exec_cb(&a, arc_anim_cb); lv_anim_set_values(&a, 0, 260); lv_anim_set_time(&a, 1500); lv_anim_set_delay(&a, 300); lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_set_playback_time(&a, 1500); lv_anim_start(&a); }

lv_arc的 value 范围默认是 0 到 100,但我们可以通过lv_arc_set_range调整。不过在这个示例里,利用 0 到 260 的角度变化来模拟指针,配合playback_time让圆弧转过去又转回来,很像仪表指针的响应过程。

4.6 实现图标水平移动动画

图标使用lv_img控件,执行回调里修改x坐标。为了不依赖外部图片文件,这里使用一个简单的内置符号,例如 LVGL 内置的LV_SYMBOL_SETTINGS字体图标。要依赖字体符号,确保lv_conf.h中启用了对应字体,例如LV_FONT_MONTSERRAT_24

static void img_move_cb(void *img, int32_t val) { lv_obj_set_x((lv_obj_t *)img, val); } void create_move_icon(lv_obj_t *parent) { lv_obj_t *img = lv_label_create(parent); lv_label_set_text(img, LV_SYMBOL_SETTINGS); lv_obj_set_style_text_font(img, &lv_font_montserrat_24, 0); lv_obj_set_pos(img, 0, 250); lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, img); lv_anim_set_exec_cb(&a, img_move_cb); lv_anim_set_values(&a, 0, 200); lv_anim_set_time(&a, 1200); lv_anim_set_path_cb(&a, lv_anim_path_bounce); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_start(&a); }

如果你希望使用真实 PNG 图标,需要先通过 LVGL 官方图片转换工具将图片转换为 C 数组或生成到 SPIFFS,然后在代码中引用。图片内存开销很大,后面会提到优化建议。

4.7 组合 UI 并运行

create_ui中,把三个动画都创建出来,并放到一个根容器中:

void create_ui() { lv_obj_t *scr = lv_scr_act(); create_loading_bar(scr); create_meter_arc(scr); create_move_icon(scr); }

完整loop如下:

uint32_t last_tick = 0; void loop() { uint32_t now = millis(); lv_tick_inc(now - last_tick); last_tick = now; lv_timer_handler(); delay(5); }

编译烧录后,正常现象是:顶部进度条平滑增长后回退,中部圆弧像指针一样摆到最大角度再返回,底部的图标在屏幕左右来回移动。如果卡顿,优先检查屏幕驱动时的 SPI 频率和缓冲大小。

4.8 自定义路径函数

标准缓动函数不够用时,LVGL 允许自定义路径。路径函数的原型如下:

typedef int32_t (*lv_anim_path_cb_t)(const lv_anim_t *a);

其中a里可以读取act_timedurationstart_valueend_value。一个简单弹性路径示例:

static int32_t my_custom_path(const lv_anim_t *a) { int32_t t = a->act_time; int32_t d = a->duration; int32_t start = a->start_value; int32_t end = a->end_value; if (t >= d) return end; float p = (float)t / d; float result = 1 - powf(1 - p, 4); // 自定义 ease-out 四次方 return start + (int32_t)((end - start) * result); }

然后注册:

lv_anim_set_path_cb(&a, my_custom_path);

自定义路径函数的灵活性很高,但要注意避免在函数内做大量浮点运算。ESP32 有硬件 FPU,少量浮点没问题,但如果每个动画每帧都计算大量sinpow,CPU 占用会明显上升。

5. 常见问题与排查思路

问题现象常见原因解决思路
动画完全不执行没有调用lv_tick_inc,或lv_timer_handler没在主循环中调用确认主循环中调用并正确提供 tick 时间
动画卡顿、闪烁帧缓冲太小,SPI 时钟过高或不稳定,刷新次数过多增大缓冲区,调整 SPI 频率,减少重复动画
动画执行到一半突然停止代码中调用了lv_anim_del删除了动画,或控件被删除检查是否有其他逻辑误删控件或动画
动画数值有跳变回调中又使用了LV_ANIM_ON,导致内部叠加动画回调中设置值使用LV_ANIM_OFF
同一时间多个动画占用内存过高LVGL 动画对象数量过多,或动画路径复杂控制并发动画数量,按优先级启动
API 编译报错,例如lv_anim_set_path_cb不存在LVGL v8 和 v9 API 有差异确认当前库版本,参考迁移文档调整
显示图片时动画闪烁图片解码耗时,读取 SPIFFS 慢使用 C 数组图片,或先缓存解码结果
白屏或花屏屏幕初始化失败,引脚配置错误检查 TFT_eSPI 的 User_Setup.h 配置
ESP32 内存不足导致复位LVGL 内存池太小或缓冲区太大给 LVGL 分配合理内存,使用 PSRAM 存帧缓冲或图片

从实际经验看,动画不执行的问题绝大多数不是动画代码本身,而是 tick 时间没有正确喂给 LVGL。使用 Arduino 的millis时,注意loop中不要出现阻塞,比如delay(1000)或长耗时读取,否则lv_timer_handler无法及时执行。

6. 最佳实践与工程建议

6.1 尽量使用 PSRAM

ESP32-WROVER 和 ESP32-S3 开发板基本都带 PSRAM。建议在lv_conf.h或初始化代码中显式使用 PSRAM 分配大的缓冲区:

static lv_color_t *buf1 = (lv_color_t *)ps_malloc(240 * 40 * sizeof(lv_color_t));

ps_malloc分配的内存不会占用内部 SRAM,对大图像帧缓冲非常友好。LVGL 支持自定义内存分配,可以像下面这样:

static void *lvgl_malloc(size_t size) { return ps_malloc(size); } void setup() { lv_mem_init_custom(lvgl_malloc, free); }

但这部分依赖具体 SDK,如果遇到问题,可以退回到heap_caps_malloc(size, MALLOC_CAP_SPIRAM)

6.2 控制动画数量与对象数量

LVGL 动画虽然方便,但每个动画都会在每帧刷新时被计算。并发 20 个动画时,如果每个动画都操作一个大型控件,CPU 负载会线性增长。实际项目中,建议:

  • 次要元素不要同时动画,保证视觉焦点最多两三个动画;
  • 不需要动画时及时lv_anim_del(var, NULL)释放;
  • 长时间不显示的页面,用lv_obj_clean清理对象和动画。

6.3 使用 monitor 回调监控帧率

LVGL 显示驱动中提供了monitor_cb回调,可以统计实际刷新时间和帧率。在调试阶段,可以打印刷屏耗时,帮助定位动画卡顿源头:

void my_monitor_cb(lv_disp_drv_t *disp, uint32_t time, uint32_t px) { Serial.printf("flush time: %d ms, pixels: %d\n", time, px); }

注册时:

disp_drv.monitor_cb = my_monitor_cb;

如果单次 flush 时间超过 30ms,动画刷新率就很难超过 30fps,此时优先优化屏幕驱动而不是动画逻辑。

6.4 优先使用 C 数组图片,避免运行中解码大图

LVGL 支持从外部文件读取图片,但在动画过程中频繁读取 SPIFFS/Flash 图片,会带来明显的 IO 延迟。对于动画中反复移动的图标,建议把图片通过在线转换工具生成 C 数组,直接编译进固件。这样闪存读取快速且稳定。

如果 UI 资源很多,可以考虑 LVGL 的lv_snapshot机制,先把静态复杂页面渲染成一张图片缓存,动画时只移动这张缓存图,能大幅减少绘制工作量。

6.5 使用多任务时注意线程安全

如果使用 ESP-IDF 的 FreeRTOS,LVGL 默认不是线程安全的。常用的做法是:

  • 在单独的任务中调用lv_timer_handler
  • 其他任务想操作 LVGL 时,使用队列、信号量或互斥锁保护;
  • 不要在 ISR 中断中直接调用lv_anim_startlv_timer_handler

一个简单的思路是创建一个 UI 任务:

void ui_task(void *pvParameters) { while (1) { lv_timer_handler(); vTaskDelay(pdMS_TO_TICKS(5)); } }

把整个 LVGL 操作限制在这个任务中,外部事件通过xQueueSend发消息给 UI 任务处理,既简单又稳定。

7. 总结与后续方向

这一套示例包含的内容不算复杂,但它涵盖了 LVGL 动画的常用核心点:动画对象初始化、执行回调、路径函数、延迟、重复、反向播放以及动画完成回调。把这些基础吃透后,再去理解 LVGL 源码里的lv_anim实现就不会觉得困难了。

下一步可以在三个方向继续深入:一是把动画平滑度和帧率做到更好,学习 LVGL 的脏矩形刷新机制和 DMA 加速;二是研究图片动画的资源优化,把 C 数组、SPIFFS、LZ4 压缩等方案用在真实产品中;三是如果使用 LVGL v9,需要专门看一下新版动画 API 的变化,特别是路径函数和样式动画的调整。

在真实项目中,动画设计要克制,把动画用在关键交互节点上,配合 UI 主题和音效反馈,会让整个设备的使用体验提升一个档次。如果这篇文章里的示例代码或排查思路对你有帮助,欢迎收藏备用;遇到类似动画卡顿或 LVGL 版本差异的问题,也欢迎在评论区聊聊你的解决过程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:58:20

汉代制盐工艺复原:卤水提纯与蒸发结晶全解析

很久之前整理汉代制盐工艺资料时&#xff0c;我发现多数资料都是从历史或经济角度切入&#xff0c;真正把“制盐这件事具体怎么操作”讲清楚的很少。尤其是四川出土的汉代煮盐画像砖&#xff0c;画面上有盐井、高架、灶台、铁锅和忙碌的人&#xff0c;但考古图像背后涉及的取卤…

作者头像 李华
网站建设 2026/9/9 22:57:52

i wanna be the salt全流程攻略:简单长耐久的通关思路

先给结论&#xff1a;如果你玩过“I Wanna”系列的其他作品&#xff0c;再用“i wanna be the salt”这个名字来判断难度&#xff0c;很容易踩错预期。它并不是那种第一关就把人按在地上反复摩擦的硬核跳跃&#xff0c;标题里“简单长耐久”这几个字更接近它的真实定位&#xf…

作者头像 李华
网站建设 2026/9/4 8:02:50

拆解 Claude Code 内核:手写一个最小 Agent Harness

最近在终端里重度使用 Claude Code 做代码迁移和批量重构&#xff0c;发现很多同学对它的理解仍停留在“一个能聊天的命令行工具”。真正驱动它在仓库里读文件、跑命令、改代码的&#xff0c;是一套完整的 Agent Harness。本文不打算逐行搬运官方闭源代码&#xff0c;而是从工程…

作者头像 李华
网站建设 2026/9/3 20:24:39

基于51单片机的酒精气体检测仪设计:AD原理图、PCB与程序实现

简介&#xff1a;本资源是一套面向嵌入式初学者与课程设计者的51单片机酒精气体检测仪完整开发资料&#xff0c;聚焦气体传感系统从硬件搭建到软件实现的全流程实践。资料包含AD转换原理图、双层PCB设计文件&#xff08;.schdoc与.pcbdoc&#xff09;、Keil工程源码&#xff08…

作者头像 李华
网站建设 2026/9/6 3:52:58

低功耗TCXO/VCTCXO设计实战:从原理到调试的完整指南

开头 微型低功耗TCXO/VCTCXO&#xff0c;这四个词放在一起&#xff0c;通常意味着PCB上指甲盖四分之一大小的黑色封装、1mA级工作电流&#xff0c;却能保证无线设备在-40℃到85℃范围内把频率始终稳定在1ppm以内。做IoT、GNSS、可穿戴或者NB-IoT方案的工程师&#xff0c;几乎都…

作者头像 李华
网站建设 2026/9/6 3:37:02

YOLOv8与ReID结合的多摄像头人脸追踪系统实践

简介&#xff1a;本资源是一套基于YOLOv8目标检测与ReID行人重识别算法融合实现的多摄像头协同人脸追踪系统&#xff0c;面向计算机科学、人工智能、信息安全等专业的在校学生、教师及工程技术人员&#xff0c;适用于课程设计、毕业设计、项目立项演示及算法二次开发实践。压缩…

作者头像 李华