LVGL环形无限循环滚动实现:lv_roller与lv_anim两种方案详解
2026/8/31 1:28:15 网站建设 项目流程

在做嵌入式 HMI 界面时,经常会遇到一类需求:屏幕上的列表、数值或者广告位需要像转盘一样“无限滚下去”,滚到末尾之后又自动回到开头,找不到边界,也不能出现空白停顿。LVGL 本身提供了列表滚动、回弹等机制,但默认行为是“滚到底就停”,距离“无限循环”还差一步。

本文就从原理到代码,完整拆解 LVGL 中实现环形无限循环滚动效果的两种主流方式:一种是使用lv_roller滚轮控件实现无限循环选值,另一种是使用lv_anim动画接口自定义无缝轮播列表。两种思路都能拿到生产环境直接复用。

1. LVGL 环形无限循环滚动是什么

1.1 场景与需求

先明确一下“环形无限循环滚动”的定义:当滚动内容到达末尾时,不会停在边界,也不会出现空白,而是自动跳转到开头继续滚动。从用户视角看,这份内容是一个没有首尾的环。

这种交互在嵌入式 UI 中非常常见:

  • 日期、时间、温度、音量等数值选择滚轮;
  • 首页广告位、公告条、新闻标题轮播;
  • 游戏排行榜、菜单列表的循环滚动;
  • 中控大屏上的状态切换、模式选择。

以日期选择为例,用户从 1 月滚到 12 月,滚轮不会在 12 月停下来,而是继续滚回 1 月。这就是典型的环形无限循环。

1.2 两种实现路线对比

LVGL 中实现这个效果,通常有两条路线:

实现方式适用场景交互能力代码量
lv_roller滚轮控件 +LV_ROLLER_MODE_INFINITE数值选择、日期选择、单项选择支持手指拖动和滚轮滑动
lv_anim动画 + 复制首项回绕广告轮播、公告列表、菜单跑马灯以自动播放为主,也可扩展触摸控制

选择依据很简单:如果需要“用户手动选值”,优先用lv_roller;如果需要“内容自动循环播放”,优先用自定义动画方案。

2. 环境准备与工程搭建

2.1 推荐开发环境

LVGL 的跨平台特性让它既可以在 PC 上仿真,也可以直接跑在 ESP32、STM32、全志、瑞萨等嵌入式平台上。在开始写代码前,建议先在模拟器上验证逻辑,再移植到真实硬件。

本文示例以如下环境为例:

项目推荐选型
操作系统Windows / Linux / macOS
开发工具VS Code + PlatformIO
模拟器lv_sim_vscode_sdl
LVGL 版本8.3.x 为主,9.x 差异会在文中说明
语言C

如果手头没有模拟器工程,可以直接 clone 官方维护的lv_sim_vscode_sdl模板,也可以在 PlatformIO 中搜索lvgl库创建新工程。真实硬件上使用 ESP32 或 STM32 时,LVGL 的 API 不变,改动主要集中在屏幕驱动、触摸驱动和内存配置。

2.2 工程结构说明

以基于 VS Code + PlatformIO 的嵌入式工程为例,核心目录大致如下:

project/ ├── src/ │ ├── main.c │ ├── ui/ │ │ ├── demo_roller.c │ │ └── infinite_scroll.c ├── include/ ├── lib/ └── platformio.ini

模拟器工程中lv_conf.h位于配置目录下,lv_conf.h里的关键配置需要提前打开:

#define LV_USE_LOG 1 #define LV_USE_ANIMATION 1 #define LV_USE_ROLLER 1 #define LV_MEM_CUSTOM 0

LV_USE_ANIMATION决定动画接口是否可用,LV_USE_ROLLER决定滚轮控件是否编译。这两个宏如果关闭,后面的代码将无法运行。

3. 核心原理:滚动与动画机制

3.1 lv_roller 的环形模式

lv_roller是 LVGL 内置的滚轮选择控件,视觉上是一个可上下滚动的列表。它有两种模式:

  • LV_ROLLER_MODE_NORMAL:普通模式,滚到头部或尾部后停止,并存在边界回弹;
  • LV_ROLLER_MODE_INFINITE:无限循环模式,选项按环排列,尾部继续滚动会回到头部。

开启无限循环模式后,lv_roller内部会把选项数组头尾相接,所以不需要手动处理回绕逻辑。这是最简单、最不容易出错的方案。

