CALayer的动画结束回调?

79

我想知道在CALayer中动画的回调函数在哪里(或者是否存在)。具体而言,是针对像更改frame、position等隐式动画时的回调。在UIView中,您可以像这样执行:

[UIView beginAnimations:@"SlideOut" context:nil];
[UIView setAnimationDuration:.3];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(animateOut:finished:context:)];
CGRect frame = self.frame;
frame.origin.y = 480;
self.frame = frame;
[UIView commitAnimations];

具体来说,我想在CALayer中实现一个动画,我需要使用setAnimationDidStopSelector,是否有类似的功能?
谢谢!

对于任何在这里搜索的人,我已经提供了一个现代的答案来回答这个非常古老的问题!:O 向下滚动搜索到“2017年...” - Fattie
11个回答

135
您可以使用CATransaction,它有一个完成块处理程序。
[CATransaction begin];
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
[pathAnimation setDuration:1];
[pathAnimation setFromValue:[NSNumber numberWithFloat:0.0f]];    
[pathAnimation setToValue:[NSNumber numberWithFloat:1.0f]];
[CATransaction setCompletionBlock:^{_lastPoint = _currentPoint; _currentPoint = CGPointMake(_lastPoint.x + _wormStepHorizontalValue, _wormStepVerticalValue);}];
[_pathLayer addAnimation:pathAnimation forKey:@"strokeEnd"];
[CATransaction commit];

38
请注意,在将动画添加到层之前设置完成块非常重要。 - Groot
1
这并不总是有用的,因为即使您没有完成动画,当您删除动画时也会被调用。 - Yuval Tal

70

我回答了自己的问题。你需要使用CABasicAnimation添加动画,方法如下:

CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:@"frame"];
anim.fromValue = [NSValue valueWithCGRect:layer.frame];
anim.toValue = [NSValue valueWithCGRect:frame];
anim.delegate = self;
[layer addAnimation:anim forKey:@"frame"];

实现委托方法animationDidStop:finished:,然后你就可以直接使用了。感谢这个功能的存在! :D


3
这个回答提供了delegate的相关信息,但有一个很大的缺陷——由于"frame"是一个派生属性,所以无法进行动画处理。http://developer.apple.com/library/mac/#qa/qa2008/qa1620.html - Michal
您不能编辑“frame”,但是获取框架的中心并动画化位置同样容易,例如-->让theAnimation = CABasicAnimation(keyPath:“position”); 然后获取框架中心点--> theAnimation.fromValue = NSValue(cgPoint:CGPoint(x:rightImage.frame.origin.x + rightImage.frame.width / 2.0,y:rightImage.frame.origin.y + rightImage.frame.height / 2.0)) - Adam Freeman
请注意,这个10年前的答案真的不是正确的方法。(这也适用于SO上的许多旧答案!) - Fattie
1
@Fattie - 那是为什么呢?我错过了任何弃用通知吗? - silverdr

69

2018年版本...

没有比这更简单的了。

别忘了使用 [weak self],否则会崩溃。

func animeExample() {
    
    CATransaction.begin()
    
    let a = CABasicAnimation(keyPath: "fillColor")
    a.fromValue, duration = ... etc etc
    
    CATransaction.setCompletionBlock{ [weak self] in
        self?.animeExample()
        self?.ringBell()
        print("again...")
    }
    
    someLayer.add(a, forKey: nil)
    CATransaction.commit()
}

关键提示:

setCompletionBlock 必须在 someLayer.add 之前设置。

顺序很重要!这是 iOS 的一个怪癖。

在示例中,它只是再次调用自身。

当然,你也可以调用任何函数。


对于任何新手iOS动画的注意事项:

  1. "key"(例如 forKey)通常是无关紧要且很少使用的。将其设置为 nil 即可。如果要设置,将其设置为 “任何字符串”。

  2. "keyPath" 实际上是您要进行动画处理的真正 “内容”。它实际上是层的属性,如 “opacity”、“backgroundColor” 等,但必须以字符串形式编写。 (您不能在此输入 “任何您想要的东西”,它必须是图层实际属性的名称,并且必须是可动画化的。)

重申一下:“key”(很少使用 - 只需将其设置为 Nil)和 “keyPath” 完全不相关。

通常情况下,这两者被混淆在一起(由于名称上的愚蠢),这会导致各种问题。


请注意,你也可以使用委托,但是使用完成块要容易得多,因为 (A) 它是自包含的,可以在任何地方使用(B) 你通常有多个动画,在这种情况下使用委托很麻烦。


