6、仪表盘开发实战(上):圆形仪表盘绘制、Canvas 2D渲染、指针动画与刻度线生成
各位同学,欢迎来到仪表盘开发实战的第一部分。说实话,仪表盘是车载HMI里最见功力的模块之一。我早年做第一个仪表项目时,踩了不少坑,今天把这些经验揉碎了讲给你听。
这一章我们聚焦三个核心:圆形仪表盘的绘制、Canvas 2D渲染、指针动画与刻度线生成。说白了,就是让你从零手撸一个能转起来的仪表盘。
本章核心知识点
- 圆形仪表盘的坐标系与绘制流程
- Canvas 2D 在 Qt Quick 中的使用方式
- 刻度线的数学生成算法
- 指针动画的平滑实现
6.1 圆形仪表盘的坐标系与绘制流程
先聊坐标系。仪表盘本质上是一个极坐标系的应用。圆心在 (width/2, height/2),半径决定了表盘大小。角度从 -135° 到 +135° 是常见的布局(也就是左下到右下,留出底部空间给信息显示)。
我习惯把绘制流程拆成四层:
- 背景层:圆环、底色、渐变效果
- 刻度层:主刻度、次刻度、数字标签
- 指针层:指针本体、中心圆盖
- 装饰层:高光、阴影、外圈
为什么要分层?因为每一层的刷新频率不同。指针动画需要60fps,刻度线只需要绘制一次。混在一起画,性能会崩。我在项目中见过有人把所有东西塞到一个 onPaint 里,结果指针一抖一抖的——嗯,这就是不分层的后果。
我的习惯:用 Canvas 的 requestAnimationFrame 驱动指针层,刻度层用 Canvas 的静态绘制,只画一次。
6.2 Canvas 2D 在 Qt Quick 中的使用
Qt Quick 里的 Canvas 元素,本质上是对 HTML5 Canvas 的封装。API 几乎一样,但有几个坑要注意。
先看一个最简的仪表盘骨架:
Canvas {
id: dashboardCanvas
width: 400; height: 400
onPaint: {
var ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
// 绘制外圈
ctx.beginPath();
ctx.arc(width/2, height/2, 180, 0, Math.PI * 2);
ctx.strokeStyle = "#333";
ctx.lineWidth = 4;
ctx.stroke();
// 绘制刻度线(后面细讲)
drawTicks(ctx);
}
function drawTicks(ctx) {
// 刻度生成逻辑
}
}
这里有个细节:ctx.clearRect 必须放在最前面,否则上一帧的残留会叠加上去。我刚开始做的时候忘了这步,指针拖出了一条长长的尾巴——像彗星一样,还挺好看,但客户不买账。
Canvas 2D 的坐标系默认是左上角为原点,x向右,y向下。画圆形仪表盘时,记得把原点平移到中心:
ctx.translate(width/2, height/2);
这样后面所有的坐标计算都基于圆心,省心很多。
6.3 刻度线生成算法
刻度线是仪表盘的「骨架」。生成算法其实很简单:遍历角度,计算起点和终点坐标。
假设仪表盘范围是 -135° 到 +135°,总共 240° 的跨度。主刻度每 30° 一个,次刻度每 10° 一个。
function drawTicks(ctx) {
var centerX = width/2;
var centerY = height/2;
var radius = 160;
var startAngle = -135 * Math.PI / 180;
var endAngle = 135 * Math.PI / 180;
var totalAngle = endAngle - startAngle;
// 主刻度:每30度一个
for (var i = 0; i <= 8; i++) {
var angle = startAngle + (totalAngle / 8) * i;
var x1 = centerX + (radius - 15) * Math.cos(angle);
var y1 = centerY + (radius - 15) * Math.sin(angle);
var x2 = centerX + radius * Math.cos(angle);
var y2 = centerY + radius * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = "#fff";
ctx.lineWidth = 3;
ctx.stroke();
}
// 次刻度:每10度一个
for (var j = 0; j <= 24; j++) {
var angle = startAngle + (totalAngle / 24) * j;
var x1 = centerX + (radius - 8) * Math.cos(angle);
var y1 = centerY + (radius - 8) * Math.sin(angle);
var x2 = centerX + radius * Math.cos(angle);
var y2 = centerY + radius * Math.sin(angle);
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = "#aaa";
ctx.lineWidth = 1.5;
ctx.stroke();
}
}
你想想看,为什么主刻度线比次刻度长?因为人眼需要快速区分层级。主刻度长一些、粗一些,视觉上更突出。这是UI设计的基本功。
我曾经踩过的坑:角度单位搞混了。JavaScript 的 Math 函数全部用弧度,但我一开始写成了角度,结果刻度线全挤在右下角。调试了半小时才发现。建议你在代码开头加个注释:// 所有角度均为弧度。
6.4 指针动画与平滑实现
指针动画是仪表盘的灵魂。说白了,就是让指针从当前值平滑过渡到目标值。
我推荐用 线性插值 + 定时器 的方式。不要直接用 Rotation 动画,因为那样很难控制加速度和阻尼效果。
property real currentAngle: -135
property real targetAngle: -135
property real speed: 3.0 // 每帧增加的角度
function updatePointer() {
if (Math.abs(currentAngle - targetAngle) < 0.5) {
currentAngle = targetAngle;
return;
}
var diff = targetAngle - currentAngle;
currentAngle += diff * 0.15; // 缓动效果
canvas.requestPaint();
// 下一帧继续
if (Math.abs(currentAngle - targetAngle) > 0.5) {
canvas.afterFrame(updatePointer);
}
}
这里用了 diff * 0.15 的缓动系数。系数越小,指针越「肉」,看起来更稳重。系数越大,指针越「贼」,适合运动模式。我在做某款电动车的仪表时,经济模式用 0.1,运动模式用 0.3,用户反馈说手感很真实。
绘制指针本身很简单:
function drawPointer(ctx, angle) {
ctx.save();
ctx.translate(width/2, height/2);
ctx.rotate(angle * Math.PI / 180);
// 指针主体
ctx.beginPath();
ctx.moveTo(0, -140); // 指针尖
ctx.lineTo(-6, 20); // 左侧
ctx.lineTo(6, 20); // 右侧
ctx.closePath();
ctx.fillStyle = "#ff4444";
ctx.fill();
// 中心圆盖
ctx.beginPath();
ctx.arc(0, 0, 12, 0, Math.PI * 2);
ctx.fillStyle = "#222";
ctx.fill();
ctx.restore();
}
注意 ctx.save() 和 ctx.restore() 的成对使用。如果不恢复坐标系,后面的绘制全乱套了。
6.5 性能优化小贴士
仪表盘是实时渲染的,性能至关重要。我总结了几条经验:
| 优化点 | 做法 | 效果 |
|---|---|---|
| 减少绘制调用 | 刻度线只画一次,存为离屏缓存 | 减少 70% 的绘制时间 |
| 避免频繁创建对象 | 在 onPaint 外定义颜色、路径等常量 |
减少 GC 压力 |
使用 afterFrame |
代替 Timer 驱动动画 |
与屏幕刷新率同步,更流畅 |
| 降低抗锯齿 | 非关键元素关闭 antialiasing |
提升低端设备帧率 |
我的建议:在开发阶段用 console.profile() 分析绘制耗时。我曾经发现一个项目里,刻度线每帧都在重新计算三角函数,改成预计算后帧率从 30fps 飙到了 60fps。
6.6 本章知识体系
下面这张图帮你梳理本章的核心逻辑:
这张图把本章的四个核心模块串起来了。从左到右,从坐标系到动画,每一步都依赖前一步。刻度线生成是数学基础,Canvas 2D 是渲染工具,指针动画是交互灵魂。
好了,这一章的内容就到这里。代码示例你可以在本地跑跑看,调调缓动系数,感受一下指针的「手感」。下一章我们继续深入,聊仪表盘的交互事件和数值绑定。
公众号:蓝海资料掘金营,微信deep3321