【问题标题】:Swift: Animate layers in sequence from UIViewSwift:从 UIView 依次为图层设置动画
【发布时间】:2026-01-23 13:25:01
【问题描述】:

我在 UIView 中添加了几层,现在希望按顺序对它们进行动画处理。

我有 5 层动画,这些都是圆圈。我使用 observable 字段来获取值更改的通知,并开始使用 needsDisplayForKey() 和 actionForKey() 方法制作动画。

可观察字段:

@NSManaged private var _startAngle: CGFloat

@NSManaged private var _endAngle: CGFloat

当前,当我更改每个图层的一个可观察字段时,所有图层都会同时设置动画。我希望改变这一点,并希望在 2.5 秒的时间跨度内按顺序对它们进行动画处理。因此,当一个动画完成后,我希望为下一层等制作动画。

目前我知道动画已经完成的唯一方法是使用 animationDidStop() 方法。然而,这样我就无法知道正在制作动画的其他图层。你们知道有什么好的解决方案吗?

我的图层:

class HourLayer: CAShapeLayer {

@NSManaged private var _startAngle: CGFloat

@NSManaged private var _endAngle: CGFloat

@NSManaged private var _fillColor: UIColor

@NSManaged private var _strokeWidth: CGFloat

@NSManaged private var _strokeColor: UIColor

@NSManaged private var _duration: CFTimeInterval

private var _previousEndAngle: CGFloat = 0.0
private var _previousStartAngle: CGFloat = 0.0

private let _lenghtOfArc: CGFloat = CGFloat(2 * M_PI)

internal var StartAngle: CGFloat {
    get {
        return _startAngle
    }
    set {
        _startAngle = newValue
    }
}

internal var EndAngle: CGFloat {
    get {
        return _endAngle
    }
    set {
        _endAngle = newValue
    }
}

internal var FillColor: UIColor {
    get {
        return _fillColor
    }
    set {
        _fillColor = newValue
    }
}

internal var StrokeWidth: CGFloat {
    get {
        return _strokeWidth
    }
    set {
        _strokeWidth = newValue
    }
}

internal var StrokeColor: UIColor {
    get {
        return _strokeColor
    }
    set {
        _strokeColor = newValue
    }
}

internal var DurationOfAnimation: CFTimeInterval {
    get {
        return _duration
    }
    set {
        _duration = newValue
    }
}

required override init!() {
    super.init()

    self._startAngle = 0.0
    self._endAngle = 0.0
    self._fillColor = UIColor.clearColor()
    self._strokeWidth = 4.0
    self._strokeColor = UIColor.blackColor()
    self._duration = 1.0
}

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    fatalError("required init(:coder) is missing")
}

override init!(layer: AnyObject!) {
    super.init(layer: layer)

    if layer.isKindOfClass(HourLayer) {
        var other: HourLayer = layer as HourLayer
        self._startAngle = other._startAngle
        self._endAngle = other._endAngle
        self._fillColor = other._fillColor
        self._strokeWidth = other._strokeWidth
        self._strokeColor = other._strokeColor
        self._duration = other._duration
    }
}

override var frame: CGRect {
    didSet {
        self.setNeedsLayout()
        self.setNeedsDisplay()
    }
}

override func actionForKey(event: String!) -> CAAction! {
    if event == "_startAngle" || event == "_endAngle" {
        return makeAnimationForKey(event)
    }
    return super.actionForKey(event)
}

private func makeAnimationForKey(event: String) -> CABasicAnimation {
    // We want to animate the strokeEnd property of the circleLayer
    let animation: CABasicAnimation = CABasicAnimation(keyPath: event)
    // Set the animation duration appropriately
    animation.duration = self._duration

    // When no animation has been triggered and the presentation layer does not exist yet.
    if self.presentationLayer() == nil {
        if event == "_startAngle" {
            animation.fromValue = self._previousStartAngle
            self._previousStartAngle = self._startAngle
        } else if event == "_endAngle" {
            animation.fromValue = self._previousEndAngle
            self._previousEndAngle = self._endAngle
        }
    } else {
        animation.fromValue = self.presentationLayer()!.valueForKey(event)
    }
    animation.removedOnCompletion = false
    animation.delegate = self
    // Do a linear animation (i.e. the speed of the animation stays the same)
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    return animation
}

