Unexpected behaviour with UIViewAnimationOptionBeginFromCurrentState - ios

I'm trying to implement multistage animation using UIViewAnimationOptionBeginFromCurrentState to allow the user to cancel the animation at will. The animation is a view that continually and cyclically animates between two sizes. When the user touches the view to cancel the animation, I want the view to quickly revert back to its original, small size, whether it was growing or shrinking at the time.
I'm implementing the multistaging aspect by having two separate animations, one for growing the view and one for shrinking it. Each calls the other routine in its completion block, thus cycling forever unless the abort flag has been set.
I get the expected behaviour if abort is called during the grow-the-view animation: the animation quickly and immediately returns the view to its original, small size and stops. Good!
However, if abort is called during the shrink-the-view animation cycle, the view continues to shrink at the same speed (and then stops as expected), as if the UIViewAnimationOptionBeginFromCurrentState option was never invoked.
Code will hopefully make this clearer and hopefully somebody can see what I can't.
- (void)stopAnimating {
abort = YES;
[UIView animateWithDuration:.2 // some small interval
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{ self.frame = minRect;}
completion:^(BOOL done){}
];
}
- (void)animateSmall {
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{self.frame = minRect;}
completion:^(BOOL done){if (!abort)[self animateBig];}
];
}
- (void)animateBig {
[UIView animateWithDuration:4
delay:0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{self.frame = maxRect;}
completion:^(BOOL done){if (!abort)[self animateSmall];}
];
}

Just a guess here, because your code looks exactly as I would have done it. But I think what's going on is that the abort animation is setting the same attribute to the same value as the animation it's interrupting, and this gets treated as equivalent and not in need of change (even though the duration changes).
A test of this theory - and a fix to the problem - would be to make your oscillating minRect just a little bit different in size than your steady state minRect.
Hope this works. Good luck.

Related

How to force a view animation to finish before executing sendSubviewToBack?

I am building a page browser that animates pages as 'sheets of paper' being pulled on top of off a stack of papers. In order to keep smooth animations I use 3 UIViews which are stacked on top of each other. These three views hold the current page (on top), the previous page (in the middle) and the next page (at the bottom).
In the code below, I want to drag the top view off to the right, revealing the previous page underneath. This works fine. However, after that I need to move the top page to the bottom of the stack, in order to prepare the stack of three views for the next time the user does a page flip. I use the sendSubviewToBack method for this.
My problem is that ViewSample[Top] is sent to the bottom of the stack as soon as the animation starts. How can I enforce the animation to finish (so that ViewSample[Top] has moved out of the screen completely) before it is sent to the bottom of the stack?
ViewSample[Top].center = CGPointMake(x,y);
[UIView animateWithDuration:0.5
animations:^
{
ViewSample[Top].center = CGPointMake(x+w,y); //slide away to the right
}
completion:^(BOOL finished)
{
}
];
[self.MainView sendSubviewToBack:ViewSample[Top]];
EDIT
i just ran into a very peculiar behaviour which has to do with my problem.
I followed your advice, and found that the behaviour in the 'finished' section of the animation depends on the value of the variable 'top' when it is set AFTERWARDS:
ViewSample[Top].center = CGPointMake(x,y);
[UIView animateWithDuration:0.5
animations:^
{
ViewSample[Top].center = CGPointMake(x+w,y); //slide away to the right
}
completion:^(BOOL finished)
{ [self.MainView sendSubviewToBack:ViewSample[Top]];
}
];
Top++; // THIS COMMAND AFFECTS THE LINE ABOVE!!!
In other words, when I add the line 'Top++;' another View is moved back on the stack, even though the statement sendSubviewToBack came first. This is very confusing to me. Does this make sense? Is it a bug?
The other answers correctly identified the issue. What you're running into with your updated code is a problem of execution order:
Because completion is executed only after the animation completes, your code actually executes in this order:
ViewSample[Top].center = CGPointMake(x,y);
ViewSample[Top].center = CGPointMake(x+w,y);
Top++;
[self.MainView sendSubviewToBack:ViewSample[Top]];
There are two possible solutions. You can either store the view in a variable so you have the same view in all your calls, or you can delay setting the value of Top until completion.
Option 1
UIView *viewMovingFromTopToBottom = ViewSample[Top];
viewMovingFromTopToBottom.center = CGPointMake(x,y);
[UIView animateWithDuration:0.5 animations:^{
viewMovingFromTopToBottom.center = CGPointMake(x+w,y); //slide away to the right
} completion:^(BOOL finished) {
[self.MainView sendSubviewToBack:viewMovingFromTopToBottom];
}];
Top++;
// Other code that depends on the new value of Top...
Option 2
ViewSample[Top].center = CGPointMake(x,y);
[UIView animateWithDuration:0.5 animations:^{
ViewSample[Top].center = CGPointMake(x+w,y); //slide away to the right
} completion:^(BOOL finished) {
[self.MainView sendSubviewToBack:ViewSample[Top]];
Top++;
// Other code that depends on the new value of Top...
}];
Which option makes the most sense to you depends on what you're doing. If you're chaining animations together, you may want to move most of your code into the completion block to delay it until the slide animation completes. If you have a lot of work that needs to be done right away without dependencies on animation, you may want to use option 1 to configure you animations and move on. Or you may want a mix.
Use the completion block:
ViewSample[Top].center = CGPointMake(x,y);
[UIView animateWithDuration:0.5
animations:^
{
ViewSample[Top].center = CGPointMake(x+w,y); //slide away to the right
}
completion:^(BOOL finished)
{
[self.MainView sendSubviewToBack:ViewSample[Top]];
}
];
Brian Nickel's first suggestion (local variable) did the trick. However, there was a caveat: you have to be careful in the order of statements. This does not work:
[self.MainView addSubview:LocalView];
LocalView = View[1];
...whereas this does:
LocalView = View[1];
[self.MainView addSubview:LocalView];
I first had the top version, which just makes the blank view appear.
So a working approach is to use three global views to do the page caching, and use two local views for the animation. The local views are stacked in the appropriate order, copy the data from the global views and then perform the animation.

