导读:本期聚焦于胡建平创作的《如何用Core Graphics在iOS上绘制带指针动画与功率因数显示的仪表盘功率表?》,敬请观看详情。仪表盘类控件在电力监控、充电桩、智能家居等iOS应用中出现的频率越来越高,而系统并没有提供现成的仪表盘组件,多数场景需要开发者自行绘制。本文围绕如何使用Core Graphics实现一个可用的功率表仪表盘展开,先讲刻度盘、弧线和颜色分区的绘制思路,再分析有功功率、视在功率与功率因数之间的计算关系,说明数值到指针角度的换算方法,随后介绍用CADisplayLink驱动指针惯性摆动的动画技巧,最后补充功率因数的实时显示与表盘状态刷新细节,并附上可直接运行的代码示例,帮助读者快速落地一套高性能的自绘仪表控件。

在充电桩、光伏监控、智能电表这类App里,仪表盘几乎是绕不开的界面元素。UIKit自带的控件库里并没有现成的仪表盘,第三方库又常常样式固定、难以贴合设计稿,因此掌握用Core Graphics自绘仪表盘的思路就显得很实用。本文以一个功率表为例,完整走一遍刻度绘制、功率计算、指针动画和功率因数显示的流程,代码以Swift为主,思路同样适用于Objective-C。

如何用Core Graphics在iOS上绘制带指针动画与功率因数显示的仪表盘功率表?

一、绘制刻度盘:路径计算是核心

仪表盘的灵魂在于弧线和刻度的几何计算。通常功率表的量程是0到最大值,对应表盘上从135度到405度(也就是起始角在左下、终止角在右下,中间留出底部缺口),这个角度区间可以根据设计稿调整。Core Graphics中的角度以弧度为单位,且屏幕坐标系的Y轴向下,所以真实绘制的角度方向需要做一次换算,否则画出来的弧会跑到表盘上方。

刻度分为主刻度和次刻度。主刻度带数字标签,数量一般是量程分段数加一;次刻度在每个主刻度之间再均分若干份。绘制时先算出每根刻度线两端的坐标,外端落在刻度圆上,内端向圆心方向收缩固定长度,主刻度收缩得多一些、线宽更粗,视觉上就能形成层次。坐标计算用最基础的三角函数即可。

func point(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
    let x = center.x + radius * cos(angle)
    let y = center.y + radius * sin(angle)
    return CGPoint(x: x, y: y)
}

// 表盘起始角与结束角,底部留缺口
let startAngle = CGFloat.pi * 0.75
let endAngle = CGFloat.pi * 2.25
let totalSweep = endAngle - startAngle

func drawTicks(in ctx: CGContext, rect: CGRect) {
    let center = CGPoint(x: rect.midX, y: rect.midY)
    let radius = min(rect.width, rect.height) * 0.4
    let mainTickCount = 11   // 0~10共11个主刻度

    for i in 0...10 {
        let angle = startAngle + totalSweep * CGFloat(i) / CGFloat(mainTickCount - 1)
        let outer = point(center: center, radius: radius, angle: angle)
        let inner = point(center: center, radius: radius - 14, angle: angle)
        ctx.setLineWidth(i % 2 == 0 ? 2.5 : 1.5)
        ctx.strokeLineSegments(between: [outer, inner])
    }
}

颜色分区也是常见需求,比如0到30%用绿色、30%到70%用黄色、70%以上用红色。实现方式是按百分比换算出三段弧各自的角度区间,分别调用addArc后设置不同颜色描边即可。注意弧度制的角度是逆时针为正还是顺时针为正,取决于clockwise参数与坐标系方向,绘制前用小demo验证一下方向可以省去不少调试时间。

二、功率计算与数值到角度的映射

功率表显示的数值通常来自电表或传感器的原始数据,比如电压U、电流I、相位差或者直接的有功功率P。这里需要理清三个概念:有功功率P等于电压与电流的有效值乘以功率因数,单位是瓦;视在功率S等于电压有效值乘以电流有效值,单位是伏安;功率因数cosφ等于P除以S,反映电能的有效利用程度。如果设备只上报P和S,功率因数直接做除法就能得到,注意S为零时要做保护,避免除零。

struct PowerSample {
    let voltage: Double      // 电压有效值 V
    let current: Double      // 电流有效值 A
    let activePower: Double  // 有功功率 W
}

extension PowerSample {
    var apparentPower: Double { voltage * current }          // 视在功率 VA
    var powerFactor: Double {
        let s = apparentPower
        guard s > 0.001 else { return 0 }
        return min(1, max(0, activePower / s))               // 限制在0~1
    }
}

