
LVGL 里的lv_arc这个控件属于那种默认能用但离好看差着十万八千里的典型。我第一次在 STM32 上把圆弧跑起来的时候看着那个灰扑扑、粗细均匀、端点方方正正的圈心里就一个念头这玩意儿要是直接放到产品上客户怕是要退货。后来折腾了 FreeRTOS LVGL 的移植、又在 PC 模拟器上反复调样式才慢慢摸清楚这个控件到底能玩出多少花样。这篇就把我从默认丑到高级感踩过的路完整梳理一遍涉及lv_arc的样式属性、渐变、动画、自定义绘制几个层面适合已经能把 LVGL 跑起来、但被圆弧颜值卡住的嵌入式 UI 开发者。不管你是用 Keil 移植 STM32还是在 Linux 上跑 LVGL 模拟器思路都是通用的。1. 先搞清楚 lv_arc 到底由哪几块零件组成很多人调样式调不明白根本原因是一上来就改属性却没弄明白这个控件在渲染层面被拆成了几个部分。LVGL 的lv_arc本质上不是一个圆环而是由背景弧background arc和前景弧indicator arc两条弧叠加出来的再加上一个可选的旋钮knob。这三块各自有独立的样式部件part你改哪一块得先找对 part。1.1 三个核心 part 的职责划分LVGL 为lv_arc定义了三个样式部件分别对应Part 名称宏定义作用背景弧LV_PART_MAIN整条圆弧的底色也就是轨道前景弧LV_PART_INDICATOR当前值覆盖的那一段从起始角度画到当前值旋钮LV_PART_KNOB圆弧末端那个可拖拽的小圆点这里有个特别容易踩的坑背景弧和前景弧的起始角度是共享的但它们的绘制方向、圆角、宽度都可以分别设置。我见过有人把LV_PART_MAIN的arc_width调得很粗结果LV_PART_INDICATOR没跟着调画出来前景弧细得像根线浮在粗轨道上面特别难看。所以第一条经验就是改宽度、改圆角两个 part 要成对改。1.2 角度系统0 度在哪正方向朝哪lv_arc的角度不是我们数学课上熟悉的从正右方逆时针而是从正右方3 点钟方向开始顺时针为正。lv_arc_set_bg_angles(arc, start, end)里的 start 和 end 都是这个坐标系下的角度值范围 0 到 360。举个实际例子如果你想做一个从顶部12 点开始、顺时针扫到 6 点的半圆仪表盘那 start 应该是 270end 是 90因为 270 度在正上方顺时针转到 90 度正好经过右侧。这个角度换算我一开始老是搞反后来干脆在纸上画了个表盘标好 0/90/180/270 的位置贴显示器边上调一次看一次几次之后就形成肌肉记忆了。提示lv_arc_set_angles()设置的是前景弧的角度lv_arc_set_bg_angles()设置的是背景弧的角度。两者独立但前景弧的角度范围必须落在背景弧范围内才符合视觉预期。1.3 默认样式为什么丑默认情况下LVGL 给lv_arc的配置大概是这样的背景弧和前景弧宽度相同大约 4 到 8 像素取决于主题端点没有圆角颜色用的是主题的灰色和主色旋钮默认隐藏或者是个很小的方块。这套配置在功能验证阶段完全够用但放到产品 UI 里就显得工程感太重——线条生硬、没有层次、缺乏视觉焦点。理解了这三块零件和角度系统后面的美化才有落脚点。接下来所有的技巧本质上都是在分别控制这三个 part 的视觉表现。2. 让圆弧从生硬变柔和的四个基础属性在玩渐变、动画这些花活之前先把最基础的四个属性调对圆弧的质感就能提升一大截。这四个属性是弧宽arc_width、圆角rounded、颜色arc_color、旋钮尺寸pad 相关。它们决定了圆弧的骨架质感是高级感的地基。2.1 arc_width粗细对比是层次感的来源arc_width控制弧线的粗细单位是像素。默认值通常偏细而且背景弧和前景弧一样粗。我的做法是让背景弧略粗、前景弧略细或者反过来制造出轨道 进度的层次。具体操作上用lv_obj_set_style_arc_width()分别设置// 背景弧粗一些作为轨道 lv_obj_set_style_arc_width(arc, 12, LV_PART_MAIN); // 前景弧稍细作为进度指示 lv_obj_set_style_arc_width(arc, 8, LV_PART_INDICATOR);这里有个数值上的经验弧宽最好不要超过控件半径的 1/4。假设你的圆弧控件是 200x200半径 100那弧宽控制在 25 以内比较协调。超过这个比例圆弧会显得笨重中间的空白区域被挤压整体比例失调。我在一个 120x120 的小圆弧上试过 20 像素的弧宽结果中间只剩一个很小的洞看起来像个甜甜圈而不是仪表盘。2.2 rounded一个布尔值带来的质变rounded属性决定弧线两端是不是圆头。默认是关闭的所以弧线两端是平的、方的。把它打开弧线两端变成半圆整个控件立刻柔和很多。lv_obj_set_style_arc_rounded(arc, true, LV_PART_MAIN); lv_obj_set_style_arc_rounded(arc, true, LV_PART_INDICATOR);这个属性对前景弧尤其重要。因为前景弧的末端就是当前进度的位置圆头会让这个位置看起来更自然像是笔触收尾。不过要注意当弧宽很细比如 2 到 3 像素时圆角效果几乎看不出来因为圆头半径太小。所以圆角和弧宽要配合着调弧宽至少 6 像素以上圆角才有明显效果。2.3 arc_color别只用主题色颜色是最直观的美化手段。默认主题给的颜色往往比较保守你可以大胆换。但换色有个原则背景弧用低饱和度的中性色前景弧用高饱和度的强调色这样对比强烈进度一目了然。// 背景弧用深灰低调 lv_obj_set_style_arc_color(arc, lv_color_hex(0x2A2A2A), LV_PART_MAIN); // 前景弧用亮青醒目 lv_obj_set_style_arc_color(arc, lv_color_hex(0x00E5FF), LV_PART_INDICATOR);在深色背景的 UI 上这种暗轨道 亮进度的组合特别出效果。我做过一个车载风格的界面背景纯黑圆弧轨道用0x1A1A1A进度用0x00BFFF配合圆角看起来相当有科技感。反过来浅色背景上就用浅灰轨道配深色进度。2.4 旋钮的尺寸与位置微调旋钮knob默认可能不显示或者显示得很小。要让它出现并好看需要设置它的尺寸和背景。旋钮的尺寸通过lv_obj_set_style_pad_all()或者直接设置宽高来调但更常用的方式是给它一个明显的背景色和圆角。// 让旋钮显示出来 lv_obj_set_style_bg_color(arc, lv_color_hex(0xFFFFFF), LV_PART_KNOB); lv_obj_set_style_bg_opa(arc, LV_OPA_COVER, LV_PART_KNOB); // 旋钮做成圆形 lv_obj_set_style_radius(arc, LV_RADIUS_CIRCLE, LV_PART_KNOB); // 通过 pad 控制旋钮大小 lv_obj_set_style_pad_all(arc, 6, LV_PART_KNOB);旋钮的pad值决定了它比弧线末端大出多少。pad 越大旋钮越大。我一般让旋钮直径比弧宽大 4 到 8 像素这样它既能盖住弧线末端又不会大得突兀。旋钮颜色通常用白色或前景弧的亮色形成视觉焦点。把这四个属性调好圆弧已经从默认丑进化到能看了。但要做到高级感还得往下走。3. 渐变、阴影与多层叠加高级感的真正来源基础属性解决的是不难看而渐变、阴影、多层叠加解决的是好看且有质感。这一层是区分普通 UI 和精致 UI 的分水岭。LVGL 从 8.x 开始对渐变和阴影的支持已经比较完善lv_arc也能吃到这些红利。3.1 给前景弧加线性渐变LVGL 的样式支持bg_grad系列属性可以给弧线加渐变。虽然名字叫 bg_grad但它对LV_PART_INDICATOR同样生效。渐变能让进度弧从一种颜色平滑过渡到另一种视觉上更有动感。lv_obj_set_style_bg_grad_color(arc, lv_color_hex(0xFF00FF), LV_PART_INDICATOR); lv_obj_set_style_bg_grad_dir(arc, LV_GRAD_DIR_HOR, LV_PART_INDICATOR); lv_obj_set_style_bg_color(arc, lv_color_hex(0x00E5FF), LV_PART_INDICATOR);上面这段代码让前景弧从青色渐变到品红方向是水平。实际效果是圆弧的起点和终点颜色不同中间平滑过渡。这里要注意渐变方向是相对于控件的包围盒不是相对于弧线走向。所以水平渐变在圆弧上看起来是左边一种色、右边一种色而不是沿着弧线从起点渐变到终点。想要沿弧线渐变LVGL 原生支持有限需要靠自定义绘制或者分段设置颜色来模拟。我试过用分段的方式模拟沿弧渐变把圆弧拆成若干小段每段单独设色但这样代码复杂且性能一般。更实际的做法是接受水平/垂直渐变的效果或者干脆用纯色 阴影来营造质感。3.2 阴影让圆弧浮起来阴影shadow是提升质感的利器。给前景弧加一层同色系的阴影能让它看起来像是从背景上浮起来有立体感。lv_obj_set_style_shadow_color(arc, lv_color_hex(0x00E5FF), LV_PART_INDICATOR); lv_obj_set_style_shadow_width(arc, 20, LV_PART_INDICATOR); lv_obj_set_style_shadow_opa(arc, LV_OPA_50, LV_PART_INDICATOR); lv_obj_set_style_shadow_spread(arc, 2, LV_PART_INDICATOR);shadow_width控制阴影扩散范围shadow_opa控制透明度shadow_spread控制阴影从边缘向外扩展的距离。我的经验是阴影颜色用前景弧的颜色透明度控制在 30% 到 50%这样出来的是一种发光效果而不是脏兮兮的灰影。在深色背景上这种同色发光特别好看有种霓虹灯的感觉。不过阴影是有性能代价的。在 STM32 这类资源受限的平台上大面积阴影会明显增加渲染负担。如果你的圆弧在动画中频繁重绘阴影宽度建议控制在 10 到 15 像素以内透明度也别太高。我在 F429 上跑过一个带 20 像素阴影的圆弧动画帧率掉得比较明显后来降到 12 像素就流畅多了。3.3 多层圆弧叠加做出刻度环想要更高级的效果可以用多个lv_arc叠加。比如底层放一个细的、带很多小段的圆弧当刻度环上层放一个粗的进度弧。刻度环可以用多个小角度的 arc 拼出来或者用一个 arc 配合LV_PART_MAIN的虚线效果LVGL 部分版本支持 dash。更简单的做法是两个 arc 控件叠在同一个位置一个负责外圈细刻度一个负责内圈粗进度。用lv_obj_align()让它们居中对齐尺寸略有差异。lv_obj_t *outer lv_arc_create(lv_scr_act()); lv_obj_set_size(outer, 200, 200); lv_obj_center(outer); lv_obj_set_style_arc_width(outer, 2, LV_PART_MAIN); lv_obj_set_style_arc_color(outer, lv_color_hex(0x444444), LV_PART_MAIN); lv_obj_t *inner lv_arc_create(lv_scr_act()); lv_obj_set_size(inner, 170, 170); lv_obj_center(inner); lv_obj_set_style_arc_width(inner, 10, LV_PART_INDICATOR);这种外细内粗的双层结构视觉层次非常丰富做仪表盘特别合适。外圈可以再配合多个小 arc 做刻度点虽然代码量上去了但效果确实值。3.4 用透明度做未激活状态高级 UI 里经常有激活/未激活的状态区分。对于圆弧可以用透明度来表现未激活的圆弧整体降低不透明度激活时恢复。// 未激活 lv_obj_set_style_arc_opa(arc, LV_OPA_30, LV_PART_INDICATOR); // 激活 lv_obj_set_style_arc_opa(arc, LV_OPA_COVER, LV_PART_INDICATOR);配合lv_anim做透明度过渡切换状态时会有淡入淡出的效果比生硬地切换颜色高级得多。这个技巧在多圆弧的仪表盘上特别好用——当前选中的那个圆弧全亮其他的半透明视觉焦点自然就出来了。4. 动画与交互让圆弧活起来静态的圆弧再好看也只是个装饰。真正让 UI 有生命力的是动画和交互。lv_arc本身支持拖拽交互但默认的交互反馈比较朴素需要额外加工。4.1 用 lv_anim 做数值平滑过渡直接lv_arc_set_value()会让圆弧瞬间跳到目标值很生硬。用lv_anim做插值圆弧会平滑地扫过去。static void arc_value_anim_cb(void *obj, int32_t value) { lv_arc_set_value((lv_obj_t *)obj, value); } lv_anim_t a; lv_anim_init(a); lv_anim_set_var(a, arc); lv_anim_set_values(a, 0, 75); lv_anim_set_time(a, 800); lv_anim_set_exec_cb(a, arc_value_anim_cb); lv_anim_set_path_cb(a, lv_anim_path_ease_out); lv_anim_start(a);这里的关键是lv_anim_path_ease_out它让动画先快后慢收尾自然。LVGL 内置了好几种缓动曲线ease_in、ease_out、ease_in_out、overshoot、bounce等。做进度类动画ease_out最稳妥做强调类动画overshoot会有一点冲过头再回弹的效果很俏皮。注意动画回调里调用的lv_arc_set_value会触发重绘如果圆弧上还有阴影、渐变重绘开销会叠加。在低端 MCU 上动画时长别设太短建议 500ms 以上给渲染留出时间。4.2 拖拽交互的视觉反馈lv_arc默认是可拖拽的如果没禁用。拖拽时旋钮会跟着走但视觉反馈仅限于位置变化。可以给拖拽状态加样式让用户明确知道我正在操作它。LVGL 支持状态样式比如LV_STATE_PRESSEDlv_obj_set_style_shadow_width(arc, 30, LV_PART_KNOB | LV_STATE_PRESSED); lv_obj_set_style_shadow_opa(arc, LV_OPA_70, LV_PART_KNOB | LV_STATE_PRESSED);这样按下旋钮时旋钮的阴影会变大变亮有种被点亮的感觉。松开后恢复。这种微交互是提升精致度的关键细节成本很低但效果明显。4.3 圆弧作为进度指示器的动画节奏如果圆弧是用来显示进度的比如加载、音量动画节奏要和业务语义匹配。加载进度适合匀速或 ease_out音量调节适合跟手直接 set_value 不动画温度显示适合慢速 ease_in_out。我做过一个音量圆弧一开始用了 800ms 的 ease_out 动画结果用户快速拖动时圆弧跟不上手指体验很割裂。后来改成拖拽时直接 set_value、松手后再做一次短动画归位就顺畅多了。交互类圆弧跟手优先于好看这个优先级不能搞反。4.4 多圆弧联动动画仪表盘上经常有多个圆弧需要联动比如转速和时速。这时候可以用一个动画时间轴统一驱动或者用lv_anim_timelineLVGL 8.2 支持把多个动画串起来。lv_anim_timeline_t *timeline lv_anim_timeline_create(); lv_anim_timeline_add(timeline, 0, anim1); lv_anim_timeline_add(timeline, 200, anim2); lv_anim_timeline_start(timeline);时间轴的好处是可以精确控制每个动画的启动延迟做出依次点亮的效果。这种编排感是高级 UI 的标志之一。5. 自定义绘制当内置样式不够用时的终极手段前面四招基本能覆盖 90% 的美化需求。但有些效果比如沿弧线渐变、虚线刻度、特殊端点形状内置样式确实做不到这时候就得上自定义绘制custom draw。这是lv_arc美化的终极手段也是最考验功力的一层。5.1 自定义绘制事件的挂载方式LVGL 允许给对象挂LV_EVENT_DRAW_MAIN或LV_EVENT_DRAW_POST事件回调在回调里用lv_draw_arc等底层 API 自己画。lv_obj_add_event_cb(arc, my_draw_cb, LV_EVENT_DRAW_POST, NULL); static void my_draw_cb(lv_event_t *e) { lv_obj_t *obj lv_event_get_target(e); lv_draw_ctx_t *draw_ctx lv_event_get_draw_ctx(e); // 在这里用 lv_draw_arc 画自定义内容 }LV_EVENT_DRAW_POST是在控件默认绘制完成之后触发适合叠加内容LV_EVENT_DRAW_MAIN是在默认绘制之前适合替换默认绘制。我一般用 POST 来叠加刻度点、装饰线之类的东西。5.2 用 lv_draw_arc 画刻度点刻度点是仪表盘的经典元素。用自定义绘制可以在圆弧外侧均匀地画一圈小圆点或短线。static void draw_ticks(lv_event_t *e) { lv_obj_t *obj lv_event_get_target(e); lv_draw_ctx_t *ctx lv_event_get_draw_ctx(e); lv_area_t coords; lv_obj_get_coords(obj, coords); lv_coord_t cx (coords.x1 coords.x2) / 2; lv_coord_t cy (coords.y1 coords.y2) / 2; lv_coord_t r (coords.x2 - coords.x1) / 2 - 5; for (int i 0; i 60; i) { int angle i * 6; // 每 6 度一个刻度 // 计算刻度点坐标并绘制 // ... } }这段代码是骨架实际绘制时要用lv_draw_arc或者lv_draw_rect配合三角函数算出每个刻度的位置。刻度点的密度、长度、颜色都可以参数化做出很专业的仪表盘效果。5.3 沿弧线渐变的模拟实现前面提到 LVGL 原生渐变是相对于包围盒的做不到沿弧渐变。用自定义绘制可以模拟把圆弧按角度分成 N 段每段用插值出来的颜色单独画。for (int i 0; i segments; i) { int a1 start i * step; int a2 a1 step; lv_color_t c lv_color_mix(color_end, color_start, i * 255 / segments); lv_draw_arc_dsc_t dsc; lv_draw_arc_dsc_init(dsc); dsc.color c; dsc.width arc_width; lv_draw_arc(ctx, dsc, area, cx, cy, radius, a1, a2); }分段数越多渐变越平滑但绘制开销越大。实测 30 到 60 段在视觉上已经足够平滑再往上收益递减。这个技巧在 PC 模拟器上跑毫无压力但在 STM32 上要谨慎尤其是动画中每帧都重绘的话。5.4 自定义绘制的性能与维护权衡自定义绘制虽然强大但有两个代价性能和可维护性。性能上它绕过了 LVGL 的样式缓存机制每次重绘都要重新计算可维护性上代码和具体坐标强绑定换个尺寸可能就要重调。我的建议是能用内置样式解决的绝不自定义绘制。只有当内置样式确实无法实现目标效果且这个效果对产品体验至关重要时才动用自定义绘制。而且自定义绘制的代码要封装成独立函数参数化尺寸和颜色方便复用和调整。另外自定义绘制的内容如果不需要每帧变化可以考虑画到一个lv_img或者 canvas 上缓存起来避免重复计算。这个优化在复杂仪表盘上能省不少 CPU。6. 移植环境下的样式调试经验最后聊聊调试环境。很多同学在 Keil STM32 上直接调样式改一次编译一次烧录一次效率极低。我的做法是先在 PC 模拟器上把样式调到位再移植到板子上。LVGL 官方提供了基于 SDL 或 Windows API 的模拟器工程在 PC 上跑起来后改样式、看效果几乎是实时的效率提升十倍不止。在模拟器上要注意的是PC 的屏幕色彩和实际 LCD 可能有差异。模拟器上看着很正的青色到了某些 SPI 屏上可能偏绿。所以颜色最终还是要以实际屏幕为准模拟器只用来调结构、比例、动画节奏这些与屏幕无关的部分。还有一点如果你用的是 FreeRTOS LVGL 的组合lv_timer_handler的调用周期会影响动画流畅度。我一般把它放在一个独立的低优先级任务里周期 5ms 左右配合lv_tick_inc的 1ms 心跳。周期太长动画会卡太短又浪费 CPU。这个参数在模拟器上感受不明显到了板子上一定要实测调整。关于字体和图标如果圆弧中间要显示数值记得把对应字号的字体使能上LVGL 默认只开了很小的字号。用lv_arc配合lv_label居中显示数值是常见组合label 的字体、颜色、对齐都要单独调别指望默认效果。调lv_arc的样式说到底是个分层推进的过程先把 part 和角度搞明白再把基础属性调顺然后上渐变阴影接着加动画交互最后才考虑自定义绘制。每一层都扎实了圆弧的质感自然就上来了。我在实际项目里最深的体会是高级感往往不来自某个炫酷的效果而来自比例、对比、节奏这些基础要素的精确控制。一个弧宽比例协调、颜色对比得当、动画节奏舒服的简单圆弧比一个堆满渐变阴影但比例失调的圆弧要好看得多。