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

Swift iOS:倒计时剩5秒时仅CALayer已进度部分变红问题

解决方案:实现剩余5秒时仅已进度环形变红

核心思路

要实现“剩余5秒时仅已完成进度段变红,背景层保持原色”的效果,核心是区分正常进度段和高亮进度段,通过双图层或动态切换进度层颜色来实现,避免修改整个图层的颜色。

以下提供两种可行方案,优先推荐双图层方案(更灵活,支持渐变高亮):


方案一:双进度层精准控制(推荐)

通过两个CAShapeLayer分别承载正常颜色进度和红色高亮进度,根据剩余时间动态切换显示逻辑:

1. 修改第三方库视图类

假设原库用单个progressLayer绘制进度,替换为两个进度层:

import UIKit

class RingCountdownView: UIView {
    // 背景层(保持原逻辑不变)
    private let backgroundLayer = CAShapeLayer()
    // 正常颜色进度层
    private let normalProgressLayer = CAShapeLayer()
    // 红色高亮进度层
    private let highlightProgressLayer = CAShapeLayer()
    
    // 配置属性
    var progressLineWidth: CGFloat = 8.0
    var originalProgressColor: UIColor = .systemBlue
    var originalBackgroundColor: UIColor = .systemGray5
    private var totalDuration: TimeInterval = 0
    private var remainingTime: TimeInterval = 0
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupLayers()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupLayers()
    }
    
    private func setupLayers() {
        // 背景层配置(原逻辑保留)
        backgroundLayer.lineWidth = progressLineWidth
        backgroundLayer.strokeColor = originalBackgroundColor.cgColor
        backgroundLayer.fillColor = UIColor.clear.cgColor
        backgroundLayer.lineCap = .round
        layer.addSublayer(backgroundLayer)
        
        // 正常进度层配置
        normalProgressLayer.lineWidth = progressLineWidth
        normalProgressLayer.strokeColor = originalProgressColor.cgColor
        normalProgressLayer.fillColor = UIColor.clear.cgColor
        normalProgressLayer.lineCap = .round
        normalProgressLayer.strokeEnd = 0
        layer.addSublayer(normalProgressLayer)
        
        // 高亮进度层配置(默认隐藏)
        highlightProgressLayer.lineWidth = progressLineWidth
        highlightProgressLayer.strokeColor = UIColor.systemRed.cgColor
        highlightProgressLayer.fillColor = UIColor.clear.cgColor
        highlightProgressLayer.lineCap = .round
        highlightProgressLayer.strokeStart = 0
        highlightProgressLayer.strokeEnd = 0
        highlightProgressLayer.isHidden = true
        layer.addSublayer(highlightProgressLayer)
        
        // 更新路径(和背景层一致)
        updateLayerPaths()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        updateLayerPaths()
    }
    
    private func updateLayerPaths() {
        let radius = min(bounds.width, bounds.height)/2 - progressLineWidth/2
        let path = UIBezierPath(arcCenter: center,
                                radius: radius,
                                startAngle: -CGFloat.pi/2,
                                endAngle: CGFloat.pi*3/2,
                                clockwise: true)
        backgroundLayer.path = path.cgPath
        normalProgressLayer.path = path.cgPath
        highlightProgressLayer.path = path.cgPath
    }
    
    // 对外暴露的更新方法
    func updateCountdown(remainingTime: TimeInterval, totalDuration: TimeInterval) {
        self.remainingTime = remainingTime
        self.totalDuration = totalDuration
        
        let totalProgress = CGFloat((totalDuration - remainingTime)/totalDuration)
        let criticalProgress = CGFloat((totalDuration - 5)/totalDuration) // 剩余5秒时的已完成进度
        
        if remainingTime > 5 {
            // 未到最后5秒:仅显示正常进度层
            highlightProgressLayer.isHidden = true
            normalProgressLayer.strokeEnd = totalProgress
        } else {
            if totalProgress <= criticalProgress {
                // 进度还未进入最后5秒区间:继续显示正常进度
                highlightProgressLayer.isHidden = true
                normalProgressLayer.strokeEnd = totalProgress
            } else {
                // 进入最后5秒:正常进度显示到临界值,高亮进度显示后续新增部分
                highlightProgressLayer.isHidden = false
                normalProgressLayer.strokeEnd = criticalProgress
                highlightProgressLayer.strokeStart = criticalProgress
                highlightProgressLayer.strokeEnd = totalProgress
            }
        }
    }
}

2. 业务代码中调用

// 初始化倒计时视图
let ringView = RingCountdownView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.addSubview(ringView)

// 配置总时长和剩余时间
let totalDuration: TimeInterval = 60 // 总倒计时60秒
var remainingTime = totalDuration

// 实时更新倒计时
let timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in
    guard let self = self else { return }
    remainingTime -= 0.1
    if remainingTime <= 0 {
        timer.invalidate()
        remainingTime = 0
    }
    ringView.updateCountdown(remainingTime: remainingTime, totalDuration: totalDuration)
}

方案二:动态切换进度层颜色(简单版)

如果不需要区分“最后5秒前的进度”和“最后5秒内的进度”,仅需要剩余5秒时整个已进度段变红,可直接修改进度层颜色:

修改第三方库的进度更新方法

找到原库中更新进度的方法(比如updateProgress(to:)),添加颜色判断逻辑:

func updateProgress(to progress: CGFloat, remainingTime: TimeInterval) {
    // 背景层保持原颜色,不修改
    backgroundLayer.strokeColor = originalBackgroundColor.cgColor
    
    // 根据剩余时间切换进度层颜色
    let targetColor = remainingTime <= 5 ? UIColor.systemRed.cgColor : originalProgressColor.cgColor
    // 添加平滑颜色过渡动画(可选)
    let colorAnim = CABasicAnimation(keyPath: "strokeColor")
    colorAnim.fromValue = progressLayer.strokeColor
    colorAnim.toValue = targetColor
    colorAnim.duration = 0.3
    progressLayer.add(colorAnim, forKey: "colorTransition")
    
    progressLayer.strokeColor = targetColor
    progressLayer.strokeEnd = progress
}

注意事项

  1. 若不想直接修改第三方库源码,可通过继承原库视图类重写绘制逻辑,避免后续库更新冲突。
  2. 确保进度层的lineWidth、lineCap等属性与背景层一致,避免显示错位。
  3. 若需要更流畅的动画,可使用CADisplayLink替代Timer进行帧级更新。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 05:54:17