太好了,Fattie,谢谢!你介意详细解释一下[weak self]这部分吗?对我来说,即使没有它也可以正常工作。 - ixany
嗨@ixany - 如果你不使用weak self,在动画期间视图消失时它会崩溃。这是iOS工程的基础,而且你可以在这里阅读许多有关它的QA!享受吧。 - Fattie

56

这是一份基于bennythemink的解决方案,使用Swift 3.0的答案:

    // Begin the transaction
    CATransaction.begin()
    let animation = CABasicAnimation(keyPath: "strokeEnd")
    animation.duration = duration //duration is the number of seconds
    animation.fromValue = 0
    animation.toValue = 1
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
    circleLayer.strokeEnd = 1.0

    // Callback function
    CATransaction.setCompletionBlock { 
        print("end animation")
    }

    // Do the actual animation and commit the transaction
    circleLayer.add(animation, forKey: "animateCircle")
    CATransaction.commit() 

50

我浪费了4个小时在这个垃圾上,只是为了做一个淡入淡出。 注意代码中的注释。

   [CATransaction begin];
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    animation.duration = 0.3;
    animation.fromValue = [NSNumber numberWithFloat:0.0f];
    animation.toValue = [NSNumber numberWithFloat:1.0f];
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeBoth;
  ///  [box addAnimation:animation forKey:@"j"]; Animation will not work if added here. Need to add this only after the completion block.

    [CATransaction setCompletionBlock:^{

        CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:@"opacity"];
        animation2.duration = 0.3;
        animation2.beginTime = CACurrentMediaTime()+1;
        animation2.fromValue = [NSNumber numberWithFloat:1.0f];
        animation2.toValue = [NSNumber numberWithFloat:0.0f];
        animation2.removedOnCompletion = NO;
        animation2.fillMode = kCAFillModeBoth;
        [box addAnimation:animation2 forKey:@"k"];

    }];

    [box addAnimation:animation forKey:@"j"];

    [CATransaction commit];

我猜在你的情况下,你可以使用autoreverse属性,因为你所做的基本上是反转第一个动画。 - denis631
2
这个回答让我笑了!我完全经历过为了一个看似小问题而努力几个小时的情况...✊ - jason z
1
他们说你不应该使用 animation.removedOnCompletion = NO;,而是在启动动画之前设置图层的值。这样会破坏演示和模型层。 - Martin Berger

13
在Swift 4+中,我刚刚添加了delegate。
class CircleView: UIView,CAAnimationDelegate {
...

let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.delegate = self//Set delegate

动画完成的回调函数 -

func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
     print("Animation END")
  }

12

对于通过Google找到这个页面的人,有一个提示:您可以通过将动画对象的“delegate”属性设置为接收通知的对象,并在该对象的.m文件中实现“animationDidStop”方法来完成工作。我刚试过了,它能够正常工作。我不知道为什么Joe Blow说这不是正确的方法。


尝试了从removeAllAnimations到具有setCompletionBlock和CATransation.begin和commit的特定CAAnimations,就像user3077725的答案一样。尝试了UIView.animateWithDuration。对于简单的alpha和position.x动画没有起作用。委托方法是唯一正常工作的东西。 - KorinW

10

Swift 5.0

func blinkShadow(completion: @escaping (() -> Void)) {
    CATransaction.begin()
    let animation = CABasicAnimation(keyPath: "shadowRadius")
    animation.fromValue = layer.shadowRadius
    animation.toValue = 0.0
    animation.duration = 0.1
    animation.autoreverses = true
    CATransaction.setCompletionBlock(completion)
    layer.add(animation, forKey: nil)
    CATransaction.commit()
}

这对我非常有效,但我必须在Swift 5中使用CATransaction.setCompletionBlock(completion),并确保设置:animation.fillMode = .forwards animation.isRemovedOnCompletion = false以确保任何链接在一起的动画不会因为链式项而中断。谢谢! - kirikintha

0

针对2020年...

使用ValueAnimator,更新您的自定义属性。

https://github.com/Only-IceSoul/ios-jjvalueanimator

 class OnAnimationListener : AnimatorListener {

        weak var s : ViewController?

        init(_ ins: ViewController) {
            s = ins
        }
        func onAnimationStart(_ animation: Animator) {}
        func onAnimationEnd(_ animation: Animator) {

           print("end")
           s?.label.text = "end"

        }
        func onAnimationCancel(_ animation: Animator) {}
        func onAnimationRepeat(_ animation: Animator) {}

    }

0

在设置 CAAnimation 对象时,您可以设置给定动画的名称。在 animationDiStop:finished 方法中,只需比较所提供的 theAnimation 对象的名称,以执行基于动画的特定功能。


3
没有名称属性吗? - pronebird

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接