Подтвердить что ты не робот

Восстановление анимации, когда она прекратилась, когда приложение возобновляется с фона

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

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

4b9b3361

Ответ 1

После довольно многого поиска и переговоров с гуру разработки 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. Но на данный момент для фоновой работы это не работает, и это кажется единственным решением.

Ответ 2

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

В моем случае используются анимации 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 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;
}

Ответ 3

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

CALayer + MBAnimationPersistence

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

[movingView.layer MB_setCurrentAnimationsPersistent];

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

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

Ответ 4

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

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

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

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

Ответ 5

Я пишу расширение версии Swift 4.2, основанное на ответах @cclogg и @Matej Bukovinski. Все, что вам нужно, это вызвать 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 = 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()
    }
}

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

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

чтобы сделать что-то, когда были завершены только одноразовые анимации, этот код будет запускаться, когда я возобновил свое приложение (используя решение cclogg), когда бы эти конкретные одноразовые анимации выполнялись, когда они были приостановлены. Поэтому я добавил флаг (переменную-член моей модели UIImageView) и установил его в YES, в котором вы возобновляете все анимации слоя (resumeLayer в cclogg, аналогичном решению 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;
}

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

Ответ 9

Я распознал состояние жеста так:

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