news 2026/9/9 19:04:34

LVGL环形无限循环滚动实现:动画、触摸与索引计算详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LVGL环形无限循环滚动实现:动画、触摸与索引计算详解

这次我们来看一个 LVGL 开发里经常被问到的问题:环形无限循环滚动效果。无论是设置界面里的横向菜单,还是智能家居屏幕上的轮播卡片,很多时候我们并不希望列表滚到最后一个就停住,而是希望它能在视觉上无缝地循环回开头。LVGL 自带的滚动逻辑是有边界限制的,swipe 到头部或尾部会触发边界回弹,直接拿来做无限循环会非常别扭。要做出真正的环形效果,通常需要自己处理动画回调、坐标复位和索引计算这几件事。

这篇文章不会绕太多概念,重点讲怎么在 LVGL v8.x/v9.x 上实现横向环形无限循环,并且给出可直接运行的示例代码。核心思路很简单:用一个容器保存一组相同宽度的子项,在末尾多放一个首项的副本,再用取模运算把不断增长的滚动坐标映射到固定周期内,视觉上就不会出现跳变。顺着这个思路,还可以扩展出纵向滚动、自动轮播、触摸拖动和批量列表生成。

如果你正在做 STM32、ESP32 或者 PC 模拟器上的 LVGL 项目,这篇文章可以直接收藏。读完你能知道:环形循环的核心逻辑是什么、怎么写循环动画、如何处理触摸交互、怎么计算当前索引、在 MCU 上运行时怎么观察内存和帧率,以及常见的坑怎么排查。

1. LVGL 环形无限循环滚动核心能力速览

能力项说明
效果类型横向/纵向无限循环滚动、自动轮播、触摸拖动
实现方案容器 + 首项副本 + 取模坐标映射 + lv_anim/lv_timer
适用版本LVGL v8.x 与 v9.x,注意部分 API 差异
输入方式触摸、鼠标、滚轮、编码器,取决于屏和驱动
内存开销低,主要占用来自 item 数量和屏幕重绘区域
硬件门槛低,STM32、ESP32 等 MCU 可运行
可选能力当前索引回调、批量动态创建列表项
启动方式嵌入式项目编译烧录,或 PC 模拟器运行
接口能力无 Web API,可把滚动模块封装成可复用的 C 接口
适合场景车载菜单、仪表盘、智能家居面板、彩屏轮播控件

先说结论:这个效果不需要额外第三方库,LVGL 自身的动画系统、对象坐标系和事件机制已经足够。代码量也不大,核心逻辑集中在取模运算和一个动画回调里,很好维护。真正影响工作量的是你如何处理触摸手势和索引回调,这两点决定了它能不能从“演示效果”变成“可用控件”。

2. LVGL 环形无限滚动效果的设计思路

首先解释为什么原生滚动不够用。LVGL 里lv_objscroll特性,但它的滚动范围受父容器大小限制,滚动到边界后还会触发回弹效果。你要实现无限循环,本质上是在和一个“有界”的滚动模型做对抗。与其修改原生滚动逻辑,不如用更基础的坐标系操作来绕过它:把多个子项放进一个容器,容器在父视图里移动,只要移动范围在一个周期内循环,视觉上就是无限滚动。

实现时有两个核心问题要解决:

第一是视觉上的无缝衔接。如果容器只在 0 到-(N * ITEM_W)之间往复,用户一定会看到“滚到底再弹回来”的过程,这不是环形效果。正确做法是在容器末尾多放一个“首项副本”,让最后一个可视位置和第一个可视位置内容一致,这样当容器坐标跳回起点时,画面看起来没有变化。副本数量不需要额外再加,常见做法是N + 1个对象,最后一个与第一个内容相同。

第二是坐标不能无限增大。如果你用一个不断累加递减的变量来记录滚动位置,几十秒后int32_t会溢出,而且一旦需要回绕,还要额外写很多边界判断。更稳定的做法是用取模运算:让一个动画值v从 0 持续往下走,实际显示的坐标永远是v % TOTAL_W,其中TOTAL_W = ITEM_W * ITEM_NUM。这样容器坐标始终被限制在0 ~ -TOTAL_W之间,既不会溢出,也不需要手动“重置”。

从实现方式上看,常见的有三种:

  1. lv_anim线性动画加取模坐标映射,适合自动轮播和无缝循环。
  2. 触摸事件驱动坐标更新,按下拖动时直接改容器坐标,适合手势交互。
  3. lv_timer定时推进,逻辑最简单,适合在资源有限的 MCU 上做演示。

