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

如何在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自动完成插值计算。

  1. 自定义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()
    }
}
  1. 使用自定义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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 17:53:10