ios transform.rotation.y breaks up window - objective-c

I have an odd problem in one of my chain animations. I have block object for each of the blocks on my screen (26 of them), and when the user presses them I perform a flip. It works great, but I'm adding a scale animation before I do these to make them about 50% bigger on the screen. So, my sequence is:
enlarge
delay (let user see the large image)
spin-out 90% (removes block from view) - using transform.rotation.y
change image & spin-in
shrink.
I have setup the window view controller to be a delegate of these blocks, such that it can pass a counter to the blocks so I can position the right sublayer on the top (using setZposition).
It all works great, except when I have 2 blocks on positioned above/below each other on the screen, such that the enlarge will cause them to overlap, and then when the spin-out animation starts, it immediately has the right side of the block pop behind the block below it. I've tried changing the animation to transform.rotation.x and get the same behavior when the blocks are side-to-side.
I'm not sure if it's an iOS bug of if I'm just not doing something correct. Any suggestions are greatly appreciated. Here is the spin-out method:
- (void)spinOut:(id)sender
{
NSTimeInterval animationTime=0.85;
[UIView animateWithDuration:animationTime
delay:0
options: UIViewAnimationOptionCurveLinear
animations:^{
// setup the animation to spin the current view out
[CATransaction begin];
[CATransaction setDisableActions:YES];
CABasicAnimation *spinOut = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[spinOut setDelegate:self];
[spinOut setDuration:animationTime];
CAMediaTimingFunction *tf = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[spinOut setTimingFunction:tf];
[spinOut setFromValue:[NSNumber numberWithFloat:M_PI * 0.0]];
[spinOut setToValue:[NSNumber numberWithFloat:M_PI * 0.5]];
[spinOut setRemovedOnCompletion:NO];
[spinOut setFillMode:kCAFillModeForwards];
// setup variables used to roll in the next view on animation completion.
[pageView.layer addAnimation:spinOut forKey:kMyVeryOwnABCsSpinOutKey];
[CATransaction commit];
}
completion:^(BOOL finished){
[self setFlipOutAnimationTimer:[NSTimer scheduledTimerWithTimeInterval:animationTime target:self selector:#selector(spinIn:) userInfo:nil repeats:NO]];
}];
}

OK - this is an old question, but I kindof figured out this problem. Since I was flipping these images around the Y axis, I was able to change the animated image's Zposition. In my startAnimation method, I added:
float pageLevelLayerPosition;
// logic to set the zposition variable 100 more than anything nearby
_originalImageViewZPosition = bigImageView.layer.zPosition;
[bigImageView.layer setZPosition:pageLevelLayerPosition];
and then when in the animationDidStop, I added the relevant line of code to reset it back to 0 where I was removing the animation. I had some code to manage the variable so that I was assured that the number was greater than anything nearby.
I guess if I ever get into 3-D animation, then I'll need to control this much more closely, but that is what was causing my problem.

Related

Custom animation when switching from one UICollectionViewLayout to another?

As a test I made one layout that displays cells in a vertical line and another that displays them in a horizontal layout. When I call [collectionView setCollectionViewLayout:layout animated:YES]; it animates between the two positions very cleanly.
Now that I'd like to do is have all the views do a few spins, warps and flips around the screen (probably using CAKeyframeAnimations) before finally arriving at their destination, but I can't find a good place to hook this in.
I tried subclassing UICollectionViewLayoutAttributes to contain an animation property, then setting those animations in an overridden applyLayoutAttributes: method of the UICollectionViewCell I'm using. This works... EXCEPT it appears to happen only after the layout transition is complete. If I wanted to use this, I'd have to have the layout not change the current positions of the objects right away, only after the it reaches this apply attributes part of the code, and that seems like a lot of work...
Or I could subclass UICollectionView and override setCollectionViewLayout:animated:, but that also seems like a lot of state to keep around between layouts. Neither of these optins seems right, because there's such an easy way to animate additions/deletions of cells within a layout. I feel like there should be something similar for hooking into the animations between layouts.
Does anyone know the best way to get what I'm looking to accomplish?
#define degreesToRadians(x) (M_PI * (x) / 180.0)
UICollectionView *collectionView = self.viewController.collectionView;
HorizontalCollectionViewLayout *horizontalLayout = [HorizontalCollectionViewLayout new];
NSTimeInterval duration = 2;
[collectionView.visibleCells enumerateObjectsUsingBlock:^(UICollectionViewCell *cell, NSUInteger index, BOOL *stop)
{
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotationAnimation.toValue = #(degreesToRadians(360));
rotationAnimation.duration = duration;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[cell.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}];
[UIView animateWithDuration:duration
animations:^
{
collectionView.collectionViewLayout = horizontalLayout;
}];

animationDidStop in ios

I'm generating CALayer when user taps the screen. Then I'm translating that layer to certain position using Animation. Then I'm removing it using this code in animationDidStop:
[mylayer removeFromSuperLayer];
Here everything is working fine, but when I tap again before the previous animation stops, my current layer is not removed from the superlayer. How do I removed it under these circumstances?
If you are creating a new layer each time, then the delegate method will only be able to remove the current one (i.e. the older one will be lost)
You could try using CATransaction begin/commit pairs around your animations and adding completion block, this way you can pass the layers reference for each animation
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[myLayer removeFromSuperlayer];
}];
//your existing animation code
[CATransaction commit];

Animating label (NSTextField) horizontally

I have a NSTextField as a label, showing a string.
I want to animate this label from right to left, if the content of it is too large to be displayed at once.
I've done this with an NSTimer so far, it works, but it's just not a very good solution.
The labels are displayed in an NSTextFieldCell, in a Table View.
They often get out of sync, and I guess it's just eating up a lot of CPU/GPU resources.
Is there another way with Core Animation to do this?
I have tried it with layers, as you can see right here:
CALayer and drawRect
but I didn't get it working either.
I would really appreciate your help.
You can simply animate the position of NSTextField with animator like
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
you can also embed it in "CATrancation" code like this:
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
[CATransaction commit];
if you need animation delegate, you can use CABasicAnimation
CABasicAnimation* animation = [CABasicAnimation animation];
animation.delegate = self;
NSDictionary *animations = [NSDictionary dictionaryWithObjectsAndKeys:animation,#"frameOrigin",nil];
[textField setAnimations:animations];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
Delegate methods are
- (void)animationDidStart:(CAAnimation *)theAnimation;
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag;
If you need to mask your text field, just embed it in other NSView.
First, animate the label using one of the functions offered in the other answers.
Then, if you want to display another view on the sides without overlapping, you can:
Insert the label in a subview with the limits you wish
Use bringSubviewtoFront: or sendSubviewToBack: to make sure your label stays in the back

removeAllAnimations not working on successively called animations

I'm trying to set up an image gallery type view where the image is nearly full screen, and the nav controller, toolbar, buttons (to move between images), and slider (to quickly move between images) all fade out after periods without interaction, and then return on a tap. What I have so far (which I'm sure isn't even close to the right way to do this, I'm something of a beginner) is this:
-(void)fadeOutViews{
[self fadeOutView:rightButton];
[self fadeOutView:leftButton];
[self fadeOutView:mySlider];
mySlider.enabled = NO;
[self fadeOutView:myToolbar];
[self fadeOutView:self.navigationController.navigationBar];
}
-(void)fadeOutView:(UIView *)view{
view.alpha = 1;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationCurve:UIViewAnimationCurveEaseIn];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:2];
view.alpha = 0;
[UIView commitAnimations];
}
-(void)stopFadeOut{
[rightButton.layer removeAllAnimations];
[leftButton.layer removeAllAnimations];
[mySlider.layer removeAllAnimations];
[myToolbar.layer removeAllAnimations];
[self.navigationController.navigationBar.layer removeAllAnimations];
}
-(void)resetToInitialConfigurationWithDelay:(int)delay{
if (buttonWasPressed){
delay = 4;
buttonWasPressed = NO;
}
rightButton.alpha = 1;
leftButton.alpha = 1;
mySlider.alpha = 1;
myToolbar.alpha = 1;
self.navigationController.navigationBar.alpha = 1;
mySlider.enabled = YES;
[self stopFadeOut];
[self performSelector:#selector(fadeOutViews) withObject:nil afterDelay:delay];
}
So, the theory is, you reset to the initial state (the delay is because images fade in and out when using the buttons to advance, so there needs to be more time in before fading after a button press, or else the fading started immediately after the new image loaded. This resets everything to how it started out, and begins the process of fading again. And stopFadeOut removes all the animations if something occurs that should stop the fading process. So, for example, if a tap occurs:
- (IBAction)tapOccurs:(id)sender {
[self stopFadeOut];
[self resetToInitialConfigurationWithDelay:2];
}
Any previous animations are stopped, and then the process is restarted. Or at least that's the theory. In practice, if, say, there are several taps in quick succession, the faded views will start to fade briefly, and the reset, over and over again, so that it looks like they are flashing, until they finally fade out completely. I thought that perhaps the issue was the the animations were delayed, but the removeAllAnimation calls were not, so I replaced
[self stopFadeOut];
with
[self performSelector:#selector(stopFadeOut) withObject:nil afterDelay:2];
but the results were the same. The behavior is EXACTLY the same if stopFadeOut is never called, so the only conclusion I can draw is that for whatever reason, the removeAllAnimations calls aren't working. Any ideas?
What is happening
It sounds to me like the reset method is called multiple times before the previous run finished. You could easily verify this by adding log-statements to both the tap and reset method and count the number of logs for each method and watch what happens when you tap multiple times in a row.
I've tried to illustrate the problem with drawing below.
T = tapOccurs:
O = fadeOutViews:
--- = wait between T & O
Normal single tap
T-----O T-----O
---------------------> time
Multiple taps in a row
T-----O
T-----O
T-----O
---------------------> time
What it sound like you are trying to do
T-
T---
T-----O
---------------------> time
Every time fadeOutViews: (called O in the illustration) gets called the view will fade out. Looking at your fadeOutView: implementation this means that the opacity will jump to 1 and then fade slowly to 0, thus it looks like they are flashing an equal number of times to the number of taps until finally starting over.
How you can prevent this
You could do a number of things to stop this from happening. One thing would be to cancel all the scheduled reset methods by calling something like cancelPerformSelectorsWithTarget: or cancelPreviousPerformRequestsWithTarget:selector:object:.

CALayer fade from current value

My app uses CALayer to draw views. More precisely, it uses the drawLayer:inContext: method on a sublayer of a UIView's top layer. This is a nice way to get the 'implicit' animation of consecutive drawLayer:inContext: drawings to fade into each other over time. The fading animations happen fairly fast, maybe in 0.25 seconds, but to change its duration, simply implement another delegate method called actionForLayer:forKey:. In this perfectly working example implementation here the default duration is stretched to 2.0 seconds:
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:event];
animation.duration = 2.0;
return animation;
// or return nil for the default duration.
}
On to the issue at hand.
If you call [sublayer setNeedsDisplay] faster than the fades have time to complete, with each new fade you'll see a sudden jump. From the looks of it, the fade that's in progress is cancelled and it's final state is used as the starting point of the new fade. This might not be very surprising, but the visual result is rather unwanted.
Consider the scenario of a ten second fade from black to white, with another fade, to black, triggered five seconds after the start. The animation will start fading from black to white, but when it's at a 'half way gray' it jumps to full white before fading to black again.
Is there a way to prevent this from happening? Can I get the layer to fade from the gray back down to black? Is there a CALayer drawing equivalent of saying UIViewAnimationOptionBeginFromCurrentState (used in UIView animations)?
Cheers.
A layer's animation is only a visual representation of what the layer should look like as it animates. In CA when you animate from one state to another, the entire state of the layer changes immediately. A presentation layer is created and displays the animation, and when the animation completes the actual layer is left in place at the end.
So, my guess is that when you want to transition from one state to another, and the current animation hasn't completed yet, you have to capture the current state of the animation and then use this as the starting point for your next animation.
The problem lies in not being able to modify a layer's current animation.
In the following post I capture the current state of an animation, set that as the current state for the layer and use that as the beginning value from which to animate. The post applies this technique to the speed / duration of an animation, but can also be applied to your scenario.
https://stackoverflow.com/a/9544674/1218605
I'm a little stumped on this one too.
Did you forget to specify the fillMode kCAFillModeForwards. There's more info about that in the reference docs.
For example, I got this to work without any snapping, although I'm not changing the duration.
#implementation FadingLayer
- (void)fadeOut {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animation.fillMode = kCAFillModeForwards;
animation.fromValue = (id)[UIColor redColor].CGColor;
animation.toValue = (id)[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.0].CGColor;
animation.removedOnCompletion = FALSE;
animation.delegate = self;
[self addAnimation:animation
forKey:#"test"];
}
- (void)fadeIn {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
animation.fillMode = kCAFillModeForwards;
animation.fromValue = (id)[UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.0].CGColor;
animation.toValue = (id)[UIColor redColor].CGColor;
animation.removedOnCompletion = FALSE;
animation.delegate = self;
[self addAnimation:animation
forKey:#"test"];
}
#end
You'll probably want to animate a custom property however.
Hope this helps :/
I wanted to accomplish the same thing with a zoom animation of a layer tree. I have a zoom in/out key-equivalent where the user can zoom the layer tree accordingly. However, if the user presses the zoom key-equivalent in rapid succession, there would be a temporary snap-back to the values prior to the onset of the animation, since the previous animation hadn't yet completed.
At the end of the animation code, performing a sole [CATransaction commit] forced any pending transactions to be committed to the layer model before the start of the next animation, and solved the problem.
The documentation says:
+ commit
Commit all changes made during the current transaction.
Declaration
+ (void)commit
Special Considerations
Raises an exception if no current transaction exists.
However, testing this with many [CATransaction commit] messages in succession doesn't actually raise an exception. I've used this same technique to squelch warnings of the form:
CoreAnimation: warning, deleted thread with uncommitted
CATransaction;
in an NSOperation whose thread of execution finishes before layer animations do. It could be that Apple changed this behaviour in recent OS releases to a no-op (which would be much saner) if no current transaction exists, without updating the documentation.