拿到功率值后,要把它映射成指针角度。公式很直接:先算当前值占满量程的比例ratio,再用起始角加上比例乘以总扫过角度,就得到目标角度。满量程的选择建议比实际最大值高出一档,比如设备峰值7.5千瓦,量程设为10千瓦,指针平时工作在表盘中段,既好看又留有超限提示的余量。

let maxPower: Double = 10000.0   // 满量程 10kW

func angle(for power: Double) -> CGFloat {
    let clamped = min(maxPower, max(0, power))
    let ratio = clamped / maxPower
    return startAngle + totalSweep * ratio
}

三、指针动画:CADisplayLink比UIView动画更细腻

指针从当前角度转到目标角度,如果直接用UIView的animate方法做隐式动画,视觉上会显得生硬,因为真实的机械表针有惯性,快速变化时会过冲再回摆。推荐的做法是在视图内部维护当前角度和目标角度两个状态,用CADisplayLink每帧做插值更新,并叠加一个简单的弹簧效果。

具体思路:每帧计算目标角度与当前角度的差值,让当前角度以一定比例向目标靠拢,同时记录速度,速度参与下一帧计算,就形成了类似弹簧阻尼的动态。数值更新后调用setNeedsDisplay触发重绘,Core Graphics会重新画指针。这种方式CPU占用很低,一秒六十帧重绘一个中等复杂度的表盘完全没有压力。

class GaugeView: UIView {
    var targetAngle: CGFloat = 0
    private var currentAngle: CGFloat = 0
    private var velocity: CGFloat = 0
    private var displayLink: CADisplayLink?

    func startAnimating() {
        displayLink?.invalidate()
        displayLink = CADisplayLink(target: self, selector: #selector(tick))
        displayLink?.add(to: .main, forMode: .common)
    }

    @objc private func tick() {
        // 弹簧阻尼模型:加速度正比于角度差,速度按阻尼系数衰减
        let stiffness: CGFloat = 120
        let damping: CGFloat = 14
        velocity += (targetAngle - currentAngle) * stiffness * (1.0 / 60.0)
        velocity *= (1 - damping / 60)
        currentAngle += velocity * (1.0 / 60.0)
        setNeedsDisplay()

        if abs(targetAngle - currentAngle) < 0.002 && abs(velocity) < 0.002 {
            currentAngle = targetAngle
            velocity = 0
            setNeedsDisplay()
            displayLink?.invalidate()
            displayLink = nil
        }
    }
}

有一个容易踩的坑:CADisplayLink默认模式在列表滚动时会暂停回调,如果仪表盘嵌在ScrollView里,务必加到common`模式,否则滚动列表时指针会卡住。另外数值稳定后记得invalidate掉displayLink,常驻空转既费电又没有意义。

四、功率因数显示与整体刷新策略

功率因数一般不占用指针,而是在表盘中央或下方用数字直接显示,格式化为两位小数,并按区间着色:0.9以上绿色,0.8到0.9黄色,低于0.8红色提示电能利用效率低。文字绘制不要用Core Text硬写,直接在draw(_ rect:)里调用NSAttributedString的draw(at:)方法即可,字号和颜色都方便调整。

func drawPowerFactor(_ pf: Double, in rect: CGRect) {
    let text = String(format: "PF %.2f", pf)
    let color: UIColor = pf >= 0.9 ? .systemGreen : (pf >= 0.8 ? .systemYellow : .systemRed)
    let attrs: [NSAttributedString.Key: Any] = [
        .font: UIFont.monospacedDigitSystemFont(ofSize: 20, weight: .semibold),
        .foregroundColor: color
    ]
    let size = text.size(withAttributes: attrs)
    let point = CGPoint(x: rect.midX - size.width / 2, y: rect.midY + 40)
    (text as NSString).draw(at: point, withAttributes: attrs)
}

数值刷新方面,传感器上报频率可能达到每秒十次以上,如果每次都直接更新目标角度,指针会不停抖动。实践中有两种处理方式:一是对上报数据做滑动平均滤波,取最近几个采样值的平均;二是对目标角度本身做节流,比如每200毫秒才更新一次目标值,中间由弹簧动画自然过渡,视觉效果反而更接近真实仪表。

最后补充绘制细节:指针建议画成带阴影的细长三角形,圆心处叠加两层不同半径的圆做立体感;刻度数字用等宽数字字体,避免数值跳动时文字宽度变化;整个表盘的静态部分(弧线、刻度、数字)可以提前渲染成一张位图缓存,draw方法里只重绘指针,性能还能再提升一截。这套方案在真机上实测帧率稳定在60fps,即使叠加多个仪表盘同屏展示也毫无压力。

Core GraphicsiOS仪表盘功率计算修改时间:2026-09-12 08:32:42

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260912/55216.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。