如何在UIKit中实现类似SwiftUI的UIBezierPath endAngle平滑动画
UIKit实现饼状计时器endAngle平滑动画方案
问题描述
我在UIKit中制作了一款饼状计时器,需要实现类似SwiftUI里animatableData控制endAngle的平滑收缩动画。目前用UIBezierPath绘制了饼状图形,但直接使用CABasicAnimation或UIView.animate无法为endAngle添加动画。SwiftUI里通过Shape的animatableData已实现线性动画,想知道UIKit中的可行方案。
绘制饼状图的代码:
private func getInnerTimerPiePathByPercent() -> CGPath { let radius: CGFloat = (min(bounds.size.width, bounds.size.height) - 34) / 2 let filledPercent: CGFloat = 1 - clearSegmentPercent let start: CGFloat = CGFloat.pi / 2 * 3 let end = getEndAngleByPercent(filledPercent) let center = CGPoint(x: bounds.size.width / 2,y:bounds.size.height / 2) let bezierPath = UIBezierPath() bezierPath.move(to: center) bezierPath.addArc(withCenter: center, radius: radius, startAngle: start, endAngle: end, clockwise: false) bezierPath.addLine(to: center) return bezierPath.cgPath } private func getEndAngleByPercent(_ percent: CGFloat) -> CGFloat { -CGFloat.pi * 2 * percent + CGFloat.pi / 2 * 3 }
尝试过的动画代码:
let animation = CABasicAnimation(keyPath: "path") animation.duration = 10 animation.toValue = getInnerTimerPiePathByPercent(percent: 0) animation.isRemovedOnCompletion = false innerTimerPieShapeLayer?.add(animation, forKey: nil)
方案一:自定义CALayer,暴露可动画的endAngle属性
这种方式和SwiftUI的animatableData思路一致,通过自定义Layer管理可动画属性,让Core Animation自动完成插值计算。
- 自定义Layer类:
class PieTimerLayer: CAShapeLayer { // 定义可被Core Animation识别的动画属性 @NSManaged var endAngle: CGFloat override class func needsDisplay(forKey key: String) -> Bool { if key == "endAngle" { return true } return super.needsDisplay(forKey: key) } override func action(forKey key: String) -> CAAction? { if key == "endAngle" { let animation = CABasicAnimation(keyPath: key) animation.fromValue = presentation()?.value(forKey: key) return animation } return super.action(forKey: key) } override func draw(in ctx: CGContext) { super.draw(in: ctx) // 复用饼状图绘制逻辑 let radius: CGFloat = (min(bounds.width, bounds.height) - 34) / 2 let startAngle: CGFloat = .pi * 3 / 2 let center = CGPoint(x: bounds.midX, y: bounds.midY) let path = UIBezierPath() path.move(to: center) path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) path.addLine(to: center) ctx.addPath(path.cgPath) ctx.setFillColor(fillColor ?? UIColor.clear.cgColor) ctx.fillPath() } }
- 使用自定义Layer并添加动画:
// 初始化自定义Layer let pieLayer = PieTimerLayer() pieLayer.frame = bounds pieLayer.fillColor = UIColor.blue.cgColor pieLayer.endAngle = getEndAngleByPercent(1) // 设置初始角度 view.layer.addSublayer(pieLayer) // 方式一:用UIView动画块 UIView.animate(withDuration: 10) { pieLayer.endAngle = getEndAngleByPercent(0) // 目标角度 } // 方式二:用CABasicAnimation let animation = CABasicAnimation(keyPath: "endAngle") animation.duration = 10 animation.toValue = getEndAngleByPercent(0) animation.isRemovedOnCompletion = false animation.fillMode = .forwards pieLayer.add(animation, forKey: "endAngleAnimation")
方案二:修复path动画的兼容性问题
直接使用path动画时,Core Animation要求起始和结束路径的元素数量、类型完全一致,否则会出现异常动画。同时要同步更新模型层路径,避免动画结束后跳回原状态。
修改后的动画代码:
// 先更新模型层路径,避免动画结束后跳转 let targetPath = getInnerTimerPiePathByPercent(percent: 0) innerTimerPieShapeLayer?.path = targetPath // 添加path动画 let animation = CABasicAnimation(keyPath: "path") animation.duration = 10 // 从当前显示状态的路径开始动画 animation.fromValue = innerTimerPieShapeLayer?.presentation()?.path ?? innerTimerPieShapeLayer?.path animation.toValue = targetPath innerTimerPieShapeLayer?.add(animation, forKey: "pieShrinkAnimation")
关键说明
- 方案一更适合需要频繁调整角度的场景,动画逻辑封装在Layer内部,维护更清晰。
- 方案二更适合简单一次性动画,需确保生成的起始、结束路径结构完全一致。
内容的提问来源于stack exchange,提问作者EVGKZN
相关产品推荐
相关产品推荐