3.2 lv_anim 动画执行机制

lv_anim是 LVGL 的通用动画接口。它做的事情很简单:在一段时间内,把一个整数从起始值逐步变化到结束值,每次变化时回调一个执行函数。

核心参数包括:

  • 动画目标对象var
  • 执行回调exec_cb
  • 起始值和结束值;
  • 动画时长;
  • 路径函数path_cb
  • 重复次数repeat_count

对于自定义滚动效果,执行回调只需要做一件事:把容器对象的 Y 坐标设置为当前动画值。

static void scroll_anim_cb(void *var, int32_t v) { lv_obj_set_y((lv_obj_t *)var, v); }

动画值从0变到某个最大偏移量,容器就会向上平滑滚动。重复次数设置为LV_ANIM_REPEAT_INFINITE,整个动画就会无限执行。

3.3 无缝回绕的关键

自定义动画面临一个问题:动画结束后要重新从 0 开始,如果直接跳回起点,画面会闪一下。

解决思路其实很简单——在列表末尾复制一份首项。假设列表有 N 项,动画从 0 滚到N * itemHeight。当滚动结束时,第 N+1 个副本刚好位于视口顶部,它的内容和第 1 项完全一样。此时动画瞬间跳回 0,第 1 项原始内容出现,视觉上没有任何变化,用户便感知不到“跳变”。

这就是环形无限循环滚动在动画层面的核心原理。

4. 实战一:基于 lv_roller 实现环形滚轮选择器

4.1 创建滚轮控件

先创建一个最简单的滚轮,选项用换行符分隔。

/* file: demo_roller.c */ #include "lvgl.h" void demo_roller_create(void) { lv_obj_t *roller = lv_roller_create(lv_scr_act()); lv_obj_set_size(roller, 160, 200); lv_obj_center(roller); }

此时roller是一个空对象,还没有选项,需要在下一步设置。

4.2 设置无限循环模式

lv_roller_set_options的第三个参数用于设置模式,传入LV_ROLLER_MODE_INFINITE即可开启环形无限循环。

lv_roller_set_options(roller, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", LV_ROLLER_MODE_INFINITE);

选项字符串以\n分隔,每个\n之间是一个选项。这里实现了从 1 到 10 的环形选择。

4.3 读取选中值

实际项目中,滚轮选完值后通常需要联动其他控件,比如调节温度、切换模式。可以通过LV_EVENT_VALUE_CHANGED事件监听值变化。

static void roller_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_VALUE_CHANGED) { lv_obj_t *roller = lv_event_get_target(e); char buf[16]; lv_roller_get_selected_str(roller, buf, sizeof(buf)); LV_LOG_USER("selected: %s", buf); } }

在创建函数中注册事件回调:

