当应用程序从后台恢复时,恢复动画停止的位置

时间:2011-09-27 11:46:02

标签: ios core-animation

我在视图中无休止地循环CABasicAnimation重复的图像拼贴:

a = [CABasicAnimation animationWithKeyPath:@"position"];
a.timingFunction = [CAMediaTimingFunction 
                      functionWithName:kCAMediaTimingFunctionLinear];
a.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
a.toValue = [NSValue valueWithCGPoint:CGPointMake(image.size.width, 0)];
a.repeatCount = HUGE_VALF;
a.duration = 15.0;
[a retain];

我试图按照Technical Q&A QA1673中的描述“暂停并恢复”图层动画。

当应用程序进入后台时,动画将从图层中删除。 为了补偿,我听取UIApplicationDidEnterBackgroundNotification并致电stopAnimation并回复UIApplicationWillEnterForegroundNotification来电startAnimation

- (void)startAnimation 
{
    if ([[self.layer animationKeys] count] == 0)
        [self.layer addAnimation:a forKey:@"position"];

    CFTimeInterval pausedTime = [self.layer timeOffset];
    self.layer.speed = 1.0;
    self.layer.timeOffset = 0.0;
    self.layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = 
      [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    self.layer.beginTime = timeSincePause;
}

- (void)stopAnimation 
{
    CFTimeInterval pausedTime = 
      [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
    self.layer.speed = 0.0;
    self.layer.timeOffset = pausedTime;    
}

问题是它在开始时再次启动,并且从上一个位置开始出现丑陋的跳跃,如应用程序进入后台时系统所采用的应用程序快照所示,回到动画循环的开始。

当我重新添加动画时,我无法弄清楚如何让它从最后位置开始。坦率地说,我只是不明白QA1673的代码是如何工作的:在resumeLayer中它将layer.beginTime设置两次,这似乎是多余的。但是当我删除第一个设置为零时,它没有恢复暂停的动画。这是通过简单的点击手势识别器测试的,它确实切换了动画 - 这与我从后台恢复的问题并不严格相关。

在移除动画之前我应该​​记住什么状态?当我稍后重新添加动画时,如何从该状态恢复动画?

10 个答案:

答案 0 :(得分:49)

嘿我在游戏中偶然发现了同样的事情,最终找到了一个与你不同的解决方案,你可能会喜欢:)我想我应该分享我找到的解决方法...

我的情况是使用UIView / UIImageView动画,但它基本上仍然是CAAnimations的核心...我的方法的要点是我在视图上复制/存储当前动画,然后让Apple的暂停/恢复仍然工作,但在恢复之前,我重新添加动画。让我来介绍一下这个简单的例子:

假设我有一个名为 movingView 的UIView。 UIView的中心通过标准[ UIView animateWithDuration ... ]调用动画。使用上面提到的 QA1673 代码,它可以很好地暂停/恢复(当不退出应用程序时)...但无论如何,我很快意识到退出时,无论是否暂停,动画都被完全删除......而且我在这里。

所以在这个例子中,这就是我所做的:

  • 在头文件中有一个名为 animationViewPosition 的变量,类型为* CAAnimation **。
  • 当应用退出后台时,我这样做:

    animationViewPosition = [[movingView.layer animationForKey:@"position"] copy]; // I know position is the key in this case...
    [self pauseLayer:movingView.layer]; // this is the Apple method from QA1673
    
    • 注意:那些2 ^调用是一个方法,它是 UIApplicationDidEnterBackgroundNotification 的处理程序(类似于你)
    • 注意2:如果您不知道(动画的)键是什么,您可以遍历视图的图层的' animationKeys '属性并将其记录下来(大概是动画中期)。
  • 现在在我的 UIApplicationWillEnterForegroundNotification 处理程序中:

    if (animationViewPosition != nil)
    {
        [movingView.layer addAnimation:animationViewPosition forKey:@"position"]; // re-add the core animation to the view
        [animationViewPosition release]; // since we 'copied' earlier
        animationViewPosition = nil;
    }
    [self resumeLayer:movingView.layer]; // Apple's method, which will resume the animation at the position it was at when the app exited
    

这就是它!到目前为止它对我有用:)

