资讯动态

LVGL环形无限循环滚动实现原理与实战

发布时间:2026/8/31 5:20:06 来源:尧图企业网站定制
在嵌入式界面开发里环形无限循环滚动效果是一个看起来很花哨、实现起来又容易踩坑的需求。所谓的环形无限循环滚动效果就是屏幕上的一组图标或选项围绕圆形轨迹排列用户左右滑动时整圈连续旋转没有起点也没有终点滑到任何一个位置都像是可以继续一直转下去。这种交互常见于智能手表的圆盘菜单、温度或数值选择滚轮、图片轮播以及在 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.3v9.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_layoutring_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。如果直接使用负值判断前后关系会出现错误。正确的做法是先取模再根据结果加 360float 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

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价