本文主推第一种,并在此基础上扩展触摸手势。因为lv_anim自带帧回调、动画路径和重复次数,能减少很多定时器代码。

3. LVGL 开发环境准备与前置条件

在写具体代码之前,先把环境确认好。环形滚动本身不挑平台,LVGL 能跑起来的地方都可以做,但推荐按下面两种方式准备:

3.1 PC 模拟器环境

先用 PC 模拟器跑通效果,调试效率最高。官方提供了基于 SDL 和 Eclipse 的模拟器工程,也可以在 VS Code 里配置 CMake 工程,把 LVGL 源码包进来,再接入一个模拟器驱动。模拟器不需要真实屏幕和触摸驱动,鼠标移动可以模拟触摸滑动,非常适合验证滚动逻辑、动画回调、索引计算这些核心功能。

建议固定使用一个版本,比如 v8.3 或 v9.x。不要混合使用不同版本的头文件,LVGL v8 到 v9 之间 API 有一些变化,比如部分lv_obj函数名、事件获取方式、动画初始化方式都存在差异。本文代码以 v8.x 的写法为主,v9 用户编译时结合官方迁移说明调整即可。

3.2 MCU 平台环境

如果直接做 STM32 或 ESP32 移植,需要准备以下部分:

  • LVGL 源码,建议以子模块或固定版本目录方式管理。
  • 显示屏驱动,确认分辨率、颜色深度和刷新接口。
  • 触控驱动,返回坐标点给lv_indev
  • 一个定时器或者时基,LVGL 的lv_tick_inc()需要定期调用。
  • 内存堆配置,确认LV_MEM_CUSTOM或者lv_mem的堆大小足够。

环境准备的重点不是版本越新越好,而是“锁定版本、保持稳定”。环形滚动这种功能对帧率有一定要求,如果刷新链路或者触控链路不稳定,后面排查问题时会分不清是代码问题还是驱动问题。

3.3 通用检查清单

检查项说明
LVGL 版本固定 v8.x 或 v9.x,不要混用
屏幕分辨率决定视口大小和裁剪区域
颜色深度常见 RGB565、RGB888,影响内存和刷新速度
内存配置确认堆大小足够创建页面对象
字体包含需要的数字、英文、中文,避免显示方块
输入设备触摸、鼠标、滚轮任选其一
刷新缓冲单缓冲省内存,双缓冲减少撕裂

4. 横向环形无限循环滚动核心代码实现

下面给出一段完整的 LVGL 横向环形无限循环示例代码。先定义一个固定宽度和高度的卡片区域,然后创建N + 1个子项,其中最后一项复制第一项的内容,用于视觉无缝衔接。动画值v从 0 一直递减到-(TOTAL_W * 2),执行回调时用v % TOTAL_W映射到合法坐标范围,动画设置无限重复,这样容器就会一直循环滚动。

#include "lvgl.h" #define ITEM_W 100 #define ITEM_H 80 #define ITEM_NUM 6 #define TOTAL_W (ITEM_W * ITEM_NUM) /* 600 */ static void circular_scroll_cb(void *var, int32_t v) { lv_obj_t *wrap = (lv_obj_t *)var; /* v 持续递减,取模后映射到 0 ~ -TOTAL_W */ lv_obj_set_x(wrap, v % TOTAL_W); } static void start_loop_anim(lv_obj_t *wrap) { lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, wrap); lv_anim_set_exec_cb(&a, circular_scroll_cb); lv_anim_set_time(&a, 12000); lv_anim_set_values(&a, 0, -(TOTAL_W * 2)); lv_anim_set_path_cb(&a, lv_anim_path_linear); lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE); lv_anim_start(&a); } void lv_demo_circular_scroll(void) { lv_obj_t *parent = lv_obj_create(lv_scr_act()); lv_obj_set_size(parent, 320, ITEM_H); lv_obj_align(parent, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_border_width(parent, 1, 0); lv_obj_set_style_clip_corner(parent, true, 0); lv_obj_t *wrap = lv_obj_create(parent); lv_obj_remove_style_all(wrap); lv_obj_set_size(wrap, ITEM_W * (ITEM_NUM + 1), ITEM_H); lv_obj_set_pos(wrap, 0, 0); for (int i = 0; i < ITEM_NUM + 1; i++) { int idx = i % ITEM_NUM; lv_obj_t *card = lv_obj_create(wrap); lv_obj_set_size(card, ITEM_W - 4, ITEM_H - 4); lv_obj_set_pos(card, i * ITEM_W + 2, 2); lv_obj_set_style_radius(card, 8, 0); lv_obj_set_style_bg_color(card, lv_palette_main(idx), 0); lv_obj_t *label = lv_label_create(card); lv_label_set_text_fmt(label, "Item %d", idx + 1); lv_obj_center(label); } start_loop_anim(wrap); }

