ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LVGL实现环形无限循环滚动:三种高效方案

LVGL实现环形无限循环滚动:三种高效方案 很多嵌入式开发者在做 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 snapscroll 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 callbackLVGL 提供了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 的环形滚动效果再对照方案二理解滚动偏移量的取模逻辑。下一步可以尝试给方案二加入自动轮播并在真实开发板上测试触摸交互和动画效果。如果遇到页面初始化位置不对、副本数量不够、滚动跳变等问题优先检查滚动事件回调中的偏移量计算把边界条件用日志打印出来问题通常会很快定位。
返回列表