☰
LVGL环形无限循环滚动实现原理与实战
2026/10/20 6:47:03 网站建设 项目流程

在嵌入式界面开发里,环形无限循环滚动效果是一个看起来很花哨、实现起来又容易踩坑的需求。所谓的环形无限循环滚动效果,就是屏幕上的一组图标或选项围绕圆形轨迹排列,用户左右滑动时整圈连续旋转,没有起点也没有终点,滑到任何一个位置都像是可以继续一直转下去。这种交互常见于智能手表的圆盘菜单、温度或数值选择滚轮、图片轮播,以及在 LVGL 中做自定义控件时经常遇到的旋转选择器。

实现这种效果,看起来只需要计算圆上的坐标,再让角度随着触摸变化就可以。但真实工程里还会遇到角度漂移、触摸事件被子项拦截、惯性结束后不吸附、低性能 MCU 上刷新卡顿等问题。这篇文章会从 LVGL 的角度模型开始,讲清楚环形无限循环的实现原理,再给出一份基于 LVGL 8.3 的可运行示例,最后补充常见的坑和优化方向。

1. 环形无限循环的两种典型实现思路

1.1 使用 LVGL 内置 lv_roller 的受限方案

LVGL 自带了一个滚轮选择器lv_roller,它可以让文本垂直滚动,用户滑动后停在某个选项上。lv_roller在部分版本中支持无限模式,例如LV_ROLLER_MODE_INFINITE。开启无限模式后,列表不再有第一项和最后一项的概念,滚动到最后一项后继续往前滑动,会回到第一项。

这种方案的优点是接入成本低,只需要调用几个 API 就能完成文本选项的循环滚动。适合数字调节、下拉选项、时间选择这类以文本为主的场景。

但它也有明显限制:

  • 布局形态是垂直列表,不是真正的圆形排列。
  • 每个选项只能是一行文本,不容易做成图标、按钮或图片。
  • 控件内部逻辑已经固定,想加入自定义的缩放、透明度渐变、前后遮挡关系会很麻烦。

如果只是需要一个简单的无限选择列表,lv_roller是首选。但文章标题里强调的是“环形”,也就是圆形轨迹上的连续滚动,此时更适合自己做一个小型自定义控件。

1.2 自定义环形转盘:为什么必须使用角度模型

自定义环形转盘的核心思路,是把所有子项的位置和状态统一交给一个旋转角度来控制。

假设屏幕上有 6 个图标,它们均匀分布在半径 90 像素的圆上。那么第i个子项的初始位置角度是:

angle_i = i * (360.0f / item_count)

用户滑动时,不直接修改每个子项的坐标,而是修改一个公共的旋转偏移量:

rotate_angle += touch_delta_x * sensitivity

每个子项的最终角度是:

final_angle_i = angle_i + rotate_angle

再通过三角函数计算圆上的坐标:

x = center_x + radius * cos(final_angle_i * PI / 180.0f) y = center_y + radius * sin(final_angle_i * PI / 180.0f)

使用角度模型而不是直接移动坐标,是环形无限循环能够“无限”的关键。坐标模型有边界,但角度模型没有首尾。当rotate_angle超过 360 度或者小于 0 度时,只需要对 360 取模,布局状态依然有效,子项仍然均匀分布在圆环上。

1.3 环形无限循环的核心不是动画,而是角度归一化

很多人第一次实现这个效果时,会直接用lv_anim做一段从左到右的位移动画,然后在动画结束时把位置重置到起点,再重新播放。这种方式虽然也能制造循环播放的错觉,但它是“播放一个动画”的思路,不是“持续交互”的思路。

真正的无限循环滚动,应该让角度值可以无限制地增长或减少,同时保证数值不会失控。归一化有两种做法:

// 最终布局时取模 float a = fmodf(origin_angle + rotate_angle, 360.0f); if (a < 0.0f) a += 360.0f;

或者定期把rotate_angle本身归一到 0 到 360 的范围内:

rotate_angle = fmodf(rotate_angle, 360.0f); if (rotate_angle < 0.0f) rotate_angle += 360.0f;

如果不做归一化,角度值会随着长时间交互变成很大的浮点数,精度下降后子项位置会出现轻微抖动,最终影响观感。

