导读:本期聚焦于天马创作的《如何用Core Graphics在iOS上绘制一个带液位动画和低油量警告的燃油仪表盘?》,敬请观看详情。仪表盘类控件几乎是车载App和智能座舱HMI的标配,而系统自带的UIKit控件很难满足燃油表这种扇形刻度加指针加液位展示的需求。本文基于Core Graphics和Quartz 2D,完整实现一个iOS端燃油表组件:先讲解扇形分段刻度的数学计算与路径绘制,包括主刻度、次刻度以及E到F区间的颜色分段;再通过CADisplayLink驱动液位上升下降的缓动动画,让油量变化有真实的填充感;最后实现低油量状态下的红色闪烁警告,结合图层重绘与Core Animation两种方案的取舍分析。文末给出完整可运行的绘制代码,可以直接集成到自定义项目中。

燃油表是车载界面里最经典的控件之一:一个扇形表盘、若干分段刻度、底部一段像液体一样的填充区域,油量低的时候还要闪红灯提醒司机。这套效果用UIKit的默认控件拼不出来,最合适的做法是继承UIView,重写draw(_ rect:)方法,用Core Graphics逐层绘制。这篇文章就把整个实现过程拆开讲清楚:刻度怎么算角度、液位怎么画、动画怎么驱动、警告怎么闪。

如何用Core Graphics在iOS上绘制一个带液位动画和低油量警告的燃油仪表盘?

一、表盘结构与角度计算

燃油表通常是240度左右的扇形,表盘下方留出开口。Core Graphics里角度是以弧度计量的,我们把表盘的起始角定为135度(左下方向),结束角定为405度(右下方向),正好扫过270度。为了方便后续计算,先定义两个常量,并用degreesToRadians扩展把角度转成弧度。

extension CGFloat {
    func degreesToRadians() -> CGFloat {
        return self * .pi / 180
    }
}

class FuelGaugeView: UIView {
    let startAngle: CGFloat = 135    // 起始角
    let endAngle: CGFloat = 405      // 结束角(即45度)
    let totalSweep: CGFloat = 270    // 总扫过角度

    override func draw(_ rect: CGRect) {
        guard let ctx = UIGraphicsGetCurrentContext() else { return }
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius = min(rect.width, rect.height) * 0.4
        // 后续所有绘制都围绕 center 和 radius 展开
    }
}

需要注意一点:iOS的坐标系y轴向下,所以正角度在屏幕上是顺时针方向。135度在屏幕上指向左下方,405度等价于45度指向右下方,这正是我们想要的开口朝下的布局。如果把起止角弄反,画出来的扇形就会开口朝上,这是新手最容易踩的坑之一。

分段刻度的绘制思路是:把270度按照E到F的比例切分。假设表盘分为10个主刻度,每个主刻度之间再插入4个次刻度。刻度线本质是从半径R1到R2的线段,只需计算每个刻度对应的角度,再用CGContext的move和addLine画线即可。主刻度画粗一点、长一点,次刻度画细一点、短一点,视觉层次立刻就出来了。

func drawTicks(ctx: CGContext, center: CGPoint, radius: CGFloat) {
    let mainTickCount = 11
    let subTicksPerMain = 4

    for i in 0...((mainTickCount - 1) * subTicksPerMain) {
        let ratio = CGFloat(i) / CGFloat((mainTickCount - 1) * subTicksPerMain)
        let angle = (startAngle + totalSweep * ratio).degreesToRadians()
        let isMain = (i % subTicksPerMain == 0)

        let innerR = isMain ? radius * 0.78 : radius * 0.84
        let outerR = radius * 0.92

        let x1 = center.x + innerR * cos(angle)
        let y1 = center.y + innerR * sin(angle)
        let x2 = center.x + outerR * cos(angle)
        let y2 = center.y + outerR * sin(angle)

        ctx.setStrokeColor(isMain ? UIColor.white.cgColor : UIColor.gray.cgColor)
        ctx.setLineWidth(isMain ? 2.5 : 1.2)
        ctx.move(to: CGPoint(x: x1, y: y1))
        ctx.addLine(to: CGPoint(x: x2, y: y2))
        ctx.strokePath()
    }
}

颜色分段可以在刻度内侧再画一圈弧线:从E开始的前六分之一用红色表示低油量区,中间大部分用白色或浅灰,靠近F的最后一段用绿色。画弧用addArc(center:radius:startAngle:endAngle:clockwise:),注意clockwise参数在iOS坐标系里传true才是屏幕上的顺时针,传反了会画出300度的大弧而不是你想要的小弧。

二、液位填充的绘制方法

液位是燃油表最有特色的部分:底部开口区域像液体一样填充到当前油量对应的高度。实现思路有两种。第一种是裁剪法:先把表盘扇形区域作为裁剪路径,然后在底部画一个矩形,矩形的高度由油量百分比决定,超出部分被裁掉,视觉上就是液体涨到了某个位置。第二种是扇形填充法:直接把135度到目标角度之间的扇形填上颜色。

两种方案各有优劣。扇形填充法实现简单,一次addArcfillPath就完事,但看起来更像普通进度条,缺少液体的质感。裁剪法可以进一步加波浪效果:用两条贝塞尔曲线拼出波浪形的液面,再让波浪相位随时间偏移,液面就会有轻微晃动感。下面是裁剪法加静态波浪的示例。

