Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stop and reverse a UIView animation?

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;
        }];
    }
}
like image 770
Mischa Avatar asked Apr 30 '13 14:04

Mischa


People also ask

Does UIView animate need weak self?

No, it is not needed in this case. animations and completion are not retained by self so there is no risk of strong retain cycle.

Does UIView animate run on the main thread?

The contents of your block are performed on the main thread regardless of where you call [UIView animateWithDuration:animations:] .

How do you animate a view in Swift?

To be exact, whenever you want to animate the view, you actually call layoutIfNeeded on the superview of that view. Try this instead: UIView. animate(withDuration: 0.1, delay: 0.1, options: UIViewAnimationOptions.


2 Answers

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:

  1. 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.)

  2. Import the necessary header if you haven't already (again, not needed in contemporary versions of Xcode):

    #import <QuartzCore/QuartzCore.h>
    
  3. Have your code stop the existing animation:

    [self.subview.layer removeAllAnimations];
    
  4. 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;
    
  5. Reset the transform (or frame or whatever) according to the current value in the presentationLayer:

    self.subview.layer.transform = currentLayer.transform;
    
  6. 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];
}
like image 149
Rob Avatar answered Sep 28 '22 16:09

Rob


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.

like image 36
Roberto Canogar Avatar answered Sep 28 '22 16:09

Roberto Canogar