代码里需要注意几个点:

  1. wrap的宽度是ITEM_W * (ITEM_NUM + 1),比实际循环周期多了一个 item,这是为了放首项副本。
  2. TOTAL_W是真正的循环周期ITEM_W * ITEM_NUM,取模时用它。
  3. lv_obj_set_style_clip_corner(parent, true, 0)很重要,它让父容器裁剪掉超出边界的子项,否则移出视口的内容会直接显示出来,破坏视觉效果。
  4. lv_anim_set_timelv_anim_set_values配合起来看,可以实现匀速移动。上面设置 12 秒内移动-1200px,会循环两轮,每一轮 6 秒,每个 item 停留约 1 秒,速度比较适中。

这段代码在 PC 模拟器上可以直接调用lv_demo_circular_scroll(),编译运行后就能看到卡片循环向左滚动。验证方法很简单:看最后一个卡片划出左边后,第一张卡片是不是从右侧无缝进入,中间没有空白也没有跳变。

5. 触摸滑动交互扩展

自动轮播只是第一步,实际项目中用户更希望“能拖动”。触摸拖动和自动轮播的最大区别是:自动轮播由动画值驱动,触摸拖动则由触摸坐标差驱动容器位置。两者可以共存,但需要在事件回调里切换状态。

实现思路如下:

  1. 按下时删除正在运行的循环动画,记录按下时的容器坐标和触摸点。
  2. 拖动过程中,计算当前触摸点和上次触摸点的差值,把差值叠加到容器坐标上,并用取模回绕保持范围。
  3. 释放时重新启动自动循环动画,或者根据速度做惯性归位。
static lv_obj_t *g_wrap; static lv_point_t g_last_point; static lv_coord_t g_last_x; static void wrap_event_cb(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t *wrap = lv_event_get_target(e); lv_indev_t *indev = lv_indev_get_act(); lv_point_t p; if (code == LV_EVENT_PRESSED) { lv_indev_get_point(indev, &p); g_last_point = p; g_last_x = lv_obj_get_x(wrap); lv_anim_del(wrap, circular_scroll_cb); } else if (code == LV_EVENT_PRESSING) { lv_indev_get_point(indev, &p); int32_t dx = p.x - g_last_point.x; g_last_point = p; int32_t pos = g_last_x + dx; if (pos >= 0) pos -= TOTAL_W; if (pos <= -TOTAL_W) pos += TOTAL_W; g_last_x = pos; lv_obj_set_x(wrap, pos); } else if (code == LV_EVENT_RELEASED) { start_loop_anim(wrap); } }

然后把事件回调挂到容器上:

lv_obj_add_event_cb(wrap, wrap_event_cb, LV_EVENT_ALL, NULL);

这里有一个细节:LVGL 的事件会向父级冒泡,所以触摸卡片时wrap也能收到按下、拖动、释放事件。如果你发现事件收不到,优先检查是否有其他对象拦截了指针事件,或者把事件挂到更上层的父容器上再试。

如果拖动后不想立刻恢复自动滚动,可以在LV_EVENT_RELEASED里改成“回弹到最近的一个整数 item 位置”,类似轮播卡片停在整张卡片的中心。做法是释放时取index = (-x + ITEM_W / 2) / ITEM_W,然后启动一个短动画,把x平移到-index * ITEM_W,这个短动画可以把lv_anim_set_time设置为 200ms,路径用lv_anim_path_ease_out,手感和原生的惯性吸附比较接近。

6. 当前索引计算与事件回调接口

无线循环滚动之后,业务层真正关心的是“当前显示的是哪一项”。这里有一个很简洁的计算方式:

int get_current_index(int32_t x) { int idx = (-x) / ITEM_W; return idx % ITEM_NUM; }

因为x一直被限制在0 ~ -TOTAL_W之间,-x的范围是0 ~ TOTAL_W,除以ITEM_W得到 0 到ITEM_NUM之间的数值,再取模就映射回 0 到ITEM_NUM - 1。即使动画值因为取模出现跳变,这个索引计算结果也是连续的,不会出现突然跳到错误项的情况。

在实际项目中,不建议直接用全局变量传递索引,而是封装成一个环形滚动组件的回调。例如定义:

typedef void (*circular_scroll_index_cb_t)(int index); static circular_scroll_index_cb_t s_index_cb;

在滚动回调或者释放回调里调用s_index_cb(current_index),这样业务层只需要注册一个回调,就能实时拿到当前项。对于车载菜单、仪表盘、家居面板这类场景,这比在多个文件里查找坐标计算逻辑要干净得多。

