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

iOS Swift:圆形UIBezierPath进度动画填充超出预期问题

解决iOS圆形进度动画超出预期的问题

嘿,我来帮你搞定这个圆形进度动画超出预期的问题!结合你说的CollectionView Cell子视图里用CAShapeLayer做进度的场景,大概率是这几个常见的小坑在捣乱,咱们一步步捋清楚:

1. 检查strokeEnd的动画范围与模型层同步

CAShapeLayer的strokeEnd属性取值范围是0到1,如果你的CABasicAnimation的toValue设成了大于1的值,进度肯定会超出预期。另外,默认情况下动画只会修改图层的“呈现层”,不会同步到“模型层”,如果动画结束后你没把progressLayer.strokeEnd设为目标值,不仅可能出现回弹,也可能因为旧状态叠加导致异常。

正确的动画写法应该是:

func animateProgress(to targetProgress: CGFloat) {
    // 先把进度 clamp 在 0-1 范围内,避免超出
    let safeProgress = max(0.0, min(1.0, targetProgress))
    
    // 先同步模型层属性,避免动画结束后回弹或状态混乱
    progressLayer.strokeEnd = safeProgress
    
    // 创建动画
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    // 从当前呈现的进度开始,避免跳变
    animation.fromValue = progressLayer.presentation()?.strokeEnd ?? 0.0
    animation.toValue = safeProgress
    animation.duration = 0.6
    animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
    progressLayer.add(animation, forKey: "progressAnimation")
}

2. 确保圆形路径的正确性

如果你的UIBezierPath圆形不是基于正方形的rect创建,或者起点角度不对,也会让进度看起来“超出”:

  • 必须用正方形的绘制区域:取视图宽高中的较小值作为直径,避免画出椭圆
  • 建议把圆形起点设为12点钟方向(默认是3点钟),符合用户对进度的直观认知

正确的圆形路径创建代码:

private func createCircularPath() -> UIBezierPath {
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    // 减去线宽的一半,避免描边超出视图边界
    let radius = min(bounds.width, bounds.height) / 2 - progressLayer.lineWidth / 2
    // 从-π/2(12点)到3π/2(12点),顺时针绘制完整圆形
    return UIBezierPath(arcCenter: center,
                        radius: radius,
                        startAngle: -CGFloat.pi / 2,
                        endAngle: CGFloat.pi * 3 / 2,
                        clockwise: true)
}

3. 不要在drawRect里创建/添加CAShapeLayer

drawRect会在视图重绘时被多次调用(比如Cell复用、布局更新),每次调用都创建图层的话,会导致多个进度层叠加,看起来就是进度“超出”了。

正确的做法是在视图初始化时创建图层,在layoutSubviews里更新路径(适配布局变化):

class CircularProgressView: UIView {
    private let backgroundLayer = CAShapeLayer()
    private let progressLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayers()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayers()
    }
    
    private func setupLayers() {
        // 配置背景层
        backgroundLayer.lineWidth = 4
        backgroundLayer.strokeColor = UIColor.lightGray.cgColor
        backgroundLayer.fillColor = UIColor.clear.cgColor
        layer.addSublayer(backgroundLayer)
        
        // 配置进度层
        progressLayer.lineWidth = 4
        progressLayer.strokeColor = UIColor.systemBlue.cgColor
        progressLayer.fillColor = UIColor.clear.cgColor
        progressLayer.strokeEnd = 0.0
        layer.addSublayer(progressLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let path = createCircularPath()
        backgroundLayer.path = path.cgPath
        progressLayer.path = path.cgPath
    }
    
    // 上面的createCircularPath方法放在这里
    // 上面的animateProgress方法放在这里
}

4. 处理CollectionView Cell的复用问题

当Cell被复用时,旧的进度动画和状态会残留,导致新的进度显示异常。你需要在Cell的prepareForReuse里重置进度视图:

class MilestonesCollectionViewCell: UICollectionViewCell {
    @IBOutlet weak var progressView: CircularProgressView!
    
    override func prepareForReuse() {
        super.prepareForReuse()
        // 移除旧动画
        progressView.layer.removeAllAnimations()
        // 重置进度到初始状态
        progressView.progressLayer.strokeEnd = 0.0
    }
}

按照上面的步骤排查和修改,应该就能解决进度超出预期的问题啦!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 08:54:23