override class func needsDisplayForKey(key: String) -> Bool {
    if key == "_startAngle" || key == "_endAngle" {
        return true
    }
    return super.needsDisplayForKey(key)
}

override func drawInContext(ctx: CGContext!) {
    var center: CGPoint = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2)
    var radius: CGFloat = CGFloat((CGFloat(self.bounds.width) - CGFloat(_strokeWidth)) / 2)

    // Set the stroke color
    CGContextSetStrokeColorWithColor(ctx, _strokeColor.CGColor)

    // Set the line width
    CGContextSetLineWidth(ctx, _strokeWidth)

    // Set the fill color (if you are filling the circle)
    CGContextSetFillColorWithColor(ctx, UIColor.clearColor().CGColor)

    // Draw the arc around the circle
    CGContextAddArc(ctx, center.x, center.y, radius, _startAngle, _lenghtOfArc * _endAngle, 0)

    // Draw the arc
    CGContextDrawPath(ctx, kCGPathStroke) // or kCGPathFillStroke to fill and stroke the circle

    super.drawInContext(ctx)
}

override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
    // Trigger animation for other layer by setting their values for _startAngle and _endAngle.
}
}

【问题讨论】:

  • 制作动画时请分享代码

标签: ios swift animation cashapelayer


【解决方案1】:

Apple 文档声明如下:https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html

如果您想将两个动画链接在一起,以便一个在另一个完成时开始,请不要使用动画通知。相反,请使用动画对象的 beginTime 属性在所需时间开始每个动画对象。要将两个动画链接在一起,请将第二个动画的开始时间设置为第一个动画的结束时间。有关动画和计时值的更多信息,请参阅自定义动画的计时。

我有一些工作,但我认为它不是最理想的解决方案。我宁愿认为通知是一种更好的方式,但是如果 Apple 说得对的话,嘿嘿......

我还必须设置:animation.fillMode = kCAFillModeBackwards 否则模型层会使圆圈在动画之前达到其最终状态,并且只有稍后才会触发动画。

编辑

好的,所以我发现为我的动画使用多个图层很慢(我为每个视图使用了多个图层,所以我想制作一个圆圈,我有大约六个。这样做时间 8 非常慢(6x8 =48 个动画)。)。在一层中为所有内容设置动画效果要快得多。

【讨论】:

    【解决方案2】:

    我的解决方案是在objective-c中,你需要快速采用它。试试看:-
    创建一个 NSOperationQueue 并将动画调用添加到此队列。设置setMaxConcurrentOperationCount = 1,保证顺序执行。

    NSOperationQueue *animQueue = [[NSOperationQueue alloc] init];  
    [animQueue setMaxConcurrentOperationCount:1];
    

    现在从函数actionForKey而不是调用动画函数,将其添加到操作队列中

    NSBlockOperation *animOperation = [[NSBlockOperation alloc] init];
    __weak NSBlockOperation *weakOp = animOperation;
    [weakOp addExecutionBlock:^{
       [self makeAnimationForKey:event)]
    }];
    [animQueue addOperation:animOperation];
    

    我没有对此进行测试,但我认为它应该可以工作。

    【讨论】:

    • 你知道我是否可以毫无问题地将这个 OperationQueue 从 UIView 传递给 CAShapeLayers 吗?
    • 不能肯定地说,但据我了解,通过将方法添加到OperationQueue 我们实际上并没有将任何对象传递给其他接收器,我们只是序列化执行。所以不管你是想用UIView还是CAShapeLayer;不应该有任何区别。试试看吧,应该不会花太多时间。
    • 从添加图层的 UIView 将队列传递给 CAShapeLayers 时似乎不起作用。
    【解决方案3】:

    试试这个..

        UIview.animateWithDuration(2.0, delay 2.0, options: .CurveEaseIn, animations: {
    
    //enter item to be animated here
    
    }, completion: nil)
    

    【讨论】:

    • 我一直在研究这个问题,我在这里给出了答案:*.com/questions/28784108/… 表明你不能用这个方法为 endAngle 属性设置动画。