lv_obj_add_event_cb(roller, roller_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

lv_roller_get_selected_str会把当前选中的选项文本拷贝到缓冲区,非常适合与其他业务逻辑对接。

4.4 定制选中项样式

默认滚轮的样式比较朴素,项目落地一般会调整选中项的背景色、文字颜色和字号。需要注意,选中项属于LV_PART_SELECTED,普通选项属于LV_PART_MAIN

/* 普通选项样式 */ lv_obj_set_style_text_color(roller, lv_palette_main(LV_PALETTE_GREY), LV_PART_MAIN); /* 选中项样式 */ lv_obj_set_style_bg_color(roller, lv_palette_darken(LV_PALETTE_BLUE, 2), LV_PART_SELECTED); lv_obj_set_style_bg_opa(roller, LV_OPA_COVER, LV_PART_SELECTED); lv_obj_set_style_text_color(roller, lv_color_white(), LV_PART_SELECTED); lv_obj_set_style_text_font(roller, &lv_font_montserrat_20, LV_PART_SELECTED);

LV_PART_SELECTED的背景默认为透明,需要显式设置LV_OPA_COVER才能看到背景色。如果希望滚轮默认选中某个值,可以调用:

lv_roller_set_selected(roller, 2, LV_ANIM_ON);

第三个参数传入LV_ANIM_ON会让滚轮以动画方式滚动到目标选项,体验更平滑。

4.5 完整示例

把上面的片段组合起来,就是一个完整可运行的滚轮示例:

/* file: demo_roller.c */ #include "lvgl.h" static void roller_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_VALUE_CHANGED) { lv_obj_t *roller = lv_event_get_target(e); char buf[16]; lv_roller_get_selected_str(roller, buf, sizeof(buf)); LV_LOG_USER("selected: %s", buf); } } void demo_roller_create(void) { lv_obj_t *roller = lv_roller_create(lv_scr_act()); lv_roller_set_options(roller, "1\n2\n3\n4\n5\n6\n7\n8\n9\n10", LV_ROLLER_MODE_INFINITE); lv_roller_set_selected(roller, 2, LV_ANIM_ON); lv_obj_set_size(roller, 160, 200); lv_obj_center(roller); lv_obj_add_event_cb(roller, roller_event_cb, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_set_style_text_color(roller, lv_palette_main(LV_PALETTE_GREY), LV_PART_MAIN); lv_obj_set_style_bg_color(roller, lv_palette_darken(LV_PALETTE_BLUE, 2), LV_PART_SELECTED); lv_obj_set_style_bg_opa(roller, LV_OPA_COVER, LV_PART_SELECTED); lv_obj_set_style_text_color(roller, lv_color_white(), LV_PART_SELECTED); lv_obj_set_style_text_font(roller, &lv_font_montserrat_20, LV_PART_SELECTED); }

这个方案适合数值选择、日期选择、模式切换等交互型场景,代码量小,稳定性高。

5. 实战二:基于 lv_anim 实现自定义无缝轮播

5.1 设计思路

当业务需要的是“自动滚动的广告位”或“公告列表”时,lv_roller就不太合适了,因为滚轮控件主要面向手动交互。这时可以自己搭一个滚动容器,用lv_anim驱动。

设计如下:

  1. 创建一个外层容器作为视口,设置裁剪;
  2. 在容器内创建N + 1个卡片,其中最后一个卡片复制第 1 个卡片的内容;
  3. 用动画把容器从 Y = 0 滚动到N * itemHeight
  4. 动画重复执行,末尾副本与首项内容一致,回绕时视觉无跳变。

5.2 创建滚动容器和卡片

首先定义一些常量,便于调整布局:

/* file: infinite_scroll.c */ #include "lvgl.h" #define VIEW_WIDTH 240 #define VIEW_HEIGHT 64 #define ITEM_NUM 4 #define ITEM_HEIGHT 64

创建容器时,需要用lv_obj_remove_style_all清掉默认样式,然后设置大小和裁剪:

void infinite_scroll_create(lv_obj_t *parent) { lv_obj_t *scroller = lv_obj_create(parent); lv_obj_remove_style_all(scroller); lv_obj_set_size(scroller, VIEW_WIDTH, VIEW_HEIGHT); lv_obj_set_pos(scroller, 40, 120); lv_obj_set_style_clip_corner(scroller, true, 0); lv_obj_set_style_pad_all(scroller, 0, 0); lv_obj_set_style_border_width(scroller, 0, 0); }

lv_obj_set_style_clip_corner(scroller, true, 0)非常关键,它保证子卡片超出容器范围的部分不会绘制,避免滚动时出现“残影”。

接下来创建卡片。为了演示回绕原理,卡片高度设置为ITEM_HEIGHT - 4,位置间隔为ITEM_HEIGHT,卡片之间留出 4 像素间距:

static void create_card(lv_obj_t *parent, int index) { int idx = index % ITEM_NUM; lv_obj_t *card = lv_obj_create(parent); lv_obj_remove_style_all(card); lv_obj_set_size(card, VIEW_WIDTH, ITEM_HEIGHT - 4); lv_obj_set_pos(card, 0, index * ITEM_HEIGHT); lv_obj_set_style_bg_color(card, lv_palette_main(LV_PALETTE_BLUE + idx), 0); lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); lv_obj_set_style_radius(card, 8, 0); lv_obj_t *label = lv_label_create(card); lv_label_set_text_fmt(label, "Item %d", idx + 1); lv_obj_set_style_text_color(label, lv_color_white(), 0); lv_obj_center(label); }

创建卡片时,index可以大于ITEM_NUM,内部通过index % ITEM_NUM得到真实索引。这样第ITEM_NUM张卡片与第 0 张卡片内容完全一致,方便实现无缝回绕。

5.3 编写滚动动画

动画回调函数只需要设置容器 Y 坐标:

