How to stop and reverse a UIView animation? - ios
I have animated a UIView so that it shrinks when the user touches a toggle button and it expands back to its original size when the user touches the button again. So far everything works just fine. The problem is that the animation takes some time - e.g. 3 seconds. During that time I still want the user to be able to interact with the interface. So when the user touches the button again while the animation is still in progress the animation is supposed to stop right where it is and reverse.
In the Apple Q&As I have found a way to pause all animations immediately:
https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html
But I do not see a way to reverse the animation from here (and omit the rest of the initial animation). How do I accomplish this?
- (IBAction)toggleMeter:(id)sender {
if (self.myView.hidden) {
self.myView.hidden = NO;
[UIView animateWithDuration:3 animations:^{
self.myView.transform = expandMatrix;
} completion:nil];
} else {
[UIView animateWithDuration:3 animations:^{
self.myView.transform = shrinkMatrix;
} completion:^(BOOL finished) {
self.myView.hidden = YES;
}];
}
}
In addition to the below (in which we grab the current state from the presentation layer, stop the animation, reset the current state from the saved presentation layer, and initiate the new animation), there is a much easier solution.
If doing block-based animations, if you want to stop an animation and launch a new animation in iOS versions prior to 8.0, you can simply use the UIViewAnimationOptionBeginFromCurrentState option. (Effective in iOS 8, the default behavior is to not only start from the current state, but to do so in a manner that reflects both the current location as well as the current velocity, rendering it largely unnecessary to worry about this issue at all. See WWDC 2014 video Building Interruptible and Responsive Interactions for more information.)
[UIView animateWithDuration:3.0
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction
animations:^{
// specify the new `frame`, `transform`, etc. here
}
completion:NULL];
You can achieve this by stopping the current animation and starting the new animation from where the current one left off. You can do this with Quartz 2D:
Add QuartzCore.framework to your project if you haven't already. (In contemporary versions of Xcode, it is often unnecessary to explicitly do this as it is automatically linked to the project.)
Import the necessary header if you haven't already (again, not needed in contemporary versions of Xcode):
#import <QuartzCore/QuartzCore.h>
Have your code stop the existing animation:
[self.subview.layer removeAllAnimations];
Get a reference to the current presentation layer (i.e. the state of the view as it is precisely at this moment):
CALayer *currentLayer = self.subview.layer.presentationLayer;
Reset the transform (or frame or whatever) according to the current value in the presentationLayer:
self.subview.layer.transform = currentLayer.transform;
Now animate from that transform (or frame or whatever) to the new value:
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
Putting that all together, here is a routine that toggles my transform scale from 2.0x to identify and back:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CALayer *currentLayer = self.subview.layer.presentationLayer;
[self.subview.layer removeAllAnimations];
self.subview.layer.transform = currentLayer.transform;
CATransform3D newTransform;
self.large = !self.large;
if (self.large)
newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
else
newTransform = CATransform3DIdentity;
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
}
Or if you wanted to toggle frame sizes from 100x100 to 200x200 and back:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CALayer *currentLayer = self.subview.layer.presentationLayer;
[self.subview.layer removeAllAnimations];
CGRect newFrame = currentLayer.frame;
self.subview.frame = currentLayer.frame;
self.large = !self.large;
if (self.large)
newFrame.size = CGSizeMake(200.0, 200.0);
else
newFrame.size = CGSizeMake(100.0, 100.0);
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.frame = newFrame;
}
completion:NULL];
}
By the way, while it generally doesn't really matter for really quick animations, for slow animations like yours, you might want to set the duration of the reversing animation to be the same as how far you've progressed in your current animation (e.g., if you're 0.5 seconds into a 3.0 second animation, when you reverse, you probably don't want to take 3.0 seconds to reverse that small portion of the animation that you have done so far, but rather just 0.5 seconds). Thus, that might look like:
- (IBAction)didTouchUpInsideAnimateButton:(id)sender
{
CFTimeInterval duration = kAnimationDuration; // default the duration to some constant
CFTimeInterval currentMediaTime = CACurrentMediaTime(); // get the current media time
static CFTimeInterval lastAnimationStart = 0.0; // media time of last animation (zero the first time)
// if we previously animated, then calculate how far along in the previous animation we were
// and we'll use that for the duration of the reversing animation; if larger than
// kAnimationDuration that means the prior animation was done, so we'll just use
// kAnimationDuration for the length of this animation
if (lastAnimationStart)
duration = MIN(kAnimationDuration, (currentMediaTime - lastAnimationStart));
// save our media time for future reference (i.e. future invocations of this routine)
lastAnimationStart = currentMediaTime;
// if you want the animations to stay relative the same speed if reversing an ongoing
// reversal, you can backdate the lastAnimationStart to what the lastAnimationStart
// would have been if it was a full animation; if you don't do this, if you repeatedly
// reverse a reversal that is still in progress, they'll incrementally speed up.
if (duration < kAnimationDuration)
lastAnimationStart -= (kAnimationDuration - duration);
// grab the state of the layer as it is right now
CALayer *currentLayer = self.subview.layer.presentationLayer;
// cancel any animations in progress
[self.subview.layer removeAllAnimations];
// set the transform to be as it is now, possibly in the middle of an animation
self.subview.layer.transform = currentLayer.transform;
// toggle our flag as to whether we're looking at large view or not
self.large = !self.large;
// set the transform based upon the state of the `large` boolean
CATransform3D newTransform;
if (self.large)
newTransform = CATransform3DMakeScale(2.0, 2.0, 1.0);
else
newTransform = CATransform3DIdentity;
// now animate to our new setting
[UIView animateWithDuration:duration
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.subview.layer.transform = newTransform;
}
completion:NULL];
}
There is a common trick you can use to do this, but it is necessary to write a separate method to shrink (and another similar one to expand):
- (void) shrink {
[UIView animateWithDuration:0.3
animations:^{
self.myView.transform = shrinkALittleBitMatrix;
}
completion:^(BOOL finished){
if (continueShrinking && size>0) {
size=size-1;
[self shrink];
}
}];
}
So now, the trick is to break the 3 seconds animation of shrinking into 10 animations (or more than 10, of course) of 0.3 sec each in which you shrink 1/10th of the whole animation: shrinkALittleBitMatrix. After each animation is finished you call the same method only when the bool ivar continueShrinking is true and when the int ivar size is positive (the view in full size would be size=10 and the view with minimum size would be size=0). When you press the button you change the ivar continueShrinking to FALSE, and then call expand. This will stop the animation in less than 0.3 seconds.
Well, you have to fill the details but I hope it helps.
First: how to remove or cancel a animation with view?
[view.layer removeAllAnimations]
if the view have many animations, such as, one animation is move from top to bottom, other is move from left to right;
you can cancel or remove a special animation like this:
[view.layer removeAnimationForKey:#"someKey"];
// the key is you assign when you create a animation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"someKey"];
when you do that, animation will stop, it will invoke it's delegate:
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
if flag == 1, indicate animation is completed.
if flag == 0, indicate animation is not completed, it maybe cancelled、removed.
Second: so , you can do what you want to do in this delegate method.
if you want get the view's frame when the remove code excute, you can do this:
currentFrame = view.layer.presentationlayer.frame;
Note:
when you get the current frame and remove animation , the view will also animate a period time, so currentFrame is not the last frame in the device screen.
I cann't resolve this question at now. if some day I can, I will update this question.
Related
Reverse a UIView Animation [duplicate]
I have a problem with the setting UIViewAnimationOptionAutoReverse. Here is my code. CALayer *aniLayer = act.viewToChange.layer; [UIView animateWithDuration:2.0 delay:1.0 options:(UIViewAnimationCurveLinear | UIViewAnimationOptionAutoreverse) animations:^{ viewToAnimate.frame = GCRectMake(1,1,100,100); [aniLayer setValue:[NSNumber numberWithFloat:degreesToRadians(34)] forKeyPath:#"transform.rotation"]; } completion:nil ]; The problem is that, after the animation has reversed, the view jumps back to the frame set in the animation block. I want the view to grow and "ungrow" and stop at its original position. Is there a solution without programming two consecutive animations?
You have three options. When you use the -[UIView animateWithDuration:…] methods, the changes you make in the animations block are applied immediately to the views in question. However, there is also an implicit CAAnimation applied to the view that animates from the old value to the new value. When a CAAnimation is active on a view, it changes the displayed view, but does not change the actual properties of the view. For example, if you do this: NSLog(#"old center: %#", NSStringFromCGPoint(someView.center)); [UIView animateWithDuration:2.0 animations: ^{ someView.center = newPoint; }]; NSLog(#"new center: %#", NSStringFromCGPoint(someView.center)); you will see that 'old center' and 'new center' are different; new center will immediately reflect the values of newPoint. However, the CAAnimation that was implicitly created will cause the view to still be displayed at the old center and smoothly move its way to the new center. When the animation finishes, it is removed from the view and you switch back to just seeing the actual model values. When you pass UIViewAnimationOptionAutoreverse, it affects the implicitly created CAAnimation, but does NOT affect the actual change you're making to the values. That is, if our example above had the UIViewAnimationOptionAutoreverse defined, then the implicitly created CAAnimation would animate from oldCenter to newCenter and back. The animation would then be removed, and we'd switch back to seeing the values we set… which is still at the new position. As I said, there are three ways to deal with this. The first is to add a completion block on the animation to reverse it, like so: First Option CGPoint oldCenter = someView.center; [UIView animateWithDuration:2.0 animations: ^{ someView.center = newPoint; } completion: ^(BOOL finished) { [UIView animateWithDuration:2.0 animations:^{ someView.center = oldCenter; }]; }]; Second Option The second option is to autoreverse the animation like you're doing, and set the view back to its original position in a completion block: CGPoint oldCenter = someView.center; [UIView animateWithDuration:2.0 delay:0 options: UIViewAnimationOptionAutoreverse animations: ^{ someView.center = newPoint; } completion: ^(BOOL finished) { someView.center = oldCenter; }]; However, this may cause flickering between the time that the animation autoreverse completes and when the completion block runs, so it's probably not your best choice. Third Option The last option is to simply create a CAAnimation directly. When you don't actually want to change the final value of the property you're changing, this is often simpler. #import <QuartzCore/QuartzCore.h> CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"]; animation.autoreverses = YES; animation.repeatCount = 1; // Play it just once, and then reverse it animation.toValue = [NSValue valueWithCGPoint:newPoint]; [someView.layer addAnimation:animation forKey:nil]; Note that the CAAnimation way of doing things never changes the actual values of the view; it just masks the actual values with an animation. The view still thinks it's in the original location. (This means, for example, that if your view responds to touch events, it will still be watching for those touch events to happen at the original location. The animation only changes the way the view draws; nothing else. The CAAnimation way also requires that you add it to the view's underlying CALayer. If this scares you, feel free to use the -[UIView animateWithDuration:…] methods instead. There's additional functionality available by using CAAnimation, but if it's something you're not familiar with, chaining UIView animations or resetting it in the completion block is perfectly acceptable. In fact, that's one of the main purposes of the completion block. So, there you go. Three different ways to reverse an animation and keep the original value. Enjoy!
Here's my solution. For 2x repeat, animate 1.5x and do the last 0.5x part by yourself: [UIView animateWithDuration:.3 delay:.0f options:(UIViewAnimationOptionRepeat| UIViewAnimationOptionAutoreverse) animations:^{ [UIView setAnimationRepeatCount:1.5f]; ... animate here ... } completion:^(BOOL finished) { [UIView animateWithDuration:.3 animations:^{ ... finish the animation here .... }]; }]; No flashing, works nice.
There is a repeatCount property on CAMediaTiming. I think you have to create an explicit CAAnimation object, and configure it properly. The repeatCount for grow and ungrow would be 2, but you can test this. Something a long the lines of this. You would need two CAAnimation objects though, one for the frame and one for the rotation. CABasicAnimation *theAnimation; theAnimation=[CABasicAnimation animationWithKeyPath:#"transform.rotation"]; theAnimation.duration=3.0; theAnimation.repeatCount=1; theAnimation.autoreverses=YES; theAnimation.fromValue=[NSNumber numberWithFloat:0.0]; theAnimation.toValue=[NSNumber numberWithFloat:degreesToRadians(34)]; [theLayer addAnimation:theAnimation forKey:#"animateRotation"]; AFAIK there is no way to avoid programming two animation objects.
Here is a Swift version of #Rudolf Adamkovič's answer, where you set the animation to run 1.5 times and in the completion block run a 'reverse' animation one final time. In the below snippet I am translating my view by -10.0 pts on the y-axis 1.5 times. Then in the completion block I animate my view one final time back to its original y-axis position of 0.0. UIView.animate(withDuration: 0.5, delay: 0.5, options: [.repeat, .autoreverse], animations: { UIView.setAnimationRepeatCount(1.5) self.myView.transform = CGAffineTransform(translationX: 0.0, y: -10.0) }, completion: { finished in UIView.animate(withDuration: 0.5, delay: 0, options: [], animations: { self.myView.transform = CGAffineTransform(translationX: 0.0, y: 0.0) }, completion: nil) })
iOS adding second animation to already animated UIView
I have a UIView called waves and it has a nice endless "floating" animation [UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionAutoreverse | UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveEaseInOut animations:^{ CGPoint center = waves.center; center.y += 5; waves.center = center; } completion:nil]; Now if I add another animation, say moving this view to a different location, "floating" animations stops. It's a reasonable reaction and it's no problem to start the "floating" again in the completion block. I was just wondering if I'm missing something, perhaps in animation Options, to combine the two in a way that doesn't interrupt one another. I was able to do so if the second animation is based on CGAffineTransfromScale, they combine no problem, but when I move the centre of the view it's not the case. UPDATE: found a bug in performance. I have a button that calls the method responsible for moving the center of my View with animation. If I press it too fast before previous animation completed View just snaps into new position without animation and completion block is not called. Here's the code for that method: - (void)wavesAnimationReversed:(BOOL)reversed { CGFloat y = waves.frame.size.height*0.25; y = reversed ? -y : y; // CGFloat damping = reversed ? 1 : 0.65; CGFloat damping = 1; [UIView animateWithDuration:kWAVES_ANIMATION_DURATION delay:0 usingSpringWithDamping:damping initialSpringVelocity:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:^{ CGPoint center = waves.center; center.y += y; waves.center = center; } completion:^(BOOL finished) { [self handleStartWavesFloating]; }]; }
If you want to perform multiple animations you should use animateKeyFrames. That being said you can't animate something that is being animated (your description matches perfectly what will happen). That is, because the values from your view are already changed (it's real values), the animation happens out of your control. Therefore, if you create a new animation, the default values are the end values of your first animation, when the 2nd animation triggers, it will automatically move the view to the new location to start the second animation.
UIView animation with CGAffineTransformMakeScale changes size instantly with decimal number
- (void)startAnimation { //reverse - shrinking from full size if (_reversed == YES) { //self.transform = CGAffineTransformMakeScale(1.0, 1.0); [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^{ self.transform = CGAffineTransformMakeScale(0.1, 0.1); //this line does it instantly self.alpha = 0; } completion:^(BOOL finished) { [self removeFromSuperview]; }]; } else { //non reverse - expanding from middle self.transform = CGAffineTransformMakeScale(0.001, 0.001); [UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionCurveLinear animations:^{ self.transform = CGAffineTransformMakeScale(1.0, 1.0); self.alpha = 0; } completion:^(BOOL finished) { [self removeFromSuperview]; }]; } } The non reverse piece of code does work fine as I expect, however when I do the _reversed == YES bit, the transformation inside the animation block happens instantly. If I comment that line of code, then the view stays the right size, but if i uncomment it then it shrinks instantly but the alpha still does the fade animation. Why does this happen? Edit: I figured out what happened but I don't know how to fix it. The view does do the animation, only the size of the view changes instantly but it still 'slides' into the centre as if it is shrinking (what you see is just a small rectangle sliding to the middle as if it's the top left corner of the object). If I scale the view to 2 first, then scale down to 1 the animation works fine, its only when going from 1 to a decimal number that it doesn't work. Also I used draw rect to create an object with core graphics and the transform problem affects that, but not the actual frame if a set background colour.
I was running into a similar issue, where the position of the view would suddenly jump before animating in a change of transform with CGAffineTransformMakeScale. I noticed that the size of the "jump" seemed to be proportional to the scaling that would occur later in the animation. I could fix this problem by finding that in a viewWillLayoutSubviews() override, I was setting the frame of the view being animated. As a rule, don't set the frames of views that will have a non-identity transform. So in the viewWillLayoutSubviews() override, I set the view's bounds and layer.position instead and now the animation is smooth as silk.
Pausing a UIView's key frame animations
I'm currently creating a loading bar that I would like to pause and reverse if a specific action is taken. I have the following code below that animates one view in another. How would I go about pausing this animation and applying another animation on the CURRENT frame at that point in time for the animated object. I would like to be able to animate this bar down to the 0 width mark at any given time within the current animation. -(void)animateProgressBar { CGRect endingFrame = self.frame; [UIView animateKeyframesWithDuration:self.time delay:0.0 options:0 animations:^{ [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:1.0 animations:^{ self.loadingBar.frame = endingFrame; }]; } completion:^(BOOL finished) { NSLog(#"TADA"); }]; }
If you want to start a new animation from where this one is now, just start the new animation using the the UIViewKeyframeAnimationOptionBeginFromCurrentState option (or if just doing animateWithDuration, use UIViewAnimationOptionBeginFromCurrentState).
You can import QuartzCore framework, add #import <QuartzCore/QuartzCore.h> And you can remove existing animation by calling: [self.loadingBar.layer removeAllAnimations]; After that you can start new animation. If you want to pause it you have to get reference to the current animation: CALayer *currentLayer = self.loadingBar.layer.presentationLayer; And you have to save it: self.loadingBar.layer.transform = currentLayer.transform; You can do it in another [UIView animateWith..... method.
CGAffineTransformIdentity not resetting a UIImageView after multiple transforms?
I have created a simple app with a segmented control at the top. When I click on one of two segments of the control a UIImageView starts to rotate. I have a reset button hooked up to set its transform to CGAffineTransformIdentity. The problem occurs when the method that does the view rotation animation is called a second time by switching segments back and forth. Hitting reset only removes the most recent animation. I have to switch segments a second time to get the animations to stop with reset entirely. The following code is called when I select the segment to rotate the UIImageView and obviously called a second time when I click between segments. // Begin the animation block and set its name [UIView beginAnimations:#"Rotate Animation" context:nil]; // Set the duration of the animation in seconds (floating point value) [UIView setAnimationDuration:0.5]; // Set the number of times the animation will repeat (NSIntegerMax setting would repeat indefinately) (floating point value) [UIView setAnimationRepeatCount:NSIntegerMax]; // Set the animation to auto-reverse (complete the animation in one direction and then do it backwards) [UIView setAnimationRepeatAutoreverses:YES]; // Animation curve dictates the speed over time of an animation (UIViewAnimationCurveEaseIn, UIViewAnimationCurveEaseOut, UIViewAnimationCurveEaseInOut, UIViewAnimationCurveLinear) [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut]; // Changes the imageViews transform property to rotate the view using CGAffineTransformRotate // CGAffineTransformRotate(transformToStartFrom, angleToRotateInRadians) // Starting transform property can be set to CGAffineTransformIdentity to start from views original transform state // This can also be done using CGAffineTransformMakeRotation(angleInRadians) to start from the IdentityTransform without implicitly stating so self.imageView.transform = CGAffineTransformRotate(self.imageView.transform, degreesToRadians(90)); [UIView commitAnimations]; The reset button calls this code - self.imageView.transform = CGAffineTransformIdentity;
try this [UIView animateWithDuration:0.2 animations:^() { self.imageView.transform = CGAffineTransformIdentity; }];