How to properly animate UIScrollView contentOffset

I have UIScrollView subclass. Its content is reusable - about 4 or 5 views are used to display hundreds of elements (while scrolling hidden objects reused and jumps to another position when its needed to see them)
What i need: ability to automatically scroll my scroll view to any position. For example my scroll view displays 4th, 5th and 6th element and when I tap some button it needs to scroll to 30th element. In other words I need standard behaviour of UIScrollView.
This works fine:
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
but I need some customisation. For example, change animation duration, add some code to perform on end of animation.
Obvious decision:
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
} completion:^(BOOL finished) {
//some code
}];
but I have some actions connected to scroll event, and so now all of them are in animation block and it causes all subview's frames to animate too (thanks to few reusable elements all of them animates not how i want)
The question is: How can I make custom animation (in fact I need custom duration, actions on end and BeginFromCurrentState option) for content offset WITHOUT animating all the code, connected to scrollViewDidScroll event?
UPD:
Thanks to Andrew's answer(first part) I solved issue with animation inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[UIView performWithoutAnimation:^{
[self refreshTiles];
}];
}
But scrollViewDidScroll must (for my purposes) executes every frame of animation like it was in case of
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
However, now it executes only once at start of animation.
How can I solve this?
Did you try the same approach, but with disabled animation in scrollViewDidScroll ?
On iOS 7, you could try wrapping your code in scrollViewDidScroll in
[UIView performWithoutAnimation:^{
//Your code here
}];
on previous iOS versions, you could try:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Your code here
[CATransaction commit];
Update:
Unfortunately that's where you hit the tough part of the whole thing. setContentOffset: calls the delegate just once, it's equivalent to setContentOffset:animated:NO, which again calls it just once.
setContentOffset:animated:YES calls the delegate as the animation changes the bounds of the scrollview and you want that, but you don't want the provided animation, so the only way around this that I can come up with is to gradually change the contentOffset of the scrollview, so that the animation system doesn't just jump to the final value, as is the case at the moment.
To do that you can look at keyframe animations, like so for iOS 7:
[UIView animateKeyframesWithDuration:duration delay:delay options:options animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(floorf(index/2) * elementWidth, 0)];
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
}];
} completion:^(BOOL finished) {
//Completion Block
}];
This will get you two updates and of course you could use some math and a loop to add up a lot more of these with the appropriate timings.
On previous iOS versions, you'll have to drop to CoreAnimation for keyframe animations, but it's basically the same thing with a bit different syntax.
Method 2:
You can try polling the presentationLayer of the scrollview for any changes with a timer that you start at the beginning of the animation, since unfortunately the presentationLayer's properties aren't KVO observable. Or you can use needsDisplayForKey in a subclass of the layer to get notified when the bounds change, but that'll require some work to set up and it does cause redrawing, which might affect performance.
Method 3:
Would be to dissect exactly what happens to the scrollView when animated is YES try and intercept the animation that gets set on the scrollview and change its parameters, but since this would be the most hacky, breakable due to Apple's changes and trickiest method, I won't go into it.
A nice way to do this is with the AnimationEngine library. It's a very small library: six files, with three more if you want damped spring behavior.
Behind the scenes it uses a CADisplayLink to run your animation block once every frame. You get a clean block-based syntax that's easy to use, and a bunch of interpolation and easing functions that save you time.
To animate contentOffset:
startOffset = scrollView.contentOffset;
endOffset = ..
// Constant speed looks good...
const CGFloat kTimelineAnimationSpeed = 300;
CGFloat timelineAnimationDuration = fabs(deltaToDesiredX) / kTimelineAnimationSpeed;
[INTUAnimationEngine animateWithDuration:timelineAnimationDuration
delay:0
easing:INTULinear
animations:^(CGFloat progress) {
self.videoTimelineView.contentOffset =
INTUInterpolateCGPoint(startOffset, endOffset, progress);
}
completion:^(BOOL finished) {
autoscrollEnabled = YES;
}];
Try this:
UIView.animate(withDuration: 0.6, animations: {
self.view.collectionView.contentOffset = newOffset
self.view.layoutIfNeeded()
}, completion: nil)

