You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

CALayer frame变化动画实现:1秒间隔Timer如何让自定义进度条过渡平滑

自定义Progress Bar平滑动画优化方案

你需要的保持1秒Timer触发间隔、同时实现平滑进度动画的需求,可以通过CALayer自带的动画能力实现,完全不需要使用高频定时器,具体优化逻辑如下:


问题根因

当前代码在progress变更时触发setNeedsDisplay()重绘视图,进度条宽度是直接跳变到新值的,没有过渡效果,所以视觉上存在卡顿感。

优化思路

利用CALayer的隐式动画特性,直接修改图层的可动画属性,系统会自动生成中间过渡帧,只需让动画时长和Timer的1秒间隔对齐,就能实现每秒更新一次目标进度、中间全程平滑滚动的效果。

具体修改点

  1. 移除重复的视图配置逻辑:背景遮罩、基础色值等只需要初始化时配置一次,不需要每次重绘都重复生成
  2. 把进度更新逻辑从draw(_ rect: CGRect)迁移到layoutSubviews和progress的属性观察者中,避免触发全量重绘
  3. 给progressLayer的frame变更指定1秒的匀速动画时长,和Timer触发间隔完全匹配

优化后完整代码

class CustomProgressBar: UIView {
    
    private let progressLayer = CALayer()
    private let backgroundMask = CAShapeLayer()
    private let progressBorderRatio: CGFloat = 0.2
    
    var progress: CGFloat = 1.0 {
        didSet {
            CATransaction.begin()
            // 动画时长和Timer的1秒间隔完全对齐
            CATransaction.setAnimationDuration(1.0)
            // 设置为线性动画,保证进度匀速变化
            CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .linear))
            updateProgressLayer()
            CATransaction.commit()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        baseConfig()
        startTimer()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        baseConfig()
        startTimer()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        // 视图尺寸变化时更新全局圆角遮罩
        backgroundMask.path = UIBezierPath(roundedRect: bounds, cornerRadius: bounds.height * 0.5).cgPath
        layer.mask = backgroundMask
        updateProgressLayer()
    }
    
    private func baseConfig() {
        backgroundColor = .black
        progressLayer.backgroundColor = UIColor.white.cgColor
        layer.addSublayer(progressLayer)
    }
    
    private func startTimer() {
        Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateProgress), userInfo: nil, repeats: true)
    }
    
    @objc private func updateProgress() {
        guard progress > 0 else { return }
        progress -= 0.1
    }
    
    private func updateProgressLayer() {
        let borderSize = bounds.height * progressBorderRatio
        let targetWidth = max(bounds.width * progress - borderSize * 2, 0)
        let progressRect = CGRect(
            origin: CGPoint(x: borderSize, y: borderSize),
            size: CGSize(width: targetWidth, height: bounds.height - borderSize * 2)
        )
        progressLayer.frame = progressRect
        // 更新进度条本身的圆角遮罩
        let progressMask = CAShapeLayer()
        progressMask.path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: progressRect.size), cornerRadius: progressRect.height * 0.5).cgPath
        progressLayer.mask = progressMask
    }
}

效果说明

修改后Timer依然保持1秒触发一次,每次progress减少0.1时,系统会自动计算1秒动画过程中的所有中间帧,把进度条从旧宽度平滑过渡到新宽度,完全不需要手动维护高频定时器计算进度。

内容的提问来源于stack exchange,提问作者chr0x

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.07 08:24:02