static void scroll_anim_cb(void *var, int32_t v) { lv_obj_t *cont = (lv_obj_t *)var; lv_obj_set_y(cont, v); }

在创建函数中循环创建卡片,然后启动动画:

for (int i = 0; i < ITEM_NUM + 1; i++) { create_card(scroller, i); } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, scroller); lv_anim_set_exec_cb(&a, scroll_anim_cb); lv_anim_set_values(&a, 0, ITEM_NUM * ITEM_HEIGHT); lv_anim_set_time(&a, 2400); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_set_path_cb(&a, lv_anim_path_linear); lv_anim_start(&a); }

动画值从 0 到ITEM_NUM * ITEM_HEIGHT,也就是滚动 4 个卡片的总高度。当滚动结束时,第 4 张卡片(内容为 Item 0 的副本)位于顶部,和起始位置的第 0 张卡片画面一致。动画进入下一轮重复时,Y 坐标从 0 重新开始,视觉上完全看不出跳变。

5.4 控制动画启停与速度

在实际项目中,可能需要根据业务状态暂停或恢复动画。停止动画只需要删除对应对象上的动画:

void infinite_scroll_stop(lv_obj_t *scroller) { lv_anim_del(scroller, scroll_anim_cb); }

lv_anim_del(scroller, scroll_anim_cb)会删除该对象上绑定的滚动动画回调。如果想要调整滚动速度,修改lv_anim_set_time的数值即可。2400 毫秒表示滚动完 4 个卡片需要 2.4 秒,轮播场景可以适当加长。

5.5 完整示例

组装后的完整代码如下:

/* file: infinite_scroll.c */ #include "lvgl.h" #define VIEW_WIDTH 240 #define VIEW_HEIGHT 64 #define ITEM_NUM 4 #define ITEM_HEIGHT 64 static void scroll_anim_cb(void *var, int32_t v) { lv_obj_t *cont = (lv_obj_t *)var; lv_obj_set_y(cont, v); } static void create_card(lv_obj_t *parent, int index) { int idx = index % ITEM_NUM; lv_obj_t *card = lv_obj_create(parent); lv_obj_remove_style_all(card); lv_obj_set_size(card, VIEW_WIDTH, ITEM_HEIGHT - 4); lv_obj_set_pos(card, 0, index * ITEM_HEIGHT); lv_obj_set_style_bg_color(card, lv_palette_main(LV_PALETTE_BLUE + idx), 0); lv_obj_set_style_bg_opa(card, LV_OPA_COVER, 0); lv_obj_set_style_radius(card, 8, 0); lv_obj_t *label = lv_label_create(card); lv_label_set_text_fmt(label, "Item %d", idx + 1); lv_obj_set_style_text_color(label, lv_color_white(), 0); lv_obj_center(label); } void infinite_scroll_create(lv_obj_t *parent) { lv_obj_t *scroller = lv_obj_create(parent); lv_obj_remove_style_all(scroller); lv_obj_set_size(scroller, VIEW_WIDTH, VIEW_HEIGHT); lv_obj_set_pos(scroller, 40, 120); lv_obj_set_style_clip_corner(scroller, true, 0); lv_obj_set_style_pad_all(scroller, 0, 0); lv_obj_set_style_border_width(scroller, 0, 0); for (int i = 0; i < ITEM_NUM + 1; i++) { create_card(scroller, i); } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, scroller); lv_anim_set_exec_cb(&a, scroll_anim_cb); lv_anim_set_values(&a, 0, ITEM_NUM * ITEM_HEIGHT); lv_anim_set_time(&a, 2400); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_set_path_cb(&a, lv_anim_path_linear); lv_anim_start(&a); }

6. 常见问题与排查思路

6.1 问题现象对照表

问题现象常见原因解决思路
滚轮滚到末尾停止,无法循环未启用LV_ROLLER_MODE_INFINITE检查lv_roller_set_options第三个参数
自定义动画滚动到底后画面闪跳末尾没有复制首项卡片创建ITEM_NUM + 1个卡片,末尾卡片内容复制首项
动画卡顿、掉帧绘制区域过大,裁剪未开启开启clip_corner,减少容器内透明层级
动画启动后对象被删除导致崩溃删除对象前未停止动画先调用lv_anim_del再删除对象
滚轮触摸无反应对象不可点击检查LV_OBJ_FLAG_CLICKABLE标志
动画重复后停留位置异常动画起始值设置错误确认lv_anim_set_values是从 0 开始
内存不足无法创建多个卡片每个卡片独立创建了过多对象减少卡片内控件数量,复用样式