Smooth frame animating on UIView

I am trying to move a UIView around the screen by incrementing the UIView's x property in an animation block. I want the element to move continuously so I cannot just specify an ending x and up the duration.
This code works but it is very choppy. Looks great in the simulator but choppy on the device.
-(void)moveGreyDocumentRight:(UIImageView*)greyFolderView
{
[UIView animateWithDuration:0.05 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
NSInteger newX = greyFolderView.frame.origin.x + 5.0;
greyFolderView.frame = CGRectMake(newX, greyFolderView.frame.origin.y, greyFolderView.frame.size.width, greyFolderView.frame.size.height);
}
} completion:^(BOOL finished) {
[self moveGreyDocumentRight:greyFolderView];
}];
}
You're fighting the view animation here. Each one of your animations includes a UIViewAnimationOptionCurveEaseInOut timing curve. That means that every 0.05 seconds you try to ramp up your speed then slow down your speed then change to somewhere else.
The first and simplest solution is likely to change to a linear timing by passing the option UIViewAnimationOptionCurveLinear.
That said, making a new animation every 5ms really fights the point of Core Animation, complicating the code and hurting performance. Send the frame it to the place you currently want it to go. Whenever you want it to go somewhere else (even if it's still animating), send it to the new place passing the option UIViewAnimationOptionBeginFromCurrentState. It will automatically adjust to the new target. If you want it to repeat the animation or bounce back and forth, use the repeating options (UIViewAnimationOptionRepeat and UIViewAnimationOptionAutoreverse).

Choppy UIView animation

I use a UIView animation to randomly animate 5 squares (UIButtons) around the screen. Depending on a user selection, there are anywhere from 2 to 5 squares visible. When only 2 are visible, the other three's hidden values get set to YES, so they are actually still animating (right?), they just aren't visible. But when only 2 are visible, the animation is smooth, but when all five are visible, the animation gets choppy. I'm not really sure how to describe it, because the squares are still moving at the correct speed and moving to the correct points; the choppiness isn't terrible, just bad enough to be noticeable. Is there any way to get rid of it? This is the code I use to animate the squares:
Edit: changed animations to block:
[UIView animateWithDuration:animationSpeed
delay:0
options:UIViewAnimationOptionCurveLinear
animations:^{
view.center = destPoint;
}
completion:^(BOOL finished){
if([view isEqual:squareThree])
[self moveBadGuys];
}
];
/*for(UIButton* button in squareArray) {
if(!shouldMove)
return;
[UIView beginAnimations:#"b" context:nil];
[UIView setAnimationDuration:animationSpeed];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
view.center = destPoint;
[UIView commitAnimations];
}*/
Edit: the view presenting this is the third in a stack of three UIViewController presented with
ViewController* controller = [[[ViewController alloc] init] autorelease];
[self presentModalViewController:controller animated:NO];
Does this way of presenting views eat up memory?
There are a few things that can cause this. It always comes down to how complex the content is. Also, simulator can be really bad about handling animation, so be sure you are testing on real hardware.
Are there large images on the buttons? Are the buttons casting shadows? Those things can slow it down.
Also- use block based animation. Not the old begin-commit methods.
Not exactly sure why it's slow, but have you tried nesting the thing differently?
if(!shouldMove)
return;
[UIView beginAnimations:#"b" context:nil];
[UIView setAnimationDuration:animationSpeed];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
for(UIButton* button in squareArray) {
view.center = destPoint;
}
[UIView commitAnimations];
does (almost - the logic is a bit different in the !shouldMove case, but that's a different story) the same, but in a cleaner way.

Having problems allowing interaction in UIView animation

I have the following block of code to fade out an introView(UIView)
// Hide intro view after 5 seconds
[UIView animateWithDuration: 1.0
delay: 5.0
options: (UIViewAnimationOptionAllowUserInteraction |UIViewAnimationOptionCurveLinear)
animations: ^{
introView.alpha = 0;
}
completion: ^(BOOL finished) {
[introView removeFromSuperview];
}];
I have a skip button inside the introVew but there is no interaction whatsoever, am I missing something? I have to add this is a Universal app targeting 3.2 and I'm using XCode 4.2
Pretty sure this is impossible pre-4.0:
UIView userInteractionEnabled Docs
During an animation, user interactions are temporarily disabled for
all views involved in the animation, regardless of the value in this
property. You can disable this behavior by specifying the
UIViewAnimationOptionAllowUserInteraction option when configuring the
animation.
There seems little point in targeting 3.2 in an app you haven’t released yet.
Are you setting your button alpha to 0?
If yes here is an interesting thing about animation.
What you see on the screen during the animation is not what the application sees.
The moment you set your alpha to 0, the alpha is 0 for that view, even if you are still seeing it on the screen.
Also, a view that has an alpha lower that 0.05 (don't recall the exact number) won't get touch event.
What you can do is to implement the - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event of that view's superview. or the touchesEnded... as you like.
(Assuming that your not setting it's alpha to 0.)
So you can test for touche that occur where the button is, or just remove that button and let any touch on the screen cancel your animation.
You may also be interested in this post:
Core Animation, unexpected animated position and hitTest values
I found another circumstance which could cause this. I haven't seen this answer anywhere else. It does not deal with alpha at all.
If you use a delay in the call to UIView.animate(), then even if you specify the .allowUserInteraction option, the view does NOT receive touches during the delay period. I have no idea why, but I could help it by moving the code block to another function, and using a performSelector after the same delay seconds, and in the block I run the code without delay.
I had the same problem with a button that I animated with changing the alpha. Cueing off VinceBurn's answer...
What you see on the screen during the animation is not what the application sees. The moment >you set your alpha to 0, the alpha is 0 for that view, even if you are still seeing it on the >screen.
AND view that have an alpha lower that 0.05 (don't recall the exact number) won't get touch >event.
… the simple solution of just making the minimum alpha 0.1 instead of 0.0 worked for me:
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse | UIViewAnimationOptionAllowUserInteraction
animations:^{
self.myButton.alpha = 0.1f;
}
completion:^(BOOL finished){
}]
Button registered the touchUpInside all the time with no additional method needed, and there was virtually no difference in appearance from taking the alpha to zero.
This won't work in iOS 3.2 since Blocks are only available in iOS4
you will have to use the standard animation techniques, in a separate thread so that you don't block the interface
[UIView beginAnimations:nil context:nil];
[UIView setAnimationTransition: UIViewAnimationTransitionFlipFromLeft forView:view cache:YES];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:1.0];
[view1 setHidden:TRUE];
[UIView commitAnimations];

Resources