只需为每个动画重复这些步骤,即可轻松扩展它以获得更多动画或视图。它甚至适用于暂停/恢复UIImageView动画,即标准[ imageView startAnimating ]。 (顺便说一下)它的图层动画关键字是“内容”。

清单1暂停和恢复动画。

-(void)pauseLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    layer.speed = 0.0;
    layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer
{
    CFTimeInterval pausedTime = [layer timeOffset];
    layer.speed = 1.0;
    layer.timeOffset = 0.0;
    layer.beginTime = 0.0;
    CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    layer.beginTime = timeSincePause;
}

答案 1 :(得分:17)

经过大量搜索和与iOS开发专家的讨论后,QA1673似乎无法帮助暂停,后台处理,然后转移到前台。我的实验甚至表明,从动画中发起的委托方法(例如animationDidStop)变得不可靠。

有时候他们会开火,有时却不会开火。

这会产生很多问题,因为这意味着,您不仅会看到暂停时的不同屏幕,而且还会中断当前运动中的事件序列。

到目前为止我的解决方案如下:

当动画开始时,我得到开始时间:

mStartTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

当用户点击暂停按钮时,我会从CALayer

中删除动画

[layer removeAnimationForKey:key];

我使用CACurrentMediaTime()获得绝对时间:

CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

使用mStartTimestopTime我计算了偏移时间:

mTimeOffset = stopTime - mStartTime;

我还将对象的模型值设置为presentationLayer的模型值。所以,我的stop方法如下所示:

//--------------------------------------------------------------------------------------------------

- (void)stop
{
    const CALayer *presentationLayer = layer.presentationLayer;

    layer.bounds = presentationLayer.bounds;
    layer.opacity = presentationLayer.opacity;
    layer.contentsRect = presentationLayer.contentsRect;
    layer.position = presentationLayer.position;

    [layer removeAnimationForKey:key];

    CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
    mTimeOffset = stopTime - mStartTime;
}

在恢复时,我会根据mTimeOffset重新计算暂停动画的剩余部分。这有点乱,因为我正在使用CAKeyframeAnimation。我根据mTimeOffset找出了哪些关键帧是未完成的。此外,我考虑到暂停可能发生在帧的中间,例如在f1f2之间。该时间从该关键帧的时间开始扣除。

然后我重新将这个动画添加到图层:

[layer addAnimation:animationGroup forKey:key];

另一件需要记住的事情是,您需要检查animationDidStop中的标记,并且如果标记为removeFromSuperlayer,则仅使用YES从父级删除动画图层。这意味着在暂停期间图层仍然可见。

这种方法似乎非常费力。它确实有效!我希望能够使用QA1673简单地完成此操作。但目前为背景,它不起作用,这似乎是唯一的解决方案。

答案 2 :(得分:14)

令人惊讶的是,这并不是那么简单。我创建了一个基于cclogg方法的类别,它应该使它成为一个单行。

CALayer+MBAnimationPersistence

在设置所需的动画后,只需在图层上调用MB_setCurrentAnimationsPersistent

[movingView.layer MB_setCurrentAnimationsPersistent];

或指定应明确保留的动画。

movingView.layer.MB_persistentAnimationKeys = @[@"position"];

答案 3 :(得分:7)

我无法评论,所以我会将其添加为答案
我使用了cclogg的解决方案,但是当动画的视图从他的超级视图中删除,再次添加,然后转到后台时,我的应用程序崩溃了。

通过将animation.repeatCount设置为Float.infinity,动画变得无限 我的解决方案是将animation.removedOnCompletion设置为false
它的工作原理非常奇怪,因为动画永远不会完成。如果有人有解释,我想听听
另一个提示:如果从超级视图中删除视图。不要忘记通过拨打NSNotificationCenter.defaultCenter().removeObserver(...)来删除观察者。

答案 4 :(得分:4)

我根据@cclogg和@Matej Bukovinski的答案编写了一个 Swift 4 版本扩展。您只需拨打layer.makeAnimationsPersistent()