实现方式适用场景循环能力自定义程度
lv_roller无限模式文本列表、数字选择器自带低
自定义环形转盘图标轮播、圆盘菜单、图片选择器自己控制高,可扩展层级、透明度、缩放

2. 环境准备:先在模拟器里跑通,再考虑真实硬件

2.1 不同平台的移植差异

LVGL 是纯 C 实现,官方提供了多种模拟器工程,常见的有lv_sim_vscode_sdl、lv_sim_eclipse_sdl。在 VSCode 中调试时,可以使用 CMake 工程直接编译 SDL 模拟器,鼠标可以模拟触摸输入,非常适合先验证交互逻辑。

模拟器好处是编译快、可以打印日志、不容易受到硬件内存限制。确认效果后再移植到 STM32、ESP32 等平台,会高效很多。

如果目标是 STM32,通常需要先完成 LCD 驱动、触摸驱动、LVGL 的lv_disp_drv_t和lv_indev_drv_t注册。如果目标是 ESP32,需要注意内存分配方式,尤其是自绘控件产生的临时 buffer 不要放在栈上。

2.2 lv_conf.h 中与动画、事件相关的配置

LVGL 源码中通常有一份lv_conf_template.h,复制为lv_conf.h后需要确认以下配置:

配置项建议值说明
LV_MEM_SIZE8 * 1024 或更大环形转盘包含多个子项,内存不足时对象创建失败
LV_USE_ANIMATION1后续做惯性吸附动画时需要
LV_USE_GESTURE1如果需要使用手势方向事件可以开启
LV_FONT_MONTSERRAT_241示例代码中标签使用 24 号字体

示例代码本身没有使用手势事件,而是使用LV_EVENT_PRESSING读取触摸点,所以不强制开启LV_USE_GESTURE。但实际项目中如果要判断横向滑动,开启该配置会更方便。

2.3 本文代码基于 LVGL 8.3,v9.x 需要注意的差异

文章示例代码主要使用 LVGL 8.3 API。LVGL 9.x 中,大部分基础 API 保持一致,例如lv_obj_set_pos、lv_obj_add_event_cb、lv_timer_create都可以继续使用。但动画、样式渲染以及内部结构体字段有调整。

如果使用 LVGL 9.x,需要注意以下几点:

功能LVGL 8.3LVGL 9.x影响
事件注册lv_obj_add_event_cb(obj, cb, code, user_data)基本一致代码可复用
定时器lv_timer_create(cb, period, user_data)基本一致代码可复用
动画lv_anim_t结构体新增了部分lv_anim_set_*接口,字段有变化使用动画前需要查 API
对象溢出显示LV_OBJ_FLAG_OVERFLOW_VISIBLE保留代码可复用
样式透明度lv_obj_set_style_opa(obj, val, 0)基本一致代码可复用

3. 用角度驱动布局:环形转盘的坐标计算

3.1 定义环形数据结构

先定义一个结构体保存环形转盘的状态。项目规模不大时,可以把所有子项统一放在一个数组中:

#define ITEM_COUNT 6 #define ITEM_SIZE 60 #define RADIUS 90 typedef struct { lv_obj_t * obj; lv_obj_t * label; } ring_item_t; typedef struct { lv_obj_t * layer; ring_item_t items[ITEM_COUNT]; float angle; float velocity; lv_timer_t * timer; } ring_ctx_t; static ring_ctx_t s_ring;

这里把s_ring定义成静态全局变量,是为了让示例代码更容易阅读。真实项目中可以把ring_ctx_t用指针传入事件回调或定时器回调,通过lv_event_get_user_data和lv_timer_get_user_data传递。

3.2 核心布局函数 ring_layout

ring_layout是环形转盘的核心,它负责根据当前角度更新所有子项的位置。

