Restoring animation where it left off when app resumes from background

后端 未结 10 1306
广开言路
广开言路 2020-11-28 18:57

I have an endlessly looping CABasicAnimation of a repeating image tile in my view:

a = [CABasicAnimation animationWithKeyPath:@\"position\"];
a         


        
相关标签:
10条回答
  • 2020-11-28 19:28

    After quite a lot of searching and talks with iOS development gurus, it appears that QA1673 doesn't help when it comes to pausing, backgrounding, then moving to foreground. My experimentation even shows that delegate methods that fire off from animations, such asanimationDidStopbecome unreliable.

    Sometimes they fire, sometimes they don't.

    This creates a lot of problems because it means that, not only are you looking at a different screen that you were when you paused, but also the sequence of events currently in motion can be disrupted.

    My solution thus far has been as follows:

    When the animation starts, I get the start time:

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

    When the user hits the pause button, I remove the animation from theCALayer:

    [layer removeAnimationForKey:key];

    I get the absolute time usingCACurrentMediaTime():

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

    Using themStartTimeandstopTimeI calculate an offset time:

    mTimeOffset = stopTime - mStartTime;
    

    I also set the model values of the object to be that of thepresentationLayer. So, mystopmethod looks like this:

    //--------------------------------------------------------------------------------------------------
    
    - (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;
    }
    

    On resume, I recalculate what's left of the paused animation based upon themTimeOffset. That's a bit messy because I'm usingCAKeyframeAnimation. I figure out what keyframes are outstanding based on themTimeOffset. Also, I take into account that the pause may have occurred mid frame, e.g. halfway betweenf1andf2. That time is deducted from the time of that keyframe.

    I then add this animation to the layer afresh:

    [layer addAnimation:animationGroup forKey:key];

    The other thing to remember is that you will need to check the flag inanimationDidStopand only remove the animated layer from the parent withremoveFromSuperlayerif the flag isYES. That means that the layer is still visible during the pause.

    This method does seem very laborious. It does work though! I'd love to be able to simply do this using QA1673. But at the moment for backgrounding, it doesn't work and this seems to be the only solution.

    0 讨论(0)
  • 2020-11-28 19:29

    I was able to restore the animation (but not the animation position) by saving a copy of the current animation and adding it back on resume. I called startAnimation on load and when entering the foreground and pause when entering the background.

    - (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];
    }
    
    0 讨论(0)
  • 2020-11-28 19:30

    Just in case anyone needs a Swift 3 solution for this problem:

    All you have to do is to subclass your animated view from this class. It always persist and resume all animations on it's layer.

    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

    0 讨论(0)
  • 2020-11-28 19:34

    I write a Swift 4.2 version extension based on @cclogg and @Matej Bukovinski answers. All you need is to call layer.makeAnimationsPersistent()

    Full Gist here: CALayer+AnimationPlayback.swift, CALayer+PersistentAnimations.swift

    Core part:

    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 = UIApplication.willEnterForegroundNotification
            let enterBackground = UIApplication.didEnterBackgroundNotification
            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()
        }
    }
    
    0 讨论(0)
提交回复
热议问题