很多嵌入式开发者在做 GUI 时都会遇到这样一个需求:界面上有一串列表,或者一组页面卡片,需要让用户左右滑动,而且滑到末尾之后不要停住,直接无缝回到开头继续滑,形成一种“转圈”的效果。这个需求在手机 App 里很常见,比如 iOS 的滚轮选择器、首页 Banner 无限轮播、选项卡横向滑动列表。
在 LVGL 项目里,很多人第一次接到这个需求时,第一反应是去写复杂的滚动状态机,或者往容器里不断追加数据,甚至想去修改 LVGL 的事件分发逻辑。其实这个需求并不需要从零造轮子。我的判断是先放在前面:
- 如果你的目标是文本选择器,比如设置项里的滚动选值,官方
lv_roller控件在LV_ROLLER_MODE_INFINITE模式下本身就是环形无限循环的,你只需要做配置,不需要自己写轮播逻辑。 - 如果你的目标是 Banner、卡片或自定义控件横向无缝轮播,更推荐
scroll_snap + 数据副本的思路,用最少的代码实现“到末尾后无感跳转回开头”。 - 如果你的目标是不需要人为操作、全自动循环播放的展示页,可以再加一个
lv_anim定时驱动,让滚动更顺滑。
这篇文章会把三条路径全部拆开,从概念、代码到验证方式都讲清楚,同时把最常见的几个坑列出来,包括滚动边界、事件不触发、动画跳变、版本差异等。读完你应该能直接在自己的 LVGL 工程里实现环形无限循环滚动效果。
1. 这篇文章真正要解决的问题
先明确一下“环形无限循环滚动”到底是什么意思。
普通的滚动是线性行为:内容有多少,滚动范围就有多大,滚到末尾就停止,再继续滑动会产生回弹或者边界提示。环形无限循环滚动指的是:内容在逻辑上形成一个环,用户滑到末尾时,自动无缝回到开头,中间不能出现内容空白、不能出现回跳动画、不能让用户感知到“已经到底了”。
这个需求通常出现在以下几个场景:
- 滚轮式选择器:类似闹钟设置里选择小时、分钟,列表上下循环滚动。
- 首页 Banner 轮播:多张图片横向滑动,滑完最后一张直接回到第一张。
- 仪表菜单页:横向多个功能卡片,用户左右滑动切换,首尾相接。
- 数据列表循环展示:比如当前温度、电压等状态信息循环显示。
很多开发者在这个需求上容易犯两个错误。
第一个错误是试图在 LVGL 里直接用一个超长容器,把所有数据重复很多份,比如把 3 个页面复制 100 次,让用户永远滑不到底。这种方案能实现“看起来无限”,但内存开销大,数据同步麻烦,而且滚动位置一旦异常复位就会露馅。
第二个错误是去改 LVGL 底层滚动逻辑,比如监听滚动事件后强制设置偏移量,结果没有关闭动画,或者没有处理回弹标志位,导致页面跳变严重,体验很差。
正确的思路应该是:优先使用 LVGL 官方已经提供的循环特性,比如lv_roller;如果官方控件不适合自定义 UI,再用“逻辑环形化”的方式,在滚动事件里做位置补偿。
2. 核心概念:LVGL 滚动机制与“环形无限循环”的本质
在动手写代码之前,理解 LVGL 的滚动模型比背 API 更重要。LVGL 里几乎所有对象都可以设置为可滚动对象,比如lv_obj、lv_list、lv_roller。滚动行为依赖几个核心概念。
2.1 可滚动对象
任何对象只要内容超出自身尺寸,并且没有关闭滚动标志位,就会成为可滚动对象。在 LVGL 中,滚动方向通过LV_OBJ_FLAG_SCROLLABLE标志控制,可以使用lv_obj_add_flag/lv_obj_clear_flag来调整。例如:
lv_obj_t * cont = lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 200, 120); /* 横向滚动 */ lv_obj_set_scroll_dir(cont, LV_DIR_HOR);这里LV_DIR_HOR表示只允许横向滚动。如果是纵向选择器,则使用LV_DIR_VER。
2.2 scroll snap
scroll snap是 LVGL 中一个非常实用的功能:当用户滚动结束并且手指释放时,对象会自动吸附到指定的对齐位置。这个机制天然适合做页面轮播和卡片切换。
使用方式分两步:
- 在容器对象上设置整体 snap 策略,例如
LV_SCROLL_SNAP_CENTER表示居中吸附。 - 在每个子对象上设置该子对象自己的 snap 对齐方式,例如
LV_SCROLL_SNAP_CENTER。
lv_obj_set_scroll_snap_x(cont, LV_SCROLL_SNAP_CENTER);lv_obj_set_scroll_snap_align(child, LV_SCROLL_SNAP_CENTER);只有同时设置了容器策略和子对象对齐方式,snap 才会生效。
2.3 scroll callback
LVGL 提供了LV_EVENT_SCROLL事件,可以在滚动过程中不断触发回调。这个事件是实现“到末尾后跳回开头”的关键。环形无限循环滚动本质上就是:在滚动回调里判断当前位置,如果已经越过了逻辑边界,就立刻把滚动偏移量替换到环的另一端,并且关闭动画。
2.4 环形缓冲区的思想
“环形无限循环滚动”这个名字很容易让人联想到嵌入式开发里的环形缓冲区。环形缓冲区之所以能做到“无限写”,是因为它通过取模运算把线性空间虚拟成了一个环。LVGL 滚动也可以借鉴这个思想:内容本身是有限的,但滚动位置是取模的。
也就是说,我们要做的不是增加无限多的内容,而是让“偏移量”周期性地映射回有效区间。这也是后面方案二的核心原理。
2.5 lv_roller 的循环模式
lv_roller是 LVGL 官方自带的滚轮控件,默认支持普通模式。在普通模式下,列表滚到底后也会停止。但是当设置为LV_ROLLER_MODE_INFINITE时,它就会自动启用环形循环模式,用户上下滑动时列表首尾相接,不会出现边界。
这是 LVGL 官方封装的“环形无限循环滚动”能力,所以如果你的场景是文本选择器,直接用它是最省事的。
3. 方案选型:三种实现路径对比
在实现之前,先做一个方案选型,方便你根据自己项目的 UI 结构选择。
| 方案 | 适合场景 | 代码量 | 灵活性 | 风险点 |
|---|---|---|---|---|
| 方案一:lv_roller 循环模式 | 文本选择器、滚动选值 | 极少 | 低,只能基于 roller 样式 | 自定义样式受限于控件结构 |
| 方案二:scroll_snap + 数据副本 | Banner、卡片、自定义页面轮播 | 中等 | 高,子控件完全自定义 | 需要处理副本与真实数据的对应关系 |
| 方案三:lv_anim 自动驱动 | 全自动轮播、大屏展示 | 中等 | 高,可以配合前两种 | 需要管理定时器和动画生命周期 |
如果你的 UI 主要是几个矩形色块、图片、卡片组合成的页面,强烈建议使用方案二。如果你只是要做一个类似时钟设置里选数字的选择器,方案一最稳。方案三通常不单独使用,而是作为补充机制,叠加在方案二上。
4. 环境准备与基础配置
本文代码基于 LVGL 8.3 编写,但大多数 API 在 LVGL 9.x 也能使用。如果你使用的是 LVGL 9.x,注意以下几点:
lv_roller_create和lv_roller_set_options仍然存在。- 事件处理中
lv_event_get_target的用法不变。 - 部分对象样式接口从
lv_style_set_*调整为lv_obj_set_style_*,但本文主要使用官方推荐的后一种,兼容性更好。 - LVGL 9 中滚动事件和 snap API 基本保持一致。
如果你还没有 LVGL 环境,建议先用模拟器跑通。常见方式有三种:
- 官方在线模拟器:直接打开 LVGL 官网模拟器页面验证效果。
- VS Code + 本地模拟器:使用 CMake 或者 PlatformIO 构建模拟器工程,适合调试。
- 嵌入式开发板:在 STM32、ESP32 等平台上移植 LVGL 后测试,需要确认屏幕驱动和输入设备正常。
如果你是在单片机上使用,注意 LVGL 内存配置,环形滚动本身不会带来额外内存压力,但是lv_roller和动画系统会占用一些内存,建议开启LV_MEM_CUSTOM并使用专用的内存池。
5. 方案一:lv_roller 实现文本环形无缝滚动
先上最简单的方案。lv_roller的用法很直接,它本质是一个纵向滚动的列表,文本项之间用\n分隔。
5.1 创建循环模式的 roller
/* 创建 roller */ lv_obj_t * roller = lv_roller_create(lv_scr_act()); /* 设置选项,LV_ROLLER_MODE_INFINITE 表示环形无限循环 */ lv_roller_set_options(roller, "0\n" "1\n" "2\n" "3\n" "4\n" "5\n" "6\n" "7\n" "8\n" "9", LV_ROLLER_MODE_INFINITE); lv_obj_set_size(roller, 150, 200); lv_obj_center(roller); /* 默认选中第 2 项 */ lv_roller_set_selected(roller, 2, LV_ANIM_ON); /* 给 roller 添加选中事件回调 */ lv_obj_add_event_cb(roller, roller_event_cb, LV_EVENT_VALUE_CHANGED, NULL);这里最关键的就是LV_ROLLER_MODE_INFINITE。它让 roller 首尾相接,用户向下滑动到“9”之后继续滑动,会自动循环到“0”,不会在末端停止。从用户视角来看,这就是一个环形无限循环滚动效果。
5.2 读取当前选中项
在事件回调中,我们可以拿到当前选中的字符串或者索引:
static void roller_event_cb(lv_event_t * e) { lv_obj_t * roller = lv_event_get_target(e); char buf[16]; lv_roller_get_selected_str(roller, buf, sizeof(buf)); LV_LOG_USER("当前选中: %s", buf); }如果你需要的是选中索引,可以直接调用:
uint16_t sel = lv_roller_get_selected(roller);5.3 设置默认选中项
在初始化时,调用lv_roller_set_selected(roller, index, LV_ANIM_ON)即可。注意LV_ANIM_ON会让它滚动到目标项,LV_ANIM_OFF则是直接跳转。
5.4 方案一的局限
lv_roller虽然实现简单,但它的结构相对固定:一个滚轮容器,里面是文本标签。如果你需要在滚轮里显示图片、进度条、自定义图形,lv_roller就比较难改造。这时应该走方案二。
6. 方案二:scroll_snap + 数据副本实现容器无限轮播
这个方案适合自定义内容。核心思路是:
- 使用一个横向滚动容器,里面放置多个子页面。
- 在真实页面首尾各放一份副本,让用户可以滑过真实边界。
- 监听
LV_EVENT_SCROLL事件,当滚动到尾部副本时,立刻无动画跳转到对应的真实页面;滚动到头部副本时,立刻无动画跳转到对应的真实页面。
6.1 创建横向滚动容器
#define ITEM_WIDTH 100 #define ITEM_HEIGHT 120 #define REAL_COUNT 3 static void scroll_event_handler(lv_event_t * e); lv_obj_t * cont = lv_obj_create(lv_scr_act()); lv_obj_set_size(cont, 200, 150); lv_obj_center(cont); /* 开启横向滚动,关闭弹性回弹 */ lv_obj_set_scroll_dir(cont, LV_DIR_HOR); lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLL_ELASTIC); /* 滚动条不需要显示 */ lv_obj_set_scrollbar_mode(cont, LV_SCROLLBAR_MODE_OFF); /* 使用 flex 布局让子页面横向排布 */ lv_obj_set_layout(cont, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); /* 设置容器 snap 策略 */ lv_obj_set_scroll_snap_x(cont, LV_SCROLL_SNAP_CENTER); /* 添加滚动事件回调 */ lv_obj_add_event_cb(cont, scroll_event_handler, LV_EVENT_SCROLL, NULL);这里有一个比较容易忽略的关键点:lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLL_ELASTIC)。如果不关闭弹性回弹,滚动时可能会出现“往回拉”的效果,破坏环形滚动的连续性。
6.2 创建页面子对象
每个子对象需要设置LV_SCROLL_SNAP_CENTER,让 snap 逻辑以每个子对象的中心作为吸附点。
static void add_page(lv_obj_t * cont, const char * text, lv_color_t color) { lv_obj_t * page = lv_obj_create(cont); lv_obj_set_size(page, ITEM_WIDTH, ITEM_HEIGHT); lv_obj_set_style_bg_color(page, color, 0); lv_obj_set_style_radius(page, 8, 0); lv_obj_set_scroll_snap_align(page, LV_SCROLL_SNAP_CENTER); lv_obj_t * label = lv_label_create(page); lv_label_set_text(label, text); lv_obj_center(label); }创建 5 个页面:头部副本 + 真实 3 页 + 尾部副本。
/* 头部副本:真实最后一页的副本 */ add_page(cont, "2", lv_palette_main(LV_PALETTE_RED)); /* 真实页面 */ add_page(cont, "0", lv_palette_main(LV_PALETTE_BLUE)); add_page(cont, "1", lv_palette_main(LV_PALETTE_GREEN)); add_page(cont, "2", lv_palette_main(LV_PALETTE_RED)); /* 尾部副本:真实第一页的副本 */ add_page(cont, "0", lv_palette_main(LV_PALETTE_BLUE));为了直观,副本和真实页可以使用相同的背景色。这样用户滑动时看不出差异。
6.3 初始化位置
创建完成之后,应把滚动位置定位到第一个真实页面,也就是索引 1 的位置:
lv_obj_scroll_to_x(cont, ITEM_WIDTH, LV_ANIM_OFF);如果不做这一步,初始位置会停在第 0 个副本上,相当于用户一打开就看到重复页面。
6.4 核心滚动事件处理
接下来是环形滚动的核心逻辑:
#define TOTAL_COUNT (REAL_COUNT + 2) static void scroll_event_handler(lv_event_t * e) { lv_obj_t * cont = lv_event_get_target(e); lv_coord_t x = lv_obj_get_scroll_x(cont); /* 滚到末尾副本:偏移量 >= REAL_COUNT * ITEM_WIDTH */ if (x >= REAL_COUNT * ITEM_WIDTH) { /* 跳到开头真实页面,也就是索引 1 的位置,偏移量为 ITEM_WIDTH */ lv_obj_scroll_to_x(cont, x - REAL_COUNT * ITEM_WIDTH, LV_ANIM_OFF); } /* 滚到头部副本:偏移量 < ITEM_WIDTH */ else if (x < ITEM_WIDTH) { /* 跳到末尾真实页面,偏移量为 REAL_COUNT * ITEM_WIDTH */ lv_obj_scroll_to_x(cont, x + REAL_COUNT * ITEM_WIDTH, LV_ANIM_OFF); } }这段逻辑的本质是“取模运算”。当偏移量超过真实内容区间时,把它减去一个周期;当偏移量小于真实内容区间起点时,把它加上一个周期。因为设置了LV_ANIM_OFF,跳转过程没有动画,用户感知不到。
这里要注意:在LV_EVENT_SCROLL回调中修改滚动位置时,一定要关掉动画。如果使用LV_ANIM_ON,会看到页面突然往回滑动的过程,这就破坏了无缝效果。
6.5 与手势滑动的配合
这个方案在真实触摸滚动、鼠标滚轮、键盘方向键滚动时都能正常工作,因为LV_EVENT_SCROLL是 LVGL 滚动机制的统一事件入口。即使 snap 动画触发滚动,回调同样会被调用,环形逻辑仍然有效。
6.6 如何判断当前显示的是哪一页
在实际业务中,你可能需要知道用户当前停在哪个真实页面上。可以在LV_EVENT_SCROLL_END或LV_EVENT_VALUE_CHANGED事件中计算当前索引:
static void scroll_end_handler(lv_event_t * e) { lv_obj_t * cont = lv_event_get_target(e); lv_coord_t x = lv_obj_get_scroll_x(cont); /* 计算出当前吸附的中心位置,再换算成真实页面索引 */ int page_index = x / ITEM_WIDTH; if (page_index >= REAL_COUNT) { page_index -= REAL_COUNT; } if (page_index < 0) { page_index += REAL_COUNT; } LV_LOG_USER("当前真实页面索引: %d", page_index); }在 LVGL 8.3 中,滚动结束事件可以用LV_EVENT_SCROLL_END。如果你的版本中没有这个事件,改用LV_EVENT_RELEASED也能实现类似效果,但需要注意触摸释放的时机。
7. 方案三:lv_anim 驱动实现自动无限滚动
有时候我们不需要用户操作,而是希望页面自动循环播放,比如大屏展示、广告机、演示模式。这时候需要用一个定时器或动画循环控制滚动位置。
这个方案可以和方案二结合使用,在 scroll_snap 和环形跳转逻辑之上,增加一个定时器,每隔几秒自动滚动到下一页。
7.1 使用 lv_timer 每隔固定时间滚动
static void auto_scroll_timer_cb(lv_timer_t * timer) { lv_obj_t * cont = (lv_obj_t *)lv_timer_get_user_data(timer); lv_coord_t current_x = lv_obj_get_scroll_x(cont); /* 计算下一页位置,如果超过末尾则回到第一页 */ lv_coord_t target = current_x + ITEM_WIDTH; lv_coord_t max_x = REAL_COUNT * ITEM_WIDTH; if (target > max_x) { target = ITEM_WIDTH; } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, cont); lv_anim_set_exec_cb(&a, scroll_x_anim_cb); lv_anim_set_values(&a, current_x, target); lv_anim_set_time(&a, 400); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); lv_anim_start(&a); }7.2 动画执行回调
static void scroll_x_anim_cb(void * var, int32_t v) { lv_obj_t * cont = (lv_obj_t *)var; lv_obj_scroll_to_x(cont, v, LV_ANIM_OFF); }注意,这里在动画执行回调中调用lv_obj_scroll_to_x时必须使用LV_ANIM_OFF,否则会产生动画叠加,导致滚动位置抖动。
7.3 创建定时器
lv_timer_t * timer = lv_timer_create(auto_scroll_timer_cb, 3000, cont); lv_timer_ready(timer);lv_timer_create的第二个参数是周期,单位毫秒。上面的代码表示每 3 秒自动滚动一次。lv_timer_ready会让定时器立即触发一次,让页面一开始就进入轮播节奏。
7.4 自动滚动与用户手势的冲突处理
生产环境中一个很常见的问题是:用户正在手动滑动页面,定时器突然触发自动滚动,导致两个动画打架。
解决办法是:在检测到用户触摸时暂停定时器,触摸结束一段时间后再恢复。例如在容器上添加LV_EVENT_PRESSED和LV_EVENT_RELEASED事件,在回调中暂停和恢复定时器。
static void cont_event_cb(lv_event_t * e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_PRESSED) { lv_timer_pause(timer); } else if (code == LV_EVENT_RELEASED) { lv_timer_resume(timer); } }这个细节在演示类和交互类界面中非常重要,如果不处理,用户会明显感觉到界面“抢”自己的操作。
8. 运行结果与效果验证
写完代码后,我们需要确认三件事:
8.1 现象是否达到预期
- 手动向左滑动容器,滑过最后一个真实页面后,页面自动无缝回到第一个真实页面,中间没有空白停顿。
- 手动向右滑动容器,滑过第一个真实页面后,页面自动无缝回到最后一个真实页面。
- 文本选择器场景下,向下滚动到最后一个选项后继续滚动,列表回到第一个选项,并且没有跳动感。
8.2 如何用日志验证
在方案二的scroll_event_handler里加入日志输出:
LV_LOG_USER("scroll x = %d", (int)x);观察日志中 x 的数值变化。正常环形滚动时,x 应该始终落在[ITEM_WIDTH, REAL_COUNT * ITEM_WIDTH]区间内。如果发现 x 超出这个范围后立刻跳回,说明环形逻辑生效;如果 x 卡在边界,说明副本数量或边界判断写错了。
8.3 如何验证事件
在LV_EVENT_VALUE_CHANGED回调中打印当前页面索引,切换页面时应该能收到连续的索引变化,并且不会出现无效值。
如果事件没有触发,优先检查是否给容器添加了正确的事件回调函数。在 LVGL 8.3 中,使用lv_obj_add_event_cb时事件回调签名是void (*)(lv_event_t *),不要写错。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 滚动到末尾后停止,没有循环 | 未设置LV_ROLLER_MODE_INFINITE,或者方案二中未添加尾部副本 | 检查lv_roller_set_options的第三个参数;检查容器子对象数量 | 将 roller 模式改为LV_ROLLER_MODE_INFINITE;方案二补上尾部副本 |
| 环形跳转时出现明显回弹动画 | 在滚动回调中使用了LV_ANIM_ON,或者没有关闭弹性回弹 | 检查lv_obj_scroll_to_x参数;检查SCROLL_ELASTIC标志 | 改为LV_ANIM_OFF;调用lv_obj_clear_flag(cont, LV_OBJ_FLAG_SCROLL_ELASTIC) |
| 滚动位置异常跳变,页面错乱 | flex 布局下子对象宽度未固定,或者 snap 对齐未设置 | 打印每个子对象的坐标和宽度 | 固定子对象尺寸,并确保所有子对象设置LV_SCROLL_SNAP_CENTER |
| 事件回调不触发 | 事件回调未注册,或者注册在了错误对象上 | 检查lv_obj_add_event_cb的目标对象 | 将回调注册到实际可滚动容器上 |
| 自动轮播与手势冲突 | 未在用户触摸时暂停定时器 | 打印定时器状态,观察触发时机 | 在LV_EVENT_PRESSED暂停,在LV_EVENT_RELEASED恢复 |
| 在 LVGL 9.x 下编译报错 | 部分 API 在 9.x 中改名或调整了头文件 | 查看编译错误信息对应 API | 对照 LVGL 9 的迁移文档调整函数名,例如部分样式接口 |
10. 最佳实践与工程建议
10.1 优先使用官方循环能力
如果一个功能官方已经提供了,就不要自己再实现一遍。lv_roller的LV_ROLLER_MODE_INFINITE就是官方对环形无限滚动的封装,使用它能极大降低维护成本。只有在 UI 结构不满足需求时,才考虑自定义容器方案。
10.2 数据副本不要被业务逻辑污染
方案二中,首尾副本只是用来“骗过滚动边界”的 UI 占位,不应该存储真实业务数据。比如真实数据数组只有 3 条,副本不应该额外申请业务数组。跳转逻辑通过偏移量的取模完成,不要在副本上绑定业务状态,否则数据同步会出现麻烦。
10.3 关闭不必要的滚动效果
在环形滚动场景中,LV_OBJ_FLAG_SCROLL_ELASTIC和LV_SCROLLBAR_MODE_ON默认可能会对体验造成干扰。建议关闭弹性回弹,并隐藏滚动条。如果你需要滚动条提示用户位置,可以在容器上使用LV_SCROLLBAR_MODE_AUTO,但要注意环形跳转时滚动条位置可能瞬间变化。
10.4 考虑屏幕刷新率和动画时长
在嵌入式设备上,动画时间不宜太长。一般滑动吸附动画设置在 200ms 到 500ms 之间。如果屏幕刷新率较低,或者主控性能一般,动画时间过长会导致画面看起来拖沓。自动轮播的等待时间建议在 2 到 5 秒之间,具体根据内容量和用户阅读速度调整。
10.5 定时器生命周期管理
如果页面会被销毁,一定要在页面销毁时将自动轮播的lv_timer删除,否则会出现野指针和内存泄漏。推荐在页面析构或LV_EVENT_DELETE事件中调用lv_timer_del(timer)。
10.6 版本差异处理
LVGL 8.x 和 9.x 在 API 上有一些差异,编写代码时最好统一到项目实际使用的版本。如果你还在学习阶段,建议使用 LVGL 8.3 或 9.x 的官方模拟器工程,先跑通再移植到开发板,这样可以减少环境问题带来的干扰。
11. 总结与后续学习方向
环形无限循环滚动并不是一个需要修改 LVGL 底层的复杂功能,关键在于理解它的本质:内容有限,滚动位置取模。无论是lv_roller的LV_ROLLER_MODE_INFINITE,还是自定义容器里的副本跳转,都是在用不同的方式实现同一个取模逻辑。
这篇文章给出了三种方案:
- 文本选择器场景,直接使用
lv_roller的循环模式。 - 自定义页面轮播场景,使用
scroll_snap + 数据副本 + 滚动事件跳转。 - 自动展示场景,使用
lv_timer + lv_anim驱动滚动位置。
你可以先从方案一跑通,感受 LVGL 的环形滚动效果,再对照方案二理解滚动偏移量的取模逻辑。下一步可以尝试给方案二加入自动轮播,并在真实开发板上测试触摸交互和动画效果。如果遇到页面初始化位置不对、副本数量不够、滚动跳变等问题,优先检查滚动事件回调中的偏移量计算,把边界条件用日志打印出来,问题通常会很快定位。