免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

LVGL实现环形无限循环滚动效果及无缝边界处理

LVGL实现环形无限循环滚动效果及无缝边界处理 最近在做一个基于 LVGL 的嵌入式界面时我遇到一个很常见的需求一个横向菜单卡片要求像旋转木马一样一直往一个方向滚动停不下来。也就是标题里说的“lvgl实现环形无限循环滚动效果”。我最初的做法很简单用 lv_anim 驱动滚动位置滚动到边界时触发一次回调把 scroll_x 重置为 0。听起来毫无问题真正跑起来才发现动画最后一帧显示第 6 张卡片下一帧瞬间跳回第 1 张卡片肉眼一下就看出穿帮。整个效果从“旋转木马”变成了“幻灯片翻页”。后来我把问题从“怎么让动画继续滚”换成了“怎么在边界处让画面无缝切换”实现反而清晰了很多。这篇文章就从这个角度展开真正难的从来不是让 LVGL 滚起来而是让滚动边界对用户不可见。1. 先看一个问题为什么直接减位移不是无限滚动1.1 “看起来像无限循环”不等于“能一直滚”很多新手包括我第一次做的时候对“无限循环滚动”的理解是把动画播放完然后跳回起点重新播放。这在短视频轮播或者网页 banner 上也许够用但在嵌入式交互里这种做法几乎一定会被用户察觉。原因在于人眼对“连续运动”的敏感度很高。动画最后一帧如果显示的是最后一个 item下一帧突然变成第一个 item两张图之间没有任何过渡哪怕只差 0.1 秒大脑也会捕捉到这次不连续。用户看到的不是“循环”而是“跳变”。真正的环形无限循环滚动在视觉上需要同时满足三个条件任何时刻当前可见区域内的内容序列都是连续的。滚动位置始终落在一个有限的数值范围内不会越界。外部无论请求多少次“下一次滚动”都能得到合法的位置和内容。如果只是“滚到边界然后归零”第二条确实满足了但第一条在归零瞬间不成立第三条本质上也没有真正处理。1.2 三种主流实现路线LVGL 生态里常用无限滚动实现思路大体有三种先放在一起对比方案实现思路优点缺点适合场景lv_roller官方控件自带无限模式开箱即用、代码量极少只能做文本行列表样式定制受限年月日选择器、菜单选择器克隆拼接法内容序列复制一份首尾相接滚动位置对序列总宽度取模视觉无缝、连续性强、适合任意控件内存多占一份 item需要保证克隆内容一致横滑卡片、轮播图、连续列表索引刷新法维护逻辑索引滚动到边界时重建可见 item内存占用小、状态可管理连续滚动时容易穿帮需要处理对齐步进式菜单、编码器/按键切换我的判断是如果做“连续横向滚动”效果克隆拼接法优先级最高它最贴近“环形”这个词的本意如果只是做“选项循环”直接用 lv_roller 或索引刷新法就够了完全没必要引入复杂动画。后面所有实现都围绕克隆拼接法展开。2. 底层机制LVGL 滚动事件与动画回调决定了你能不能接到“边界”2.1 scroll_x、scrollable 容器和动画 API 的关系LVGL 里一个对象只要内容尺寸超过自身尺寸并且设置了LV_OBJ_FLAG_SCROLLABLE就会成为可滚动对象。水平滚动时内部维护的scroll_x表示内容相对容器的水平偏移量。驱动scroll_x变化通常有三种方式用户触摸拖动LVGL 自动修改scroll_x。调用lv_obj_scroll_to_x()可以带动画参数。用lv_anim创建动画把某个变量或对象属性作为目标。第三种方式自由度最高尤其适合做自动轮播。但要注意lv_anim和lv_obj_scroll_to_x最终都在 LVGL 的渲染循环里执行动画回调跑的是 UI 线程不是后台线程。这意味着动画回调里绝对不能做耗时操作比如磁盘读写、网络请求、大内存分配或者频繁日志打印。否则会导致刷新率下降甚至整个界面卡住。这也是搜索材料里很多人遇到“esp32p4 lvgl 卡死”一类问题时需要优先排查的方向之一。2.2 为什么要在动画完成或滚动结束时做回卷环形滚动最关键的节点是回卷。回卷的本质是把当前滚动位置从“第二遍内容的某个位置”映射回“第一遍内容的同一个相对位置”。在 LVGL 里这个操作最好放在以下时机之一动画结束回调滚动结束事件LV_EVENT_SCROLL_END定时器回调中发现位置越过边界的那一帧不建议在滚动过程中频繁判断和回卷。原因有两个第一滚动过程中scroll_x处于连续变化状态如果反复回卷可能出现每次回卷都触发事件、再触发回卷的死循环逻辑上很难收敛。第二用户触摸滚动时滚动位置由手势决定状态变化不可预测。如果程序在触摸过程中强制修改scroll_x很容易造成“手没有松开画面却被拉回”的怪异体验。更稳妥的做法是由程序控制的自动滚动在定时器或动画回调里完成位置推进和回卷用户手动滚动结束后在LV_EVENT_SCROLL_END里统一校正边界。这个设计看起来简单但在实际项目中会直接影响整个滚动组件是否稳定。3. 从最小示例到通用封装一个可复用的环形滚动实现3.1 先搭一个最小的横滑容器在 LVGL 8.x 里创建一个横向可滚动容器往里面放几个同样宽度的 item是很容易的。下面是一个基础示例#define ITEM_W 120 #define ITEM_H 160 #define DATA_NUM 6 static lv_obj_t * cont; static void create_items(lv_obj_t * parent) { for (int i 0; i DATA_NUM * 2; i) { lv_obj_t * card lv_obj_create(parent); lv_obj_set_size(card, ITEM_W, ITEM_H); lv_obj_set_pos(card, i * ITEM_W, 0); lv_obj_t * label lv_label_create(card); lv_label_set_text_fmt(label, item %d, i % DATA_NUM); lv_obj_center(label); } } void ui_init(void) { lv_obj_t * scr lv_scr_act(); cont lv_obj_create(scr); lv_obj_set_size(cont, 480, ITEM_H); lv_obj_set_style_pad_all(cont, 0, 0, 0); lv_obj_set_scroll_dir(cont, LV_DIR_HOR); lv_obj_add_flag(cont, LV_OBJ_FLAG_SCROLLABLE); create_items(cont); }这里创建了DATA_NUM * 2个 item也就是把真实数据复制了一份。前 6 个和后 6 个内容完全一致只是label文本按照i % DATA_NUM生成。注意这个示例是为了展示核心结构。实际项目里如果 item 是图片或复杂布局不能简单复制 label最好把创建 item 的逻辑抽成一个函数传入索引这样每次创建的都是真正一致的副本。3.2 核心循环逻辑克隆拼接 取模认真看这个位置关系内容序列是0,1,2,3,4,5,0,1,2,3,4,5每个 item 宽度为ITEM_W。当scroll_x 0时可见区域从第一个 0 号 item 开始。当scroll_x DATA_NUM * ITEM_W时可见区域从第二个 0 号 item 开始。因为第二个 0 号 item 和第一个完全一样所以这两个位置画面对用户完全一致。于是“无限循环”就变成了一个非常简单的数学问题让scroll_x在到达或越过DATA_NUM * ITEM_W时减去DATA_NUM * ITEM_W。static void on_auto_scroll_timer(lv_timer_t * timer) { int x lv_obj_get_scroll_x(cont); x ITEM_W; if (x DATA_NUM * ITEM_W) { x - DATA_NUM * ITEM_W; } lv_obj_scroll_to_x(cont, x, LV_ANIM_ON); }这个定时器每隔一定时间推进一个 item 宽度。到达边界时先减去总宽度再设置滚动位置用户看不到任何跳变。注意取模的目标不是让scroll_x变成 0而是让它回到和当前位置“同一画面”的最早位置。理解
返回列表