即可

全部要点:CALayer+AnimationPlayback.swift, CALayer+PersistentAnimations.swift

核心部分:

public extension CALayer {
    static private var persistentHelperKey = "CALayer.LayerPersistentHelper"

    public func makeAnimationsPersistent() {
        var object = objc_getAssociatedObject(self, &CALayer.persistentHelperKey)
        if object == nil {
            object = LayerPersistentHelper(with: self)
            let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
            objc_setAssociatedObject(self, &CALayer.persistentHelperKey, object, nonatomic)
        }
    }
}

public class LayerPersistentHelper {
    private var persistentAnimations: [String: CAAnimation] = [:]
    private var persistentSpeed: Float = 0.0
    private weak var layer: CALayer?

    public init(with layer: CALayer) {
        self.layer = layer
        addNotificationObservers()
    }

    deinit {
        removeNotificationObservers()
    }
}

private extension LayerPersistentHelper {
    func addNotificationObservers() {
        let center = NotificationCenter.default
        let enterForeground = NSNotification.Name.UIApplicationWillEnterForeground
        let enterBackground = NSNotification.Name.UIApplicationDidEnterBackground
        center.addObserver(self, selector: #selector(didBecomeActive), name: enterForeground, object: nil)
        center.addObserver(self, selector: #selector(willResignActive), name: enterBackground, object: nil)
    }

    func removeNotificationObservers() {
        NotificationCenter.default.removeObserver(self)
    }

    func persistAnimations(with keys: [String]?) {
        guard let layer = self.layer else { return }
        keys?.forEach { (key) in
            if let animation = layer.animation(forKey: key) {
                persistentAnimations[key] = animation
            }
        }
    }

    func restoreAnimations(with keys: [String]?) {
        guard let layer = self.layer else { return }
        keys?.forEach { (key) in
            if let animation = persistentAnimations[key] {
                layer.add(animation, forKey: key)
            }
        }
    }
}

@objc extension LayerPersistentHelper {
    func didBecomeActive() {
        guard let layer = self.layer else { return }
        restoreAnimations(with: Array(persistentAnimations.keys))
        persistentAnimations.removeAll()
        if persistentSpeed == 1.0 { // if layer was playing before background, resume it
            layer.resumeAnimations()
        }
    }

    func willResignActive() {
        guard let layer = self.layer else { return }
        persistentSpeed = layer.speed
        layer.speed = 1.0 // in case layer was paused from outside, set speed to 1.0 to get all animations
        persistAnimations(with: layer.animationKeys())
        layer.speed = persistentSpeed // restore original speed
        layer.pauseAnimations()
    }
}

答案 5 :(得分:3)

以防任何人需要针对此问题的Swift 3解决方案:

您所要做的就是从这个类中继承您的动画视图。 它始终保持并恢复其层上的所有动画。

class ViewWithPersistentAnimations : UIView {
    private var persistentAnimations: [String: CAAnimation] = [:]
    private var persistentSpeed: Float = 0.0

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.commonInit()
    }

    func commonInit() {
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func didBecomeActive() {
        self.restoreAnimations(withKeys: Array(self.persistentAnimations.keys))
        self.persistentAnimations.removeAll()
        if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it
            self.layer.resume()
        }
    }

    func willResignActive() {
        self.persistentSpeed = self.layer.speed

        self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations
        self.persistAnimations(withKeys: self.layer.animationKeys())
        self.layer.speed = self.persistentSpeed //restore original speed

        self.layer.pause()
    }

    func persistAnimations(withKeys: [String]?) {
        withKeys?.forEach({ (key) in
            if let animation = self.layer.animation(forKey: key) {
                self.persistentAnimations[key] = animation
            }
        })
    }

    func restoreAnimations(withKeys: [String]?) {
        withKeys?.forEach { key in
            if let persistentAnimation = self.persistentAnimations[key] {
                self.layer.add(persistentAnimation, forKey: key)
            }
        }
    }
}

extension CALayer {
    func pause() {
        if self.isPaused() == false {
            let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
            self.speed = 0.0
            self.timeOffset = pausedTime
        }
    }

