Восстановление анимации там, где она остановилась, когда приложение возобновляет работу из фона - PullRequest
55 голосов
/ 27 сентября 2011

У меня бесконечно повторяется 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];

Я попытался «приостановить и возобновить» анимацию слоя, как описано в Технические вопросы и ответы 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, что кажется избыточным.Но когда я удалил первый набор в ноль, он не возобновил анимацию, где она была приостановлена.Это было проверено с помощью простого распознавателя жестов касания, который переключал анимацию - это не связано строго с моими проблемами с восстановлением из фона.

Какое состояние следует запомнить до удаления анимации и как восстановитьанимация из этого состояния, когда я добавлю ее позже?

Ответы [ 9 ]

49 голосов
/ 10 апреля 2012

Эй, я наткнулся на то же самое в моей игре и в итоге нашел несколько иное решение, чем вы, которое вам может понравиться :) Я подумал, что должен поделиться решением, которое нашел ...

В моем случае используется анимация UIView / UIImageView, но в основном это все еще CAAnimations по своей сути ... Суть моего метода заключается в том, что я копирую / сохраняю текущую анимацию в представлении, а затем позволяю Apple приостановить / возобновить работу, но довозобновляя я снова добавляю свою анимацию.Итак, позвольте мне представить этот простой пример:

Допустим, у меня есть UIView с именем movingView .Центр 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;
}
17 голосов
/ 02 ноября 2011

После долгих поисков и разговоров с гуру разработки под iOS выясняется, что QA1673 не помогает, когда дело доходит до паузы, фонового изображения и перемещения на передний план. Мои эксперименты даже показывают, что методы делегирования, которые запускаются из анимации, такие как animationDidStop, становятся ненадежными.

Иногда они стреляют, иногда нет.

Это создает много проблем, потому что это означает, что вы не только смотрите на другой экран, который вы видели, когда вы делали паузу, но и последовательность событий, находящихся в движении в данный момент, может быть нарушена.

Мое решение до сих пор было следующим:

Когда начинается анимация, я получаю время начала:

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

Когда пользователь нажимает кнопку паузы, я удаляю анимацию из CALayer:

[layer removeAnimationForKey:key];

Я получаю абсолютное время, используя CACurrentMediaTime():

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

Используя mStartTime и stopTime, я вычисляю время смещения:

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. Кроме того, я принимаю во внимание, что пауза могла произойти в середине кадра, например на полпути между f1 и f2. Это время вычитается из времени этого ключевого кадра.

Затем я снова добавляю эту анимацию к слою:

[layer addAnimation:animationGroup forKey:key];

Еще одна вещь, которую нужно помнить, это то, что вам нужно будет проверить флаг в animationDidStop и удалить анимированный слой из родительского элемента только с removeFromSuperlayer, если флаг YES. Это означает, что слой все еще виден во время паузы.

Этот метод кажется очень трудоемким. Это работает, хотя! Я бы хотел просто сделать это, используя QA1673 . Но в настоящее время для фоновой работы это не работает, и это, кажется, единственное решение.

14 голосов
/ 19 марта 2014

Удивительно видеть, что это не так просто. Я создал категорию, основанную на подходе cclogg, которая должна сделать его однострочным.

CALayer + MBAnimationPersistence

Просто вызовите MB_setCurrentAnimationsPersistent на своем слое после настройки желаемой анимации.

[movingView.layer MB_setCurrentAnimationsPersistent];

Или укажите анимацию, которая должна сохраняться явно.

movingView.layer.MB_persistentAnimationKeys = @[@"position"];
7 голосов
/ 09 августа 2015

Я не могу комментировать, поэтому я добавлю его в качестве ответа.

Я использовал решение cclogg, но мое приложение зависало, когда вид анимации удалялся из его суперпредставления, добавлялся снова и затем переходил в фоновый режим.

Анимация была сделана бесконечной, установив animation.repeatCount в Float.infinity.
Решением, которое я имел, было установить animation.removedOnCompletion в false.

Очень странно, чтоэто работает, потому что анимация никогда не завершена.Если у кого-то есть объяснение, мне нравится его слышать.

Еще один совет: если вы удалите представление из его суперпредставления.Не забудьте удалить наблюдателя, позвонив по номеру NSNotificationCenter.defaultCenter().removeObserver(...).

4 голосов
/ 28 апреля 2018

Я пишу расширение версии Swift 4.2 , основанное на ответах @cclogg и @Matej Bukovinski.Все, что вам нужно, это позвонить layer.makeAnimationsPersistent()

Full Gist здесь: 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 = 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()
    }
}
3 голосов
/ 12 мая 2017

На всякий случай, если кому-то понадобится решение 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
    }
}

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

2 голосов
/ 26 февраля 2015

Мне удалось восстановить анимацию (но не позицию анимации), сохранив копию текущей анимации и добавив ее обратно при возобновлении.Я вызвал 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];
}
1 голос
/ 09 апреля 2013

Я использую решение cclogg с большим эффектом.Я также хотел поделиться некоторой дополнительной информацией, которая могла бы помочь кому-то еще, потому что это меня расстраивало некоторое время.

В моем приложении у меня есть несколько анимаций, некоторые из которых зацикливаются навсегда, некоторые запускаются только один раз ипорожденный случайно.Решение cclogg работало для меня, но когда я добавил некоторый код в

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

, чтобы сделать что-то, когда были закончены только одноразовые анимации, этот код сработал бы, когда я возобновил свое приложение (используя решение cclogg) всякий раз, когда эти конкретные одноразовые анимации выполнялись, когда она была приостановлена.Поэтому я добавил флаг (переменную-член моего пользовательского класса UIImageView) и установил для него значение YES в разделе, где вы возобновляете все анимации слоев (resumeLayer в cclogg's, аналогично решению Apple QA1673), чтобы этого не происходило.Я делаю это для каждого возобновляемого UIImageView.Затем в методе animationDidStop запускайте код обработки анимации только один раз, когда этот флаг равен NO.Если это ДА, игнорируйте код обработки.Переключите флаг обратно на НЕТ в любом случае.Таким образом, когда анимация действительно завершится, ваш код обработки будет запущен.Вот так:

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

Надеюсь, это кому-нибудь поможет.

0 голосов
/ 11 мая 2019

Я распознавал состояние жеста следующим образом:

// 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...