导读:本期聚焦于剑客创作的《iOS仪表盘油量表液位波动动画如何实现?用Core Graphics正弦波模拟液体晃动效果》,敬请观看详情。仪表盘里的油量表如果只是显示一条静态的液位线,观感会显得生硬。本文介绍一种基于Core Graphics的绘制方案,通过正弦波函数模拟液体表面的晃动,配合阻尼衰减实现接近真实物理效果的液位动画。内容涵盖波形叠加的数学原理、CADisplayLink驱动的逐帧重绘、上下波动的相位与振幅控制,以及如何根据车辆加减速状态调整晃动强度。文中给出完整的Swift绘制代码,包括贝塞尔路径裁剪、液面高光处理和性能优化建议,适合需要在iOS仪表盘、水球图或进度控件中加入液态效果的开发者参考。

车载仪表盘上的油量表想要做出真实感,光有一条水平的液位线是不够的。真实世界里的液体在车辆启停、转弯时会来回晃动,液面呈现明显的波浪起伏。要在iOS端还原这种效果,Core Graphics配合正弦波函数是最直接的方案:正弦波提供周期性的波动形态,阻尼系数模拟液体逐渐恢复平静的过程,再借助CADisplayLink做逐帧重绘,就能得到一个流畅且符合直觉的液位动画。本文从数学模型、绘制实现到性能优化,完整拆解这套方案。

iOS仪表盘油量表液位波动动画如何实现?用Core Graphics正弦波模拟液体晃动效果

一、正弦波模拟液面的数学原理

液体的晃动本质上是一种往复运动,而正弦函数 y = A * sin(ωx + φ) 恰好能描述这种形态。在这个公式里,A控制波峰到波谷的高度,也就是晃动的剧烈程度;ω决定波长,值越小波浪越宽越平缓,符合油箱里液体惯性感较强的特点;φ是相位,随时间推移不断变化,正是它让整个波形产生横向移动的动态感。

单一正弦波看起来会比较规则,真实液体的表面往往是几个不同频率的波叠加的结果。所以在实现时通常用两到三个正弦波叠加:一个低频大振幅的波承担整体倾斜感,一个高频小振幅的波叠加细节纹理,视觉上会自然很多。振幅随时间按指数衰减,公式可以写成 A(t) = A0 * e^(-kt),当外部刺激停止后,液面会在一两秒内逐渐平静下来,这个衰减过程是真实感的关键。

另外还有一个容易被忽略的细节:液面中心的位置。当车辆加速时,液面整体会向后倾斜,减速则向前倾斜。这个效果可以通过给液面添加一个随速度变化的倾斜角度来实现,也就是在正弦波的基础上叠加一个线性项,让液面不再是绕着中心对称晃动,而是整体带一个偏移。

二、Core Graphics绘制液位路径的实现

绘制思路是先把油量表的圆形或圆角矩形容器画出来,再用一条由多个点连成的波浪曲线作为液面,把波形以下的区域填充为液体颜色。具体做法是沿x轴方向每隔2到3个像素采样一次正弦函数,把每个采样点连成CGPath,最后闭合到容器底部。下面是核心的绘制函数:

class FuelGaugeView: UIView {
    // 波动参数
    private var phase: CGFloat = 0          // 当前相位
    private var amplitude: CGFloat = 0      // 当前振幅
    private var targetAmplitude: CGFloat = 0 // 目标振幅
    private var fillLevel: CGFloat = 0.5    // 油量 0~1

    private func liquidPath(in rect: CGRect) -> UIBezierPath {
        let liquidRect = bounds.insetBy(dx: 8, dy: 8)
        let surfaceY = liquidRect.maxY - fillLevel * liquidRect.height
        let path = UIBezierPath()
        path.move(to: CGPoint(x: liquidRect.minX, y: surfaceY))

        let step: CGFloat = 3
        var x = liquidRect.minX
        while x <= liquidRect.maxX {
            // 两个正弦波叠加,模拟真实液面
            let y = surfaceY
                + sin((x / liquidRect.width) * .pi * 4 + phase) * amplitude
                + sin((x / liquidRect.width) * .pi * 7 + phase * 1.7) * amplitude * 0.4
            path.addLine(to: CGPoint(x: x, y: y))
            x += step
        }
        // 闭合到容器底部
        path.addLine(to: CGPoint(x: liquidRect.maxX, y: liquidRect.maxY))
        path.addLine(to: CGPoint(x: liquidRect.minX, y: liquidRect.maxY))
        path.close()
        return path
    }