6.2 动画卡顿排查

如果自定义滚动过程中出现明显掉帧,优先检查两点:

一是容器是否开启了裁剪。没有裁剪时,LVGL 会绘制视口外的卡片,屏幕越大开销越大。开启clip_corner后,LVGL 只绘制可见区域。

二是卡片内部控件数量。每个卡片里的lv_obj_createlv_label_create都会占用内存并参与绘制计算。广告卡片中如果包含大量子控件,建议用静态样式、减少透明效果,必要时在屏幕刷新率较低的嵌入式平台上调大 LVGL 的刷新周期。

6.3 动画无法停止的处理

有些开发者会在LV_EVENT_CLICKED回调中停止动画,但忘记传对回调函数名。lv_anim_del的第二个参数是与动画绑定的执行回调函数指针,必须与lv_anim_set_exec_cb传入的函数完全一致。

lv_anim_del(scroller, scroll_anim_cb);

如果希望停止对象上的所有动画,也可以传NULL

lv_anim_del(scroller, NULL);

7. 最佳实践与工程建议

7.1 优先选择现成的滚轮控件

如果你的需求是“用户可以上下拖拽选择”,不要自己用lv_anim做。lv_rollerLV_ROLLER_MODE_INFINITE模式已经实现了环形循环选值,内部封装了触摸、惯性滑动、越界处理,稳定性和体验都远好于手写方案。

自定义动画方案更适合“没有手动交互”的数据展示类滚动,例如轮播图、飞字公告、自动切换的列表。

7.2 动画时长与路径曲线

手动交互场景中,动画时长建议控制在 200ms 到 500ms 之间,路径曲线优先选择lv_anim_path_ease_out,让滚动减速停止,手感更自然。

自动轮播场景中,动画时长可以适当拉长到 2000ms 到 4000ms,路径曲线使用lv_anim_path_linear保持匀速前进。匀速滚动在轮播广告中更专业,不会给人一顿一顿的感觉。

7.3 注意对象生命周期

使用LV_ANIM_REPEAT_INFINITE无限循环动画时,要特别注意对象销毁时序。设备休眠、页面切换、应用退出时,如果直接销毁滚动容器,动画回调会访问已经释放的内存,造成崩溃。

安全做法是在销毁对象前先停止动画:

lv_anim_del(scroller, NULL); lv_obj_delete(scroller);

7.4 内存与性能优化

LVGL 跑在资源受限的嵌入式设备上时,内存优化值得单独关注。以下几点在滚动场景中尤其重要:

  • 尽量复用样式对象,不要每创建一个卡片就新建一套样式;
  • 卡片数量不要贪多,容器外不可见的卡片应在移除后及时删除;
  • 开启裁剪,减少不可见区域的绘制开销;
  • 需要修改文字时使用lv_label_set_text_fmt,避免频繁创建标签;
  • 合理配置lv_conf.h中的LV_MEM_SIZE,如果动画过程中出现异常,优先检查内存池大小。

7.5 LVGL 9.x 版本兼容说明

本文示例基于 LVGL 8.3,如果你正在使用 LVGL 9.x,需要注意部分 API 调整:

  • lv_anim_set_time在 9.x 中更名为lv_anim_set_duration
  • lv_roller_set_optionslv_roller_get_selected_str等滚轮接口基本不变;
  • 部分颜色宏和样式接口在 9.x 中有微小变化,按编译器提示调整即可。

整体思路完全通用:滚轮无限循环模式、复制首项实现无缝回绕,这两个核心点在不同版本中都是一致的。

8. 结语

环形无限循环滚动在 LVGL 中并不是高不可攀的特性,核心就是理解两点:滚轮的环形选项模式,以及动画回绕时的首项复制。项目里如果只需要交互选值,优先考虑lv_roller;如果要做广告轮播或跑马灯,自实现动画更灵活。

拿到代码后建议先在模拟器上跑一遍,把视口尺寸、卡片高度、滚动时长调到自己项目中合适的数值,再移植到真实硬件。这样视觉和稳定性都能得到保障。如果你也在用 LVGL,欢迎按文章思路改造成适合自己的滚动组件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询