    func isPaused() -> Bool {
        return self.speed == 0.0
    }

    func resume() {
        let pausedTime: CFTimeInterval = self.timeOffset
        self.speed = 1.0
        self.timeOffset = 0.0
        self.beginTime = 0.0
        let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
        self.beginTime = timeSincePause
    }
}

On Gist: https://gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128

答案 6 :(得分:2)

我能够通过保存当前动画的副本并将其添加回简历来恢复动画(但不是动画位置)。我在加载时调用startAnimation,在进入前景时调用startAnimation,在进入后台时调用暂停。

- (void) startAnimation {
    // On first call, setup our ivar
    if (!self.myAnimation) {
        self.myAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
        /*
         Finish setting up myAnimation
         */
    }

    // Add the animation to the layer if it hasn't been or got removed
    if (![self.layer animationForKey:@"myAnimation"]) {
        [self.layer addAnimation:self.spinAnimation forKey:@"myAnimation"];
    }
}

- (void) pauseAnimation {
    // Save the current state of the animation
    // when we call startAnimation again, this saved animation will be added/restored
    self.myAnimation = [[self.layer animationForKey:@"myAnimation"] copy];
}

答案 7 :(得分:1)

我使用cclogg的解决方案效果很好。我还想分享一些可能对其他人有帮助的其他信息,因为它让我感到沮丧了一段时间。

在我的应用程序中,我有许多动画,有些动画永远循环,有些只运行一次并随机生成。 cclogg的解决方案对我有用,但是当我添加一些代码

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag

为了在只有一次性动画完成时执行某些操作,只要这些特定的一次性动画在暂停时运行,当我恢复我的应用程序(使用cclogg的解决方案)时,此代码将会触发。所以我添加了一个标志(我的自定义UIImageView类的成员变量)并在你恢复所有图层动画的部分中将其设置为YES(cclogg的resumeLayer,类似于Apple解决方案QA1673)以防止这种情况发生。我为每个正在恢复的UIImageView执行此操作。然后,在animationDidStop方法中,仅当该标志为NO时才运行一次性动画处理代码。如果是,则忽略处理代码。无论如何都要将标志切换回NO。这样,当动画真正完成时,您的处理代码将会运行。像这样:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
    if (!resumeFlag) { 
      // do something now that the animation is finished for reals
    }
    resumeFlag = NO;
}

希望能有所帮助。

答案 8 :(得分:0)

我正在像这样识别手势状态:

// Perform action depending on the state
switch gesture.state {
case .changed:
    // Some action
case .ended:
    // Another action

// Ignore any other state
default:
    break
}

我要做的就是将.ended的大小写更改为.ended, .cancelled

答案 9 :(得分:0)

当视图从可见区域消失时,iOS将删除所有动画(不仅当应用程序进入后台时)。为了解决这个问题,我创建了自定义CALayer子类并覆盖了2个方法,因此系统不会删除动画-removeAnimationremoveAllAnimations

class CustomCALayer: CALayer {

    override func removeAnimation(forKey key: String) {

        // prevent iOS to clear animation when view is not visible
    }

    override func removeAllAnimations() {

        // prevent iOS to clear animation when view is not visible
    }

    func forceRemoveAnimation(forKey key: String) {

        super.removeAnimation(forKey: key)
    }
}

在要将此层用作主层的视图中,覆盖layerClass属性:

override class var layerClass: AnyClass {

    return CustomCALayer.self
}

要暂停和恢复动画,请执行以下操作:

extension CALayer {

    func pause() {

        guard self.isPaused() == false else {

            return
        }

        let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
        self.speed = 0.0
        self.timeOffset = pausedTime
    }

    func resume() {

        guard self.isPaused() else {

            return
        }
        let pausedTime: CFTimeInterval = self.timeOffset
        self.speed = 1.0
        self.timeOffset = 0.0
        self.beginTime = 0.0
        self.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
    }

    func isPaused() -> Bool {

        return self.speed == 0.0
    }
}