UIBezierPath + CAShapeLayer - 填充一个圆圈的动画[重复]

Posted

技术标签:

【中文标题】UIBezierPath + CAShapeLayer - 填充一个圆圈的动画[重复]【英文标题】:UIBezierPath + CAShapeLayer - animate a circle filling up [duplicate] 【发布时间】:2016-03-06 03:31:11 【问题描述】:

我正在尝试为 CAShapeLayer 的路径设置动画,以便获得“填充”到特定数量的圆形效果。

问题

它“有效”,但并不像我认为的那样 AS 流畅,我想对其进行一些缓和。但是因为我是单独为每个 % 制作动画,所以我不确定现在是否可行。所以我正在寻找可以解决这个问题的替代方案!

视觉解释

首先——这是我想要实现的动画的一些“帧”(参见图片——圆形填充从 0% 到 25%)

代码

我创建了绿色笔触(外部):

let ovalPath = UIBezierPath(ovalInRect: CGRectMake(20, 20, 240, 240))
let circleStrokeLayer = CAShapeLayer()
circleStrokeLayer.path = ovalPath.CGPath
circleStrokeLayer.lineWidth = 20
circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
circleStrokeLayer.strokeColor = colorGreen.CGColor

containerView.layer.addSublayer(circleStrokeLayer)

我创建填充形状的初始路径(内部):

let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(0), clockwise: true)
filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
filledLayer = CAShapeLayer()
filledLayer.path = filledPathStart.CGPath
filledLayer.fillColor = colorGreen.CGColor
containerView.layer.addSublayer(filledLayer)

然后我使用 for 循环和 CABasicAnimations 数组制作动画。

var animations: [CABasicAnimation] = [CABasicAnimation]()

func animate() 

    for val in 0...25 
        let morph = CABasicAnimation(keyPath: "path")
        morph.duration = 0.01
        let filledPathEnd = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: Math().percentToRadians(CGFloat(val)), clockwise: true)
        filledPathEnd.addLineToPoint(CGPoint(x: 140, y: 140))

        morph.delegate = self
        morph.toValue = filledPathEnd.CGPath
        animations.append(morph)
    

    applyNextAnimation()


func applyNextAnimation() 

    if animations.count == 0 
        return
    

    let nextAnimation = animations[0]
    animations.removeAtIndex(0)
    filledLayer.addAnimation(nextAnimation, forKey: nil)


override func animationDidStop(anim: CAAnimation, finished flag: Bool) 
    filledLayer.path = (anim as! CABasicAnimation).toValue as! CGPath
    applyNextAnimation()

【问题讨论】:

我不明白问题是什么。 这有帮助吗? blog.pixelingene.com/2012/02/… 【参考方案1】:

不要试图创建一大堆单独的动画。 CAShapeLayer 有一个 strokeStart 和 strokeEnd 属性。动画那个

诀窍是在形状层中安装整个圆的弧线,然后创建一个 CABasicAnimation 将 shapeEnd 从 0 设置为 1(以将填充从 0% 设置为 100%)或您需要的任何值。

您可以在该动画上应用您想要的任何时间。

我在 Github 上有一个项目(恐怕是在 Objective-C 中),其中包括使用这种技术的“时钟擦除”动画。下面是它的外观:

(这是一个gif,看起来有点粗糙。实际的ios动画非常流畅。)

链接如下。在自述文件中查找“时钟擦除”动画。

iOS-CAAnimation-group-demo

时钟擦除动画将形状图层作为蒙版安装在图像视图的图层上。如果您想这样做,您可以直接绘制形状图层。

【讨论】:

所以在你的代码中,你屏蔽了一个图像,但在我的情况下,我想屏蔽填充路径?【参考方案2】:

所以我采用了 Duncan C 在他的链接 GitHub 项目(在 Objective-C 中)中的内容并将其应用到我的场景中(也将其转换为 Swift)——效果很好!

这是最终的解决方案:

// ---
// In some ViewController
// ---

let animatedCircle = AnimatedCircle()
self.addSubview(animatedCircle)
animatedCircle.runAnimation()

// ---
// The Animated Circle Class
// ---    

import UIKit
import Darwin

let pi: CGFloat = CGFloat(M_PI)
let startAngle: CGFloat = (3.0 * pi) / 2.0
let colorGray = UIColor(red: 0.608, green: 0.608, blue: 0.608, alpha: 1.000)
let colorGreen = UIColor(red: 0.482, green: 0.835, blue: 0.000, alpha: 1.000)

// ----
// Math class to handle fun circle forumals
// ----
class Math 
    func percentToRadians(percentComplete: CGFloat) -> CGFloat 
        let degrees = (percentComplete/100) * 360
        return startAngle + (degrees * (pi/180))
    


