code

앱이 백그라운드에서 다시 시작될 때 중단 된 위치에서 애니메이션 복원

codestyles 2020. 12. 30. 08:12
반응형

앱이 백그라운드에서 다시 시작될 때 중단 된 위치에서 애니메이션 복원


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];

기술 Q & A QA1673에 설명 된대로 레이어 애니메이션을 "일시 중지 및 다시 시작"하려고했습니다 .

앱이 백그라운드로 들어가면 애니메이션이 레이어에서 제거됩니다. 이를 보상하기 위해 나는 UIApplicationDidEnterBackgroundNotification전화를 듣고 전화 stopAnimationUIApplicationWillEnterForegroundNotification걸었다 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을 두 번 설정합니다. 그러나 첫 번째 0으로 설정을 제거하면 일시 중지 된 애니메이션이 다시 시작되지 않았습니다. 이것은 애니메이션을 토글하는 간단한 탭 제스처 인식기로 테스트되었습니다. 이것은 배경에서 복원하는 문제와 엄격하게 관련이 없습니다.

애니메이션이 제거되기 전에 어떤 상태를 기억해야하며 나중에 다시 추가 할 때 해당 상태에서 애니메이션을 복원하려면 어떻게해야합니까?


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

이력서에서 .NET Framework를 기반으로 일시 중지 된 애니메이션의 남은 부분을 다시 계산합니다 mTimeOffset. 을 사용하고 있기 때문에 약간 지저분 CAKeyframeAnimation합니다. .NET Framework를 기반으로 뛰어난 키 프레임이 무엇인지 알아냅니다 mTimeOffset. 또한 일시 중지가 프레임 중간에 발생했을 수 있음을 고려합니다 (예 : f1사이의 중간) f2. 해당 시간은 해당 키 프레임의 시간에서 차감됩니다.

그런 다음이 애니메이션을 레이어에 새로 추가합니다.

[layer addAnimation:animationGroup forKey:key];

기억해야 할 다른 것은 당신이 플래그를 확인해야한다는 것입니다 animationDidStop만 가진 부모로부터 애니메이션 층을 제거 removeFromSuperlayer플래그 인 경우 YES. 즉, 일시 중지 중에 레이어가 계속 표시됩니다.

이 방법은 매우 힘들어 보입니다. 그래도 작동합니다! QA1673을 사용하여 간단 하게이 작업을 수행 할 수 있기를 바랍니다 . 그러나 배경 화를위한 현재로서는 작동하지 않으며 이것이 유일한 해결책 인 것 같습니다.


이봐, 나는 내 게임에서 같은 것을 우연히 만났고 결국 당신과 약간 다른 해결책을 찾았습니다. 당신이 좋아할 것입니다 :) 내가 찾은 해결 방법을 공유해야한다고 생각했습니다 ...

제 경우는 UIView / UIImageView 애니메이션을 사용하고 있지만 기본적으로 여전히 CAAnimations가 핵심입니다 ... 제 방법의 요점은 현재 애니메이션을 뷰에 복사 / 저장 한 다음 Apple의 일시 중지 / 재개가 계속 작동하도록하는 것입니다. 다시 애니메이션을 다시 추가합니다. 이 간단한 예를 보여 드리겠습니다.

movingView 라는 UIView가 있다고 가정 해 보겠습니다 . UIView의 중심은 표준 [ UIView animateWithDuration ... ] 호출을 통해 애니메이션됩니다 . 언급 된 QA1673 코드를 사용하면 일시 중지 / 다시 시작 (앱을 종료하지 않을 때)이 훌륭하게 작동합니다. 당신의 위치에.

이 예에서 제가 한 작업은 다음과 같습니다.

  • 헤더 파일에 * CAAnimation ** 유형의 animationViewPosition 과 같은 변수가 있습니다 .
  • 앱이 백그라운드로 종료되면 다음을 수행합니다.

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

이것이 더 간단하지 않다는 것은 놀랍습니다. cclogg의 접근 방식을 기반으로 한 카테고리를 만들었습니다.

CALayer + MBAnimationPersistence

MB_setCurrentAnimationsPersistent원하는 애니메이션을 설정 한 후 레이어에서 간단히 호출 하십시오.

[movingView.layer MB_setCurrentAnimationsPersistent];

또는 명시 적으로 유지해야하는 애니메이션을 지정합니다.

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

댓글을 달 수 없으므로 답변으로 추가하겠습니다.

cclogg의 솔루션을 사용했지만 애니메이션 뷰가 수퍼 뷰에서 제거되고 다시 추가 된 다음 배경으로 이동하면 내 앱이 충돌했습니다.

로 설정 animation.repeatCount하여 애니메이션을 무한대로 만들었습니다 Float.infinity.
내가 가진 해결책은 세트에이었다 animation.removedOnCompletionfalse.

애니메이션이 완료되지 않았기 때문에 작동하는 것이 매우 이상합니다. 누구든지 설명이 있으면 듣고 싶습니다.

또 다른 팁 : 수퍼 뷰에서 뷰를 제거하는 경우. 을 호출하여 관찰자를 제거하는 것을 잊지 마십시오 NSNotificationCenter.defaultCenter().removeObserver(...).


나는 쓰기 스위프트 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()
    }
}

누군가이 문제에 대해 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


현재 애니메이션의 복사본을 저장하고 이력서에 다시 추가하여 애니메이션 (애니메이션 위치 제외)을 복원 할 수있었습니다. 로드 할 때와 전경에 들어갈 때 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];
}

나는 cclogg의 솔루션을 사용하여 큰 효과를냅니다. 또한 다른 사람에게 도움이 될만한 추가 정보를 공유하고 싶었습니다.

In my app I have a number of animations, some that loop forever, some that run only once and are spawned randomly. cclogg's solution worked for me, but when I added some code to

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

in order to do something when only the one-time animations were finished, this code would trigger when I resumed my app (using cclogg's solution) whenever those specific one-time animations were running when it was paused. So I added a flag (a member variable of my custom UIImageView class) and set it to YES in the section where you resume all the layer animations (resumeLayer in cclogg's, analogous to Apple solution QA1673) to keep this from happening. I do this for every UIImageView that is resuming. Then, in the animationDidStop method, only run the one-time animation handling code when that flag is NO. If it's YES, ignore the handling code. Switch the flag back to NO either way. That way when the animation truly finishes, your handling code will run. So like this:

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

Hope that helps someone.


I was recognizing the gesture state like so:

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

// Ignore any other state
default:
    break
}

All I needed to do was change the .ended case to .ended, .cancelled.

ReferenceURL : https://stackoverflow.com/questions/7568567/restoring-animation-where-it-left-off-when-app-resumes-from-background

반응형