CALayer frame变化动画实现:1秒间隔Timer如何让自定义进度条过渡平滑
自定义Progress Bar平滑动画优化方案
你需要的保持1秒Timer触发间隔、同时实现平滑进度动画的需求,可以通过CALayer自带的动画能力实现,完全不需要使用高频定时器,具体优化逻辑如下:
问题根因
当前代码在progress变更时触发setNeedsDisplay()重绘视图,进度条宽度是直接跳变到新值的,没有过渡效果,所以视觉上存在卡顿感。
优化思路
利用CALayer的隐式动画特性,直接修改图层的可动画属性,系统会自动生成中间过渡帧,只需让动画时长和Timer的1秒间隔对齐,就能实现每秒更新一次目标进度、中间全程平滑滚动的效果。
具体修改点
- 移除重复的视图配置逻辑:背景遮罩、基础色值等只需要初始化时配置一次,不需要每次重绘都重复生成
- 把进度更新逻辑从
draw(_ rect: CGRect)迁移到layoutSubviews和progress的属性观察者中,避免触发全量重绘 - 给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
相关产品推荐
相关产品推荐

