车载仪表盘、健康数据面板、运动数据环形图——这些不是普通图表而是需要仪表感的视觉组件。Progress组件太简单Canvas绘图太底层两者的折中方案是用ShapePath或Canvas画自定义表盘。这篇从最简单的环形仪表到复杂的多指针刻度盘把自定义绘图的完整方案讲清楚。环形仪表基础最基础的仪表盘一个弧线表示数值范围一个填充弧表示当前值中间显示数字。用Canvas实现privatesettings:RenderingContextSettingsnewRenderingContextSettings(true);privatecontext:CanvasRenderingContext2DnewCanvasRenderingContext2D(this.settings);Canvas(this.context).width(200).height(200).onReady((){this.drawGauge(72,100);})privatedrawGauge(value:number,total:number):void{letctx:CanvasRenderingContext2Dthis.context;letcenterX:number100;letcenterY:number100;letradius:number70;letlineWidth:number14;ctx.clearRect(0,0,200,200);// 背景弧ctx.beginPath();ctx.arc(centerX,centerY,radius,0.75*Math.PI,0.25*Math.PI);ctx.strokeStyle#e0e0e0;ctx.lineWidthlineWidth;ctx.lineCapround;ctx.stroke();// 数值弧letratio:numbervalue/total;letstartAngle:number0.75*Math.PI;lettotalAngle:number1.5*Math.PI;letendAngle:numberstartAngleratio*totalAngle;ctx.beginPath();ctx.arc(centerX,centerY,radius,startAngle,endAngle);ctx.strokeStyle#1a73e8;ctx.lineWidthlineWidth;ctx.lineCapround;ctx.stroke();// 中间数字ctx.fillStyle#333333;ctx.fontbold 36px sans-serif;ctx.textAligncenter;ctx.textBaselinemiddle;ctx.fillText(value.toString(),centerX,centerY-8);ctx.fillStyle#999999;ctx.font12px sans-serif;ctx.fillText(km/h,centerX,centerY18);}0.75π到0.25π画一个3/4圆弧从左下到右下跳过底部1/4。这是仪表盘最常见的弧度范围——底部留空显示标签。刻度线仪表盘不能只有弧线还要有刻度线标记数值区间privatedrawTicks(ctx:CanvasRenderingContext2D,centerX:number,centerY:number,radius:number,startAngle:number,totalAngle:number,tickCount:number):void{letinnerRadius:numberradius10;letouterRadius:numberradius20;for(leti:number0;itickCount;i){letangle:numberstartAngle(i/tickCount)*totalAngle;letisMajor:booleani%50;letinner:numberisMajor?innerRadius:innerRadius5;letouter:numberouterRadius;letx1:numbercenterXMath.cos(angle)*inner;lety1:numbercenterYMath.sin(angle)*inner;letx2:numbercenterXMath.cos(angle)*outer;lety2:numbercenterYMath.sin(angle)*outer;ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.strokeStyleisMajor?#333333:#cccccc;ctx.lineWidthisMajor?2:1;ctx.stroke();// 主刻度标注数字if(isMajor){letlabelRadius:numberouter12;letlx:numbercenterXMath.cos(angle)*labelRadius;letly:numbercenterYMath.sin(angle)*labelRadius;letlabelValue:numberMath.round((i/tickCount)*100);ctx.fillStyle#666666;ctx.font10px sans-serif;ctx.textAligncenter;ctx.textBaselinemiddle;ctx.fillText(labelValue.toString(),lx,ly);}}}tickCount50画50个刻度每5个一个大刻度。大刻度更长更粗小刻度短而细。大刻度旁标注数字。指针仪表盘最经典的视觉元素——指针。用三角形绘制privatedrawNeedle(ctx:CanvasRenderingContext2D,centerX:number,centerY:number,value:number,total:number,startAngle:number,totalAngle:number):void{letratio:numbervalue/total;letangle:numberstartAngleratio*totalAngle;letneedleLength:number60;lettipX:numbercenterXMath.cos(angle)*needleLength;lettipY:numbercenterYMath.sin(angle)*needleLength;letbaseAngle1:numberangleMath.PI/2;letbaseAngle2:numberangle-Math.PI/2;letbaseWidth:number4;letbx1:numbercenterXMath.cos(baseAngle1)*baseWidth;letby1:numbercenterYMath.sin(baseAngle1)*baseWidth;letbx2:numbercenterXMath.cos(baseAngle2)*baseWidth;letby2:numbercenterYMath.sin(baseAngle2)*baseWidth;ctx.beginPath();ctx.moveTo(tipX,tipY);ctx.lineTo(bx1,by1);ctx.lineTo(bx2,by2);ctx.closePath();ctx.fillStyle#e53935;ctx.fill();// 中心圆点ctx.beginPath();ctx.arc(centerX,centerY,6,0,2*Math.PI);ctx.fillStyle#333333;ctx.fill();}指针是三角形尖端指向数值角度底边在中心点。baseAngle垂直于指针方向底边宽度4px。中心圆点覆盖指针底部。颜色分段速度表的颜色分段——绿→黄→红privatedrawColorSegments(ctx:CanvasRenderingContext2D,centerX:number,centerY:number,radius:number,lineWidth:number,startAngle:number,totalAngle:number):void{letsegments:{ratio:number;color:string}[][{ratio:0.4,color:#43a047},{ratio:0.7,color:#ff9800},{ratio:1.0,color:#e53935}];letprevAngle:numberstartAngle;for(leti:number0;isegments.length;i){letendAngle:numberstartAnglesegments[i].ratio*totalAngle;ctx.beginPath();ctx.arc(centerX,centerY,radius,prevAngle,endAngle);ctx.strokeStylesegments[i].color;ctx.lineWidthlineWidth;ctx.lineCapbutt;ctx.stroke();prevAngleendAngle;}}040%绿色4070%橙色70~100%红色。每段画一段弧lineCap设butt避免段之间重叠。渐变色仪表单一颜色太无聊渐变色更高级privatedrawGradientArc(ctx:CanvasRenderingContext2D,centerX:number,centerY:number,radius:number,lineWidth:number,startAngle:number,endAngle:number):void{letsteps:number60;letangleStep:number(endAngle-startAngle)/steps;for(leti:number0;isteps;i){letratio:numberi/steps;leta1:numberstartAnglei*angleStep;leta2:numbera1angleStep0.01;letr:numberMath.round(26ratio*(227-26));letg:numberMath.round(115ratio*(57-115));letb:numberMath.round(232ratio*(53-232));ctx.beginPath();ctx.arc(centerX,centerY,radius,a1,a2);ctx.strokeStylergb(r,g,b);ctx.lineWidthlineWidth;ctx.lineCapbutt;ctx.stroke();}}把弧线分成60段每段颜色从蓝(26,115,232)到红(227,57,53)线性插值。段之间加0.01弧度重叠避免间隙。多环仪表健康面板常见外环步数、中环运动时间、内环消耗热量privatedrawMultiRing(ctx:CanvasRenderingContext2D,centerX:number,centerY:number):void{letrings:{radius:number;value:number;total:number;color:string;label:string}[][{radius:80,value:7500,total:10000,color:#1a73e8,label:步数},{radius:60,value:35,total:60,color:#43a047,label:运动(分)},{radius:40,value:280,total:400,color:#ff9800,label:千卡}];for(leti:number0;irings.length;i){letringrings[i];letlineWidth:number12;// 背景环ctx.beginPath();ctx.arc(centerX,centerY,ring.radius,0,2*Math.PI);ctx.strokeStyle#e8e8e8;ctx.lineWidthlineWidth;ctx.stroke();// 数值环letratio:numberring.value/ring.total;letstartAngle:number-Math.PI/2;letendAngle:numberstartAngleratio*2*Math.PI;ctx.beginPath();ctx.arc(centerX,centerY,ring.radius,startAngle,endAngle);ctx.strokeStylering.color;ctx.lineWidthlineWidth;ctx.lineCapround;ctx.stroke();}// 中心文字ctx.fillStyle#333333;ctx.fontbold 20px sans-serif;ctx.textAligncenter;ctx.textBaselinemiddle;ctx.fillText(75%,centerX,centerY);}三个同心环不同半径、颜色、数值。中心显示最关键的数据。动画驱动仪表盘数值变化时要有动画效果不能跳变StatedisplayValue:number0StatetargetValue:number0privateanimTimer:number-1setGaugeValue(value:number):void{this.targetValuevalue;letstart:numberthis.displayValue;letdiff:numbervalue-start;letduration:number800;letstartTime:numberDate.now();if(this.animTimer!-1){clearInterval(this.animTimer);}this.animTimersetInterval((){letelapsed:numberDate.now()-startTime;if(elapsedduration){this.displayValuethis.targetValue;clearInterval(this.animTimer);this.animTimer-1;}else{letprogress:numberelapsed/duration;leteased:number1-Math.pow(1-progress,3);this.displayValuestartdiff*eased;}this.context.clearRect(0,0,200,200);this.drawGauge(this.displayValue,100);},16);}1 - (1-t)^3 是easeOutCubic缓动——先快后慢。16ms一帧约60fps。displayValue是当前动画位置targetValue是目标值。setInterval在aboutToDisappear中清理。Shape组件替代CanvasCanvas需要手动管理RenderingContext代码量大。简单仪表可以用ShapePathShape(){Path().commands(M 30 100 A 70 70 0 0 1 170 100).stroke(Color.Gray).strokeWidth(14).fill(Color.Transparent)Path().commands(this.buildArcCommands(72)).stroke(#1a73e8).strokeWidth(14).strokeLineCap(LineCapStyle.Round).fill(Color.Transparent)}.width(200).height(200)privatebuildArcCommands(value:number):string{letratio:numbervalue/100;letstartAngle:number0.75*Math.PI;letendAngle:numberstartAngleratio*1.5*Math.PI;letstartX:number10070*Math.cos(startAngle);letstartY:number10070*Math.sin(startAngle);letendX:number10070*Math.cos(endAngle);letendY:number10070*Math.sin(endAngle);letlargeArc:numberratio0.5?1:0;returnM startX startY A 70 70 0 largeArc 1 endX endY;}SVG path的A命令画弧线。参数rx ry x-rotation large-arc-flag sweep-flag x y。largeArcFlag在弧度超过π时设1。sweepFlag1表示顺时针。Shape比Canvas简单但灵活度低——不能画刻度、指针、渐变段。只适合简单的环形仪表。踩坑清单问题原因解决弧线起点不对角度0在3点方向用0.75π从左下方开始刻度线方向不对cos/sin参数用错xcos, ysin指针三角形扭曲底边角度计算错误垂直于指针方向±90°渐变弧有间隙段之间没重叠加0.01弧度重叠动画卡顿setInterval没清aboutToDisappear中clearInterval数值跳变没做动画过渡easeOutCubic插值Canvas空白在onReady之前绘图移到onReady回调Shape弧线不显示SVG A命令参数错检查largeArc和sweep重绘残影没clearRect每次draw前clearRect文字模糊Canvas未开抗锯齿RenderingContextSettings(true)自定义仪表的核心是弧线刻度指针数字四层叠加。Canvas灵活但代码多Shape简单但限制大。选择取决于你要不要画刻度和指针——要画就用Canvas只画弧线就用Shape。