批量创建列表项也可以在这个阶段一起考虑。如果是固定数量且数量不大,直接创建N + 1个对象即可;如果是几百上千条数据,务必改成对象池方式,只显示视口内可见的少量对象,更新数据时只改文本和图标,不要再创建新对象,否则内存和初始化耗时都会失控。

7. 纵向环形滚动与二维网格扩展

横向滚动改造成纵向滚动非常容易。把lv_obj_set_x(wrap, ...)改成lv_obj_set_y(wrap, ...),把ITEM_W换成ITEM_HTOTAL_W换成TOTAL_H,触摸事件里取dy而不是dx,其余逻辑完全不变。纵向环形滚动在设置列表、频道选择、数值选择等场景下很常见。

如果把问题扩展到二维网格的环形滚动,比如一个 4 列 2 行的卡片矩阵,希望在横向和纵向都能无限循环,难度会明显上升。你需要同时维护 x 和 y 两个方向的周期,还要在容器里复制多份首行和首列的子项,坐标取模也要变成二维取模。这个方案不是不能做,但如果在低主频 MCU 上跑,重绘面积和对象数量会成倍增长,不建议在性能有限的平台上硬上。更稳妥的做法是缩小视口裁剪区域,或者干脆用单方向环形滚动,另一方向使用普通分页。

另外,如果只需要文字列表的环形选择,LVGL 自带的lv_roller控件本身就能实现循环,不需要自己写滚动

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/2 10:24:36

QGIS二次开发入门:拆解官方PyQGIS例子,掌握核心API

简介&#xff1a;本资源是QGIS官方示例代码合集&#xff0c;面向地理信息系统初学者、二次开发人员及开源GIS工具实践者&#xff0c;旨在解决国内QGIS编程学习资料匮乏、示例零散、入门门槛高的实际问题。压缩包共220个文件&#xff0c;涵盖C源码&#xff08;20个.cpp、11个.h&…

作者头像 李华
网站建设 2026/9/5 16:33:43

皇室战争狗球流进阶:熔岩猎犬+气球兵防守反击节奏全拆解

最近一直在练空中推进体系&#xff0c;打得多了以后发现&#xff0c;狗球流并不是很多人以为的“无脑沉底狗、跟上球就完事”。它其实是一套对节奏、圣水计算、防守牌顺序要求都很高的卡组&#xff0c;尤其是在对手手里捏着法术、针对牌的情况下&#xff0c;能不能把进攻窗口打…

作者头像 李华
网站建设 2026/9/6 1:04:43

Overlay相机技术解析:从图层叠加到实时合成

第一次看到“绵绵很好_overlay”这个项目名时&#xff0c;我的第一反应是&#xff1a;这应该又是一个随手练手的小项目。名字像是一句碎碎念&#xff0c;但“overlay”这个关键词把主题说得很清楚——叠加层。再配合最近“overlay相机”这个热词&#xff0c;你会发现这类东西其…

作者头像 李华
网站建设 2026/9/6 8:16:32

Simulink Subsystem Reference:模块复用与模型组织新方案

在 Simulink 中面对一个动辄上千个模块、信号线密集到几乎无法滚动查找的模型时&#xff0c;很多工程师心里都会冒出同一个念头&#xff1a;如果这套逻辑能像写函数一样&#xff0c;定义一次、引用多次、修改一处全局生效&#xff0c;该有多省心。R2020a 之前&#xff0c;这个问…

作者头像 李华
网站建设 2026/9/5 15:58:35

从素材到成品:用ffmpeg与Whisper搭建短视频批量二创流水线

好&#xff0c;这次我们不聊代码仓库&#xff0c;也不聊模型权重&#xff0c;而是拆一个看起来一点都不技术的短视频标题&#xff1a;【盒盒短视频】TREASURE 260809 | D to the E, to the L-I-C-I-O-U-S&#xff5c;玹硕 道荣 炡禹第一眼是偶像物料&#xff0c;第二眼是粉丝二…

作者头像 李华
网站建设 2026/9/2 10:10:50

英伟达AGI热议背后:开发者必须掌握的驱动安装与模型部署实战

关于“英伟达再次实现AGI”的讨论&#xff0c;这两天在技术群里又刷屏了。每次黄仁勋在公开场合提到 AGI&#xff0c;舆论都会分两派&#xff1a;一派认为通用人工智能近在眼前&#xff0c;另一派则觉得这只是发布会上的“叙事包装”。我的看法更偏向后者——AGI 能不能实现、何…

作者头像 李华