    override func draw(_ rect: CGRect) {
        guard let ctx = UIGraphicsGetCurrentContext() else { return }
        // 裁剪出容器的圆角形状
        let container = UIBezierPath(roundedRect: bounds.insetBy(dx: 8, dy: 8),
                                     cornerRadius: 20)
        ctx.addPath(container.cgPath)
        ctx.clip()
        // 填充液体
        UIColor.systemBlue.setFill()
        liquidPath(in: rect).fill()
    }
}

这段代码里有几个点值得注意。采样步长设为3像素是一个平衡值,步长太大波形会出现明显的折线感,太小则会增加路径点数量影响性能。两个正弦波的频率设置为4π和7π,相位速度略有差异,叠加后波形不会呈现出机械的重复感。

液体颜色建议做一点渐变处理,用CGGradient从液面往下由浅入深,再在液面附近画一条半透明白色的高光线,模拟液体表面对环境光的反射。这个小细节对整体质感的提升非常明显,成本却几乎可以忽略。

三、CADisplayLink驱动动画与物理参数控制

波形是动态的,必须逐帧更新相位和振幅。CADisplayLink是与屏幕刷新率同步的定时器,用它驱动重绘比Timer更平滑。在回调里做三件事:推进相位、让振幅向目标值过渡、调用setNeedsDisplay触发重绘。

extension FuelGaugeView {
    func startAnimating() {
        let link = CADisplayLink(target: self, selector: #selector(tick))
        link.add(to: .main, forMode: .common)
    }

    @objc private func tick() {
        phase += 0.08
        // 振幅向目标值平滑过渡,模拟阻尼
        amplitude += (targetAmplitude - amplitude) * 0.05
        // 附加一个随速度产生的倾斜偏移
        setNeedsDisplay()
    }

    // 外部接口:车辆状态变化时调用
    func setVehicleMotion(acceleration: CGFloat, turning: CGFloat) {
        // 加速度和转向越剧烈,目标振幅越大
        targetAmplitude = min(10, abs(acceleration) * 20 + abs(turning) * 12)
    }
}

振幅过渡采用简单的线性插值,系数0.05意味着每帧向目标靠近百分之五,换算下来大约一秒内完成大部分过渡,这个手感接近真实液体的惯性。当targetAmplitude归零后,amplitude会逐渐衰减到0,液面自然恢复平静,不需要额外写衰减逻辑。

与车速联动是提升沉浸感的部分。可以通过CoreMotion获取设备的加速度数据,也可以直接接收车机总线传来的车速信号,把加速度和横向摆动映射为目标振幅。刹车时液面前倾、起步时后仰,再叠加正弦波晃动,仪表盘就活起来了。

四、性能优化与常见问题处理

逐帧重绘对CPU有一定压力,尤其当仪表盘页面上还有转速表、速度表等多个控件同时刷新时。优化手段主要有三个:第一,把LiquidGaugeView的绘制放到独立的layer上,或者改用CAShapeLayer配合displayLink更新path属性,让动画走隐式提交,避免整块视图重绘;第二,将isOpaque设为false的同时确保backgroundColor为clear,避免无意义的透明度合成;第三,采样点数量控制在100个以内,肉眼上已经足够平滑。

还有一个常见问题是液面超过容器边界。当fillLevel接近1时,波形可能超出容器顶部,解法是在填充液体前先对容器路径做裁剪,前面代码里的clip调用就是为此准备的。反过来当fillLevel接近0时,波形可能悬空露底,可以在绘制前对surfaceY做一次clamp,保证液面最低点仍然贴着容器底部。

最后提一下适配问题。如果液位控件要在不同尺寸的屏幕上使用,振幅和波长参数应该按控件宽度做归一化,例如振幅取宽度的百分之几,而不是写死像素值,这样在大屏车机和小屏手机上才能保持一致的视觉比例。整套方案的代码量不大,但波形叠加、阻尼过渡、运动联动这三个环节缺一不可,把它们组合好,油量表的液态质感就能明显上一个台阶。

Core GraphicsiOS动画正弦波修改时间:2026-09-12 22:12:39

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