公司动态

LVGL环形无限循环滚动实现:lv_roller与lv_anim两种方案详解

📅 2026/8/31 1:28:27
LVGL环形无限循环滚动实现:lv_roller与lv_anim两种方案详解
在做嵌入式 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_sdlLVGL 版本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 0LV_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。当滚动结束时第 N1 个副本刚好位于视口顶部它的内容和第 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驱动。设计如下创建一个外层容器作为视口设置裁剪在容器内创建N 1个卡片其中最后一个卡片复制第 1 个卡片的内容用动画把容器从 Y 0 滚动到N * itemHeight动画重复执行末尾副本与首项内容一致回绕时视觉无跳变。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_create、lv_label_create都会占用内存并参与绘制计算。广告卡片中如果包含大量子控件建议用静态样式、减少透明效果必要时在屏幕刷新率较低的嵌入式平台上调大 LVGL 的刷新周期。6.3 动画无法停止的处理有些开发者会在LV_EVENT_CLICKED回调中停止动画但忘记传对回调函数名。lv_anim_del的第二个参数是与动画绑定的执行回调函数指针必须与lv_anim_set_exec_cb传入的函数完全一致。lv_anim_del(scroller, scroll_anim_cb);如果希望停止对象上的所有动画也可以传NULLlv_anim_del(scroller, NULL);7. 最佳实践与工程建议7.1 优先选择现成的滚轮控件如果你的需求是“用户可以上下拖拽选择”不要自己用lv_anim做。lv_roller的LV_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_durationlv_roller_set_options、lv_roller_get_selected_str等滚轮接口基本不变部分颜色宏和样式接口在 9.x 中有微小变化按编译器提示调整即可。整体思路完全通用滚轮无限循环模式、复制首项实现无缝回绕这两个核心点在不同版本中都是一致的。8. 结语环形无限循环滚动在 LVGL 中并不是高不可攀的特性核心就是理解两点滚轮的环形选项模式以及动画回绕时的首项复制。项目里如果只需要交互选值优先考虑lv_roller如果要做广告轮播或跑马灯自实现动画更灵活。拿到代码后建议先在模拟器上跑一遍把视口尺寸、卡片高度、滚动时长调到自己项目中合适的数值再移植到真实硬件。这样视觉和稳定性都能得到保障。如果你也在用 LVGL欢迎按文章思路改造成适合自己的滚动组件。