iOS, Restarting animation when coming out of the background
when my app comes out of the background the animation has stopped. which is normal. but i want to restart my animation from the current state. how do i do that开发者_运维技巧 without my snapping all over the place.
[UIView animateWithDuration:60 delay:0 options:(UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
} completion:nil];
i tried putting a set frame in front of the animation but that just makes it snap.
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
any ideas?
You can add an observer in your class for UIApplicationWillEnterForegroundNotification:
- (void)addNotifications {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil];
}
- (void)applicationWillEnterForeground {
[self animate];
}
- (void)animate {
[bg setFrame:CGRectMake(0, 0, 0, 0)];
[UIView animateWithDuration:60 delay:0 options:(UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
} completion:nil];
}
It is important to set the begin state of the animation (and don't forget to remove the notification observer)
well the answer of @dany_23 could work.
But I came across an other method that works just fine if you don't need to resume your animation but restart your animation, without the view or layer snapping when you reactivate the app.
in the
- (void)applicationWillResignActive:(UIApplication *)application
you call a method in your viewcontroller which implements the following code.
[view.layer removeAllAnimations];
// this following CGRect is the point where your view originally started
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
and in the
- (void)applicationDidBecomeActive:(UIApplication *)application
you call a method in your viewcontroller that just starts the animation. something like
[UIView animateWithDuration:60 delay:0 options:(UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) animations:^{
[bg setFrame:CGRectMake(0, 0, 1378, 1005)];
} completion:nil];
Hope this helps, Thanks to all who replied.
There is a better soloution here than restarting whole animation each time you come from background.
For Swift 3 you can subclass this class:
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
}
}
It will take care of pausing all your animations in current state and re-adding it when app comes from background - without resetting them.
Gist: https://gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128
You'll have to pause the animation when your app goes into the background and resume it when it becomes active again. You can find some sample code here that describes how to pause and resume an animation.
Well if I understand the question right way, you're trying to create an animation and stop it in the middle.
In this case i would suggest you to use some kind of "state" variable to store the state of animation (the frame property for example). This variable you can use in further code to put the object that is being animated in correct position. This can be done in animationDidStop function (animationDidStop:finished:). In order to implement this function you will have to use the delegation. It means to set AppNameViewController (or some other unique object for entire application) as a delegate for an animation and write the implementation in its m-file. This will allow you to run "position setup" code at the end of the animation.
Next task is to store the animation state. While running animation, Core Animation framework creates a bunch of intermediate layers (presentation layers). And these layers are displayed during the animation and then they're deleted. And when animation finishes execution the object is just gets placed into it's final state. Actually you have to set this when you create so called "explicit animation" (and if you don't, animation will play but the object will jump back in the end). Each of these presentation layers has a set or it's own copy of all the properties that are called "animatable properties". When one of the animatable properties is set as a key for the animation, Core Animation calls (BOOL)needsDisplayForKey that returns YES/NO. It tells the Core Animation whether changes to the specified key required the layer to be redisplayed. To "redisplay" means to call drawInContext method for the presentation layer. Here you can grab the properties of the presentation layer that is currently displayed and put them into "state" variable.
The animation is played in thread and in order to set the "state" variable I used delegation. It required to subclass CALayer (lets' say AnimLayer) and define a protocol in it with just one method that stores the "state". This method has one parameter which is the state. Then I implemented the protocol method that stores the state in AnimLayer class and set the unique object as a delegate. So those presentation layers (AnimLayer copies) do not store the state themselves. Instead the "state" value passed as a parameter to the delegate function is stored by unique object in main thread.
I did something like that but my task was a bit different. I don't know the easier way, sorry. Hope this helps.
@Grzegorz Krukowski's answer worked like a charm for me. I just wanted to add that if you don't need to resume the animation from where it left off, but simulate it kept running on the background, you can simply omit these lines:
let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
self.beginTime = timeSincePause
from the resume()
function in CALayer
extension, and leave beginTime
set to 0.0
.
Also, you can manually call willResignActive
and didBecomeActive
methods for animations you want to resume when ViewController
disappears, even if the app didn't go to the background.
This way will be better, just register on ApplicationDelegate will become methods and observer the status.
- (void)pauseAnimate{
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)stopAnimate{
CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0;
self.layer.timeOffset = pausedTime;
}
This is my snippet to enable a CALayer
to save and restore animations:
// CALayer+AnimationRestoration.swift
import QuartzCore
private enum AssociateKey {
static var savedAnimations: UInt8 = 0
}
public extension CALayer {
/// The saved animations to restore.
private var savedAnimations: [String: CAAnimation] {
get {
if let current = objc_getAssociatedObject(self, &AssociateKey.savedAnimations) as? [String: CAAnimation] {
return current
} else {
// lazy initialize
let new: [String: CAAnimation] = [:]
objc_setAssociatedObject(self, &AssociateKey.savedAnimations, new, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return new
}
}
set {
objc_setAssociatedObject(self, &AssociateKey.savedAnimations, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
/// Save an animation.
/// - Parameter key: The animation key for the animation to save.
func saveAnimation(for key: String) {
guard let animation = animation(forKey: key) else {
assertionFailure("no animation for key: \(key) to save")
return
}
assert(savedAnimations[key] == nil, "try to save a duplicated animation for key: \(key)")
savedAnimations[key] = animation
}
/// Restore an animation saved before.
/// - Parameter key: The animation key for the animation to restore.
func restoreAnimation(for key: String) {
guard let animation = savedAnimations[key] else {
assertionFailure("no animation for key: \(key) to restore")
return
}
add(animation, forKey: key)
savedAnimations.removeValue(forKey: key)
}
/// Clears all saved animations.
func resetSavedAnimations() {
savedAnimations = [:]
}
}
The idea is to use Objective-C Runtime to add a dictionary on the animating layer to save animations before app entering background.
To use it:
// in UIApplication.didEnterBackgroundNotification callback
animatingLayer.saveAnimation(for: "the-animation-key")
// in UIApplication.willEnterForegroundNotification callback
animatingLayer.restoreAnimation(for: "the-animation-key")
You'd better to call saveAnimation(for:)
and restoreAnimation(for:)
in pairs to keep the savedAnimations
in a good state. Or you can call resetSavedAnimations()
to clean it.
As of the release of swift5.
It is also worth noting. That in some cases, you need to reset the animated property to get the new animation to stick. I can confirm this with an animated transformation.
Just calling...
view.layer.removeAllAnimations()
did not suffice, and new animation would not run. I also had to run...
view.transform = .identity
I suspect you might be able to set it to any new value or save the previous state and set it.
I don't recall the exact details but I think you can get the current animated position of the frame by looking at the layer bounds and position property of your view. You could store those at app suspend and restore them when the app is in the foreground again.
精彩评论