class AnimatedCircle: UIView 

    let percentComplete: CGFloat = 25

    var containerView: UIView!
    var filledLayer: CAShapeLayer!

    init() 

        super.init(frame: CGRect(x: 0, y: 0, width: 260, height: 260))

        let endAngle = Math().percentToRadians(percentComplete)

        // ----
        // Create oval bezier path and layer
        // ----
        let ovalPath = UIBezierPath(ovalInRect: CGRectMake(10, 10, 240, 240))
        let circleStrokeLayer = CAShapeLayer()
        circleStrokeLayer.path = ovalPath.CGPath
        circleStrokeLayer.lineWidth = 20
        circleStrokeLayer.fillColor = UIColor.clearColor().CGColor
        circleStrokeLayer.strokeColor = colorGreen.CGColor

        // ----
        // Create filled bezier path and layer
        // ----
        let filledPathStart = UIBezierPath(arcCenter: CGPoint(x: 140, y: 140), radius: 120, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        filledPathStart.addLineToPoint(CGPoint(x: 140, y: 140))
        filledLayer = CAShapeLayer()
        filledLayer.path = filledPathStart.CGPath
        filledLayer.fillColor = colorGreen.CGColor

        // ----
        // Add any layers to container view
        // ----
        containerView = UIView(frame: self.frame)
        containerView.backgroundColor = UIColor.whiteColor()
        containerView.layer.addSublayer(circleStrokeLayer)
        containerView.layer.addSublayer(filledLayer)

        // Set the frame of the filledLayer to match our view
        filledLayer.frame = containerView.frame


    

    func runAnimation() 


        let endAngle = Math().percentToRadians(percentComplete)

        let maskLayer = CAShapeLayer()

        let maskWidth: CGFloat = filledLayer.frame.size.width
        let maskHeight: CGFloat = filledLayer.frame.size.height
        let centerPoint: CGPoint = CGPointMake(maskWidth / 2, maskHeight / 2)    
        let radius: CGFloat = CGFloat(sqrtf(Float(maskWidth * maskWidth + maskHeight * maskHeight)) / 2)

        maskLayer.fillColor = UIColor.clearColor().CGColor
        maskLayer.strokeColor = UIColor.blackColor().CGColor
        maskLayer.lineWidth = radius

        let arcPath: CGMutablePathRef = CGPathCreateMutable()
        CGPathMoveToPoint(arcPath, nil, centerPoint.x, centerPoint.y - radius / 2)
        CGPathAddArc(arcPath, nil, centerPoint.x, centerPoint.y, radius / 2, 3*pi/2, endAngle, false)

        maskLayer.path = arcPath
        maskLayer.strokeEnd = 0

        filledLayer.mask = maskLayer
        filledLayer.mask!.frame = filledLayer.frame

        let anim = CABasicAnimation(keyPath: "strokeEnd")
        anim.duration = 3
        anim.delegate = self

        anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        anim.fillMode = kCAFillModeForwards
        anim.removedOnCompletion = false
        anim.autoreverses = false
        anim.toValue = 1.0

        maskLayer.addAnimation(anim, forKey: "strokeEnd")

    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

【讨论】:

我认为您忘记将containerView 添加到视图层次结构中。另外,如果你将percentComplete 增加到 100,你会看到内圈并不完全在边界圈的中间,【参考方案3】:

谢谢,Duncan C - 非常有帮助。

以下是使用您的建议的进度圈示例。 该视图绘制了一个随着时间的推移动画的进度圈,从一个完整的圆圈开始,然后将其自身擦除以显示圆圈后面的视图。可以修改 @IBInspectable 属性以填充圆圈而不是擦除。

这是该项目的链接。视图位于CircleTimer.view。如果你运行这个项目,你可以输入秒数,然后看到圆圈在指定时间内擦除。

https://github.com/riley2012/circle-timer-ios

这是制作动画的方法:

    open func runMaskAnimation(duration: CFTimeInterval) 

    if let parentLayer = filledLayer 
        let maskLayer = CAShapeLayer()
        maskLayer.frame = parentLayer.frame

        let circleRadius = timerFillDiameter * 0.5
        let circleHalfRadius = circleRadius * 0.5
        let circleBounds = CGRect(x: parentLayer.bounds.midX - circleHalfRadius, y: parentLayer.bounds.midY - circleHalfRadius, width: circleRadius, height: circleRadius)

        maskLayer.fillColor = UIColor.clear.cgColor
        maskLayer.strokeColor = UIColor.black.cgColor
        maskLayer.lineWidth = circleRadius

        let path = UIBezierPath(roundedRect: circleBounds, cornerRadius: circleBounds.size.width * 0.5)
        maskLayer.path = path.reversing().cgPath
        maskLayer.strokeEnd = 0

        parentLayer.mask = maskLayer

        let animation = CABasicAnimation(keyPath: "strokeEnd")
        animation.duration = duration
        animation.fromValue = 1.0
        animation.toValue = 0.0
        maskLayer.add(animation, forKey: "strokeEnd")
    

【讨论】:

以上是关于UIBezierPath + CAShapeLayer - 填充一个圆圈的动画[重复]的主要内容,如果未能解决你的问题,请参考以下文章

UIBezierPath 详解

UIBezierPath精讲

使用 UIBezierPath 时出错

UIBezierPath 阴影效果

iOS学习:CAShapeLayer与UIBezierPath动画

使用UIBezierPath绘制图形