func drawFuelLevel(ctx: CGContext, center: CGPoint, radius: CGFloat, level: CGFloat, phase: CGFloat) {
    let liquidRect = CGRect(x: center.x - radius,
                            y: center.y - radius + (1 - level) * radius * 2,
                            width: radius * 2,
                            height: radius * 2)

    // 构建表盘扇形作为裁剪区域
    let clipPath = CGMutablePath()
    clipPath.addArc(center: center, radius: radius * 0.88,
                    startAngle: startAngle.degreesToRadians(),
                    endAngle: endAngle.degreesToRadians(), clockwise: true)
    clipPath.addLine(to: center)
    clipPath.closeSubpath()
    ctx.addPath(clipPath)
    ctx.clip()

    // 波浪液面
    let wave = CGMutablePath()
    let y = liquidRect.minY
    wave.move(to: CGPoint(x: liquidRect.minX, y: y))
    var x = liquidRect.minX
    while x < liquidRect.maxX {
        wave.addQuadCurve(to: CGPoint(x: x + 20, y: y),
                          control: CGPoint(x: x + 10, y: y + 6 * sin(phase + x * 0.05)))
        x += 20
    }
    wave.addLine(to: CGPoint(x: liquidRect.maxX, y: liquidRect.maxY))
    wave.addLine(to: CGPoint(x: liquidRect.minX, y: liquidRect.maxY))
    wave.closeSubpath()

    let colors = [UIColor(red: 1.0, green: 0.6, blue: 0.1, alpha: 1.0).cgColor,
                  UIColor(red: 0.9, green: 0.3, blue: 0.1, alpha: 1.0).cgColor]
    let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                              colors: colors as CFArray, locations: [0, 1])!
    ctx.addPath(wave)
    ctx.saveGState()
    ctx.clip()
    ctx.drawLinearGradient(gradient,
                           start: .zero,
                           end: CGPoint(x: 0, y: liquidRect.maxY),
                           options: [])
    ctx.restoreGState()
}

波浪路径里的phase参数是相位,后面做动画时会持续累加它,波浪就会向前滚动。渐变色让液体上部偏黄、下部偏红,比纯色填充更有层次。要注意每次调用clip()之前先saveGState(),绘制完restoreGState(),否则裁剪区域会一直生效,影响后面刻度的绘制。

三、动画驱动与低油量闪烁警告

draw方法只负责画一帧,动画需要外部驱动重绘。这里不推荐用Timer,因为它的精度和屏幕刷新不对齐。正确做法是CADisplayLink,它和屏幕刷新率同步,每帧回调一次。我们在回调里更新液位值和波浪相位,然后调用setNeedsDisplay()触发重绘。油量变化时不要直接跳到目标值,用缓动函数插值,让液位平滑地涨上去。

class FuelGaugeView: UIView {
    var fuelLevel: CGFloat = 0.75 { didSet { startAnimaton(to: fuelLevel) } }

    private var displayLevel: CGFloat = 0
    private var phase: CGFloat = 0
    private var displayLink: CADisplayLink?

    func startAnimaton(to target: CGFloat) {
        displayLink?.invalidate()
        displayLink = CADisplayLink(target: self, selector: #selector(tick))
        displayLink?.add(to: .main, forMode: .common)
        // 用简单闭包保存目标值
        animatingTarget = target
    }

    private var animatingTarget: CGFloat = 0

    @objc private func tick() {
        let diff = animatingTarget - displayLevel
        displayLevel += diff * 0.08        // 缓动插值,越接近目标走得越慢
        phase += 0.15                       // 波浪滚动
        if abs(diff) < 0.001 {
            displayLevel = animatingTarget
            // 油量稳定后可以停掉波浪动画省电,这里为了演示保留
        }
        setNeedsDisplay()
    }
}

低油量闪烁警告有两种实现思路。一种是纯Core Graphics方案:在tick回调里根据时间戳计算一个0到1的闪烁因子,比如blink = (sin(time * 4) + 1) / 2,绘制警告区域时把这个因子乘进颜色的alpha通道,红色区域就会规律地呼吸明暗。这种方案的好处是所有绘制逻辑集中在一处,状态管理简单。

另一种是Core Animation方案:给一个独立的CALayer设置opacity动画,用autoreversesrepeatCount设为无限循环,警告层就会自动闪烁,完全不需要手动刷新视图。它的优点是不占用CPU重绘开销,因为opacity动画由Core Animation渲染管线直接处理,即使主线程卡顿闪烁也不会停。缺点是警告层必须和CG绘制的表盘分层,需要额外管理一个子图层。

实际项目中建议这样取舍:如果仪表盘本来就每帧都在跑液位动画,直接用方案一,反正视图在重绘,顺手把alpha算进去零成本;如果液位是静态的、只有警告需要闪,用方案二的CAShapeLayer承载红色警告弧段,配合opacity动画,性能最优。

最后补一个容易被忽略的细节:车载场景经常跑在iPad或车机大屏上,draw方法里的半径计算要用min(bounds.width, bounds.height)做基准,同时重写layoutSubviews调用setNeedsDisplay(),保证旋转屏幕或分屏后表盘重绘到正确尺寸。把这些要点组合起来,一个专业感的燃油表组件就完成了,刻度清晰、液位有质感、警告醒目,而且全部基于系统框架,没有任何第三方依赖。

Core GraphicsiOS仪表盘绘制Quartz2D动画修改时间:2026-09-07 12:27:05

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