问题
I am trying to perform this basic UIView
animation upon receiving a button click:
- (IBAction)buttonPress:(id)sender
{
self.sampleView.alpha = 0.0;
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState
animations:^{self.sampleView.alpha = 1.0;}
completion:NULL];
}
Prior to the button click the view is visible and has an alpha value of 1.0. When I send the button action I expect the view to fade in from alpha=0.0 to alpha=1.0 over 2 seconds but this does not happen. When I remove the UIViewAnimationOptionBeginFromCurrentState
the animation works fine.
It seems like with the UIViewAnimationOptionBeginFromCurrentState
option set, the alpha=0.0 is not being set and the animation is skipped because it thinks it is already at 1.0.
I am trying to understand why this would be happening since Apple documentation states that the UIViewAnimationOptionBeginFromCurrentState
has no effect if another animation is not in progress:
"UIViewAnimationOptionBeginFromCurrentState
Start the animation from the current setting associated with an already in-flight animation. If this key is not present, any in-flight animations are allowed to finish before the new animation is started. If another animation is not in flight, this key has no effect."
回答1:
It turns out that using UIViewAnimationOptionBeginFromCurrentState
doesn't always work as expected.
Look at this example:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
UIButton *theButton = [UIButton new];
[self.view addSubview:theButton];
theButton.frame = self.view.frame;
theButton.backgroundColor = [UIColor redColor];
theButton.alpha = 0.05;
[theButton addTarget:self action:@selector(actionPressed:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)actionPressed:(UIButton *)theButton
{
theButton.alpha = 0.6;
[UIView animateWithDuration:5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
// you expect to animate from 0.6 to 1. but it animates from 0.05 to 1
theButton.alpha = 1;
}
completion:nil];
}
In the example above you expect that .alpha
would animate from 0.6 to 1. However, it animates from 0.05 to 1.
In order to resolve the issue, you should change actionPressed:
to the following:
- (void)actionPressed:(UIButton *)theButton
{
[UIView animateWithDuration:0
animations:^
{
// set new values INSIDE of this block, so that changes are
// captured in UIViewAnimationOptionBeginFromCurrentState.
theButton.alpha = 0.6;
}
completion:^(BOOL finished)
{
[UIView animateWithDuration:5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
// now it really animates from 0.6 to 1
theButton.alpha = 1;
}
completion:nil];
}];
}
Mention the animateWithDuration:0
!!!
The rule is easy: Only use an animation block with UIViewAnimationOptionBeginFromCurrentState
AFTER some other animation block, so that all your previous changes are actually applied.
In case you don't know what exactly should be included in animateWithDuration:0
block, then you can use this trick:
- (void)actionPressed:(UIButton *)theButton
{
// make all your changes outside of the animation block
theButton.alpha = 0.6;
// create a fake view and add some animation to it.
UIView *theFakeView = [UIView new];
theFakeView.alpha = 1;
[UIView animateWithDuration:0
animations:^
{
// we need this line so that all previous changes are ACTUALLY applied
theFakeView.alpha = 0;
}
completion:^(BOOL finished)
{
[UIView animateWithDuration:5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
// now it really animates from 0.6 to 1
theButton.alpha = 1;
}
completion:nil];
}];
}
If you don't want to remember about all details of this bug, then just apply Method Swizzling to UIView class.
IMPORTANT EDIT: It turns out that the code below will cause a crash at runtime, if calling performBatchUpdates:completion:
of UICollectionView
instance. So, I do not recommend using method swizzling
in this case!
Your code may look like this:
+ (void)load
{
static dispatch_once_t theOnceToken;
dispatch_once(&theOnceToken, ^
{
Class theClass = object_getClass(self);
SEL theOriginalSelector = @selector(animateWithDuration:delay:options:animations:completion:);
SEL theSwizzledSelector = @selector(swizzled_animateWithDuration:delay:options:animations:completion:);
Method theOriginalMethod = class_getClassMethod(theClass, theOriginalSelector);
Method theSwizzledMethod = class_getClassMethod(theClass, theSwizzledSelector);
if (!theClass ||!theOriginalSelector || !theSwizzledSelector || !theOriginalMethod || !theSwizzledMethod)
{
abort();
}
BOOL didAddMethod = class_addMethod(theClass,
theOriginalSelector,
method_getImplementation(theSwizzledMethod),
method_getTypeEncoding(theSwizzledMethod));
if (didAddMethod)
{
class_replaceMethod(theClass,
theSwizzledSelector,
method_getImplementation(theOriginalMethod),
method_getTypeEncoding(theOriginalMethod));
}
else
{
method_exchangeImplementations(theOriginalMethod, theSwizzledMethod);
}
});
}
+ (void)swizzled_animateWithDuration:(NSTimeInterval)duration
delay:(NSTimeInterval)delay
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL))completion
{
if (options & UIViewAnimationOptionBeginFromCurrentState)
{
UIView *theView = [UIView new];
theView.alpha = 1;
[UIView animateWithDuration:0
animations:^
{
theView.alpha = 0;
}
completion:^(BOOL finished)
{
[self swizzled_animateWithDuration:duration
delay:delay
options:options
animations:animations
completion:completion];
}];
}
else
{
[self swizzled_animateWithDuration:duration
delay:delay
options:options
animations:animations
completion:completion];
}
}
If you add this code to your custom UIView category, then this code would work fine now:
- (void)actionPressed:(UIButton *)theButton
{
theButton.alpha = 0.6;
[UIView animateWithDuration:5
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^
{
// you expect to animate from 0.6 to 1.
// it will do so ONLY if you add the above code sample to your project.
theButton.alpha = 1;
}
completion:nil];
}
In my case, this bug has completely ruined my animations. See below:
[
] []回答2:
In order to see what's going on, it's helpful to animate the frame position rather than the alpha. If you do this, try tapping the button every second or so with UIViewAnimationOptionBeginFromCurrentState
set and you will start to see some movement.
When you set the alpha to zero, this doesn't update the display until the next pass through the run loop. Since you start your animation immediately afterward, the display doesn't get a chance to update and when the animation framework starts, it looks at the current displayed alpha (1.0) and looks at where it has to get to (alpha of 1.0) and the interpolation between the two does nothing.
If you need to keep the UIViewAnimationOptionBeginFromCurrentState
option in there because this button might be tapped when an animation on this view is already in progress, you can do this:
- (IBAction)buttonPressed:(id)sender
{
self.sampleView.alpha = 0.0;
[self performSelector:@selector(animateAlpha) withObject:nil afterDelay:0.1];
}
- (void)animateAlpha
{
[UIView animateWithDuration:2.0
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionBeginFromCurrentState
animations:^{self.sampleView.alpha = 1.0;}
completion:NULL];
}
回答3:
When using UIViewAnimationOptionBeginFromCurrentState
, instead of performSelector:
I like to set initial values with UIView
's performWithoutAnimation right before calling animateWithDuration:
.
来源:https://stackoverflow.com/questions/21495970/uiviewanimationoptionbeginfromcurrentstate-unexpected-behaviour-with-basic-anima