static void ring_layout(ring_ctx_t * ctx) { lv_coord_t cx = lv_obj_get_width(ctx->layer) / 2; lv_coord_t cy = lv_obj_get_height(ctx->layer) / 2; const float deg_to_rad = 3.14159265f / 180.0f; for (int i = 0; i < ITEM_COUNT; i++) { float a = i * (360.0f / ITEM_COUNT) + ctx->angle; a = fmodf(a, 360.0f); if (a < 0.0f) a += 360.0f; float rad = a * deg_to_rad; lv_coord_t x = cx + (lv_coord_t)(RADIUS * cosf(rad)) - ITEM_SIZE / 2; lv_coord_t y = cy + (lv_coord_t)(RADIUS * sinf(rad)) - ITEM_SIZE / 2; lv_obj_set_pos(ctx->items[i].obj, x, y); } }

布局函数做了三件事:

  • 根据序号计算每个子项在圆环上的基础角度。
  • 把基础角度加上当前旋转角度ctx->angle。
  • 通过cosf和sinf计算笛卡尔坐标,并减去半个子项尺寸,让子项中心落在圆环上。

这段代码最需要注意的是坐标系:LVGL 的坐标轴是左上角原点,x 向右增大,y 向下增大。数学上标准的极坐标转换中,y = r * sin(angle)会让角度 90 度出现在下方,这与环形菜单通常“顶部开始”的习惯不同。如果希望第一个子项从顶部开始,可以改为:

lv_coord_t y = cy - (lv_coord_t)(RADIUS * sinf(rad)) - ITEM_SIZE / 2;

具体用哪种方向,取决于产品设计。在示例中为了简单,还是沿用屏幕坐标系的向下 y 方向,这样第一个子项在右侧,后面按顺时针排列。

3.3 通过透明度与层级增强环形立体感

单纯让子项沿圆形排列,看起来只是一个平面圆环,没有前后遮挡的感觉。要增强立体感,可以根据角度调整透明度。

当一个子项的角度在 180 到 360 度之间时,它处于圆环的后半部分,应该更透明;角度在 0 到 180 度时,它处于前半部分,应该完全不透明。

if (a > 180.0f && a < 360.0f) { lv_obj_set_style_opa(ctx->items[i].obj, LV_OPA_50, 0); } else { lv_obj_set_style_opa(ctx->items[i].obj, LV_OPA_100, 0); }

如果需要更真实的遮挡,可以在透明度基础上再调整层级。LVGL 中可以使用lv_obj_move_foreground和lv_obj_move_background。要注意,频繁调用层级调整会有一定开销,在低性能 MCU 上可能造成闪烁。一个折中方案是只调整透明度,不调整层级,因为大部分嵌入式屏幕尺寸不大,透明度差异已经能提供足够的空间感。

3.4 角度归一化的边界处理

fmodf对负数的处理需要特别注意。C 语言标准中,fmodf(-80.0f, 360.0f)的结果并不是 280,而是 -80。如果直接使用负值判断前后关系,会出现错误。

正确的做法是先取模,再根据结果加 360:

float a = fmodf(raw_angle, 360.0f); if (a < 0.0f) a += 360.0f;

这样无论角度怎么增长,最终都落在 0 到 360 度之间。

注意:不要在主循环里高频次地依赖fmodf大量计算。示例中子项数量少,性能可以接受。子项很多时,可以考虑在惯性更新时只对ctx->angle取模,布局函数内部不再重复取模。

4. 手势滑动与惯性滚动实现

4.1 使用 PRESSING 事件记录连续触摸

LVGL 的触摸事件中,LV_EVENT_PRESSED表示按下,LV_EVENT_PRESSING表示按住并移动,LV_EVENT_RELEASED表示释放。我们只需要在layer上注册事件回调,就能拿到连续触摸点。

static void ring_event_cb(lv_event_t * e) { lv_event_code_t code = lv_event_get_code(e); lv_indev_t * indev = lv_indev_get_act(); static lv_point_t last_p; if (code == LV_EVENT_PRESSED) { lv_indev_get_point(indev, &last_p); if (s_ring.timer != NULL) { lv_timer_del(s_ring.timer); s_ring.timer = NULL; } s_ring.velocity = 0.0f; } else if (code == LV_EVENT_PRESSING) { lv_point_t p; lv_indev_get_point(indev, &p); int32_t dx = p.x - last_p.x; s_ring.angle += dx * 0.5f; s_ring.velocity = dx * 0.5f; last_p = p; ring_layout(&s_ring); } else if (code == LV_EVENT_RELEASED) { s_ring.timer = lv_t

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询