Why the pause between animateWithDuration animatio

2019-06-18 07:33发布

问题:

Following Apple's recommendations, I'm chaining UIView animations by putting subsequent calls to -animationWithDuration:animation: in the completion: block of another call to aanimateWithDuration:animation:completion:, like so:

[UIView animateWithDuration:scaleDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
    // Scale the controllers' views down.
    self.view.transform = CGAffineTransformScale(self.view.transform, 0.8, 0.8);
} completion:^(BOOL finished) {
    // Transition to the new view and push on the new view controller.
    [UIView transitionWithView:self.view duration:1 options:UIViewAnimationOptionCurveLinear | UIViewAnimationOptionTransitionFlipFromLeft animations:^{
        [self pushViewController:viewController animated:NO];
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:scaleDuration delay:0 options:UIViewAnimationOptionCurveLinear animations:
^{
            // Scale back to the original size.
            self.view.transform = CGAffineTransformScale(self.view.transform, 1.25, 1.25);
        } completion:nil];
    }];
}];

The animations all execute the right order, but there is a tiny delay between them, especially before the -transitionWithView:duration:options:animations:completion: call. How do I smooth out the transitions between animation steps?

回答1:

Aside:
Is there any particular reason why you are abusing a navigation controller in this way? Couldn’t you just use presentViewController:animated:completion: setting its transition style to UIModalTransitionStyleFlipHorizontal?

Back to your question:

I’m pretty much sure that the stutter comes from the simple fact that pushViewController:animated: implicitly has to load the view of the view controller that is to be pushed and that this takes some time.

So if you cannot use the presentViewController:animated:completion: (or presentModalViewController:animated: if you have to support iOS 4) approach, I’d encourage you to try this:

// ensure the view is already loaded when you invoke `pushViewController:animated:`
[viewController view];

// there is no strict need to calculate those in situ, so we do it up front
CGAffineTransform originalTransform = self.view.transform;
CGAffineTransform downscalingTransform = CGAffineTransformScale(originalTransform, 0.8, 0.8);

// I found it hard to read the block-in-a-block-in-a... while editing here, so I've taken them apart:
void (^scaleDownAnimation)() = ^{
    self.view.transform = downscalingTransform;
};

void (^restoreScaleAnimation)() = ^{
    self.view.transform = originalTransform;
};

void (^pushControllerAnimation)() = ^{
    [self pushViewController:viewController animated:NO];
};

void (^pushAnimationCompletion)(BOOL) = ^(BOOL unused) {
    [UIView animateWithDuration:scaleDuration
                          delay:0
                        options:UIViewAnimationOptionCurveLinear
                     animations:restoreScaleAnimation
                     completion:nil];
};

void (^downscaleCompletion)(BOOL) = ^(BOOL unused){
    UIViewAnimationOptions linearFlipFromLeft = UIViewAnimationOptionCurveLinear | UIViewAnimationOptionTransitionFlipFromLeft;
    [UIView transitionWithView:self.view
                      duration:1
                       options:linearFlipFromLeft
                    animations:pushControllerAnimation
                    completion:pushAnimationCompletion];
};

[UIView animateWithDuration:scaleDuration
                      delay:0
                    options:UIViewAnimationOptionCurveEaseIn
                 animations:scaleDown
                 completion:downscaleCompletion];

Note that the beef is within the first six lines, all the rest is just for completeness.



回答2:

I've chained animations as you describe with no delay.

You are using transitionWithView:duration:options:animations:completion: with the animation block being a call to pushViewController:animated:. That doesn't make any sense to me.

transitionWithView:duration:options:animations:completion transitions from one subview to the next.

pushViewController:animated is a navigation controller method that pushes a view controller on the navigation controller stack. Using those 2 together makes no sense to me.

If you want your scale animation to end with a push, then just call the push animation directly from your completion block.



回答3:

In my experience, the "use the completion block to chain animations" is simply flawed (makes small pauses between sub-animations). I made sure there's no loadView/viewDidLoad/viewWillAppear/viewWillLayoutSubviews abuse going on.

The exact same code stutters with "completion-block_chainig" but works just fine if I rework it to use keyframes.

So, instead of (for example)...

        UIView.animate(withDuration: 1, animations: {[weak self] in
            ...
            self?.view.layoutIfNeeded()
        }, completion: { [weak self] _ in
            UIView.animate(withDuration: 1, animations: {
                ...
                self?.view.layoutIfNeeded()
            }, completion: { [weak self] _ in
                UIView.animate(withDuration: 1, animations: {
                    ...
                    self?.view.layoutIfNeeded()
                }, completion: { [weak self] _ in
                    ...
                })
            })
        })

... I instead prefer...

        UIView.animateKeyframes(withDuration: 3, delay: 0, options: .calculationModeLinear, animations: {[weak self] in

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

            UIView.addKeyframe(withRelativeStartTime: 0.33, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

            UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.33, animations: {[weak self] in
                ...
                self?.view.layoutIfNeeded()
            })

        }, completion: { [weak self] _ in
            ...
        })