I will start off by explaining that I have seen many questions and answers regarding this type of feature, but I am still having problems implementing it myself. I am using ARC, and am not using auto-layout or storyboard. I define my layouts with constraints in code, so the way I have been trying to implement my animation is a little different. Lastly, this is an iPad application.
To the specific problem at hand, I have a subview that starts off hidden but appears when an action takes place. I would like this subview to use the hidden feature, but slide in and out after it appears and before it is hidden. So far, I have gotten halfway there and am able to get the view to slide in without issue. Below is the code that accomplishes this.
detailView.hidden = NO;
// Perform Animation - Slide In
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
However, I have been unsuccessful with trying getting the view to slide out before it is hidden. Below is the code that I added to attempt at completing this feature.
// Perform Animation - Slide Out
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
detailView.hidden = YES;
The result I get is that the view simply disappears like it was hidden, which it always did. Do I need to remove one animation that is added to a view before I add a different animation? Or is my CATransform3DMakeTranslation incorrectly defined?
Turns out detailView.hidden was being called before the animation started. I resolved this by adding a selector with a delay that contained a method to hide my view.
[self performSelector:#selector(hideDetailView) withObject:nil afterDelay:.40];
Related
I have a UIImageView that when the user taps it, a border of 4 points toggles on and off. I'm trying to animate the border in and out as follows:
CABasicAnimation *widthAnimation = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
widthAnimation.toValue = self.isSelected ? #4.0 : #0.0;
widthAnimation.duration = 0.1;
[self.imageView.layer addAnimation:widthAnimation forKey:#"borderWidth"];
Now, as I've learned from research and scouring SO, CABasicAnimation just changes the presentation layer, but not the actual model. I've also read that using fillMode and removedOnCompletion is bad practice, since it leads to inconsistencies between the model and what the user sees. So, I tried to change the model with the following line:
self.imageView.layer.borderWidth = self.isSelected ? 4.0 : 0.0;
The problem is, this line seems to set the property straight away, so by the time the animation kicks in, the border width is already at it's desired value. I've tried sticking this line at the beginning of the code, end, and everywhere in between, but to no success. I did manage to find a hacky solution: instead of setting the property, I passed the property setter to performSelector: withObject: afterDelay:, with the delay being the duration of the animation. This works most of the time, but sometimes the cycles don't quite match up, and the animation will run first, then it jumps back to the original state, then it snaps to the new state, presumably as a result of performSelector
So is there any way to smoothly animate a border without performSelector?
Any help is greatly appreciated.
Here is an example of CABasicAnimation I made a while ago :
-(void) animateProgressFrom:(CGFloat)fromValue to:(CGFloat)toValue
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = #(fromValue);
animation.toValue = #(toValue);
animation.duration = ABS(toValue - fromValue)*3.0;
[self.layer addAnimation:animation forKey:#"opacity"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.layer.opacity = toValue;
[CATransaction commit];
}
I think what you needed is the CATransaction at the end of the layer animation.
I have an iOS app which is using a CABasicAnimation on repeat:
CABasicAnimation *fadeAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.2];
fadeAnim.duration = 1.0;
fadeAnim.autoreverses = YES;
fadeAnim.repeatCount = INFINITY;
[colourbutton.titleLabel.layer addAnimation:fadeAnim forKey:#"opacity"];
I have a button which when pressed is meant to stop the animation.
-(IBAction)stopAnim {
[colourbutton.titleLabel.layer removeAllAnimations];
}
It works fine but one thing I am noticing is that is stops the animation suddenly, it doesn't let the animation finish. So how can I get it to finish the current animation and then stop. (Or in other words how can I get it to removeAllAnimations....withAnimation?).
On a side note, do I need to include CoreAnimation framework for this to work. So far the animation is running and I havn't imported the CoreAnimation framework.
Thanks, Dan.
Just add another animation and after that remove the first one like this:
CABasicAnimation *endAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
endAnimation.fromValue = #(((CALayer *)colourbutton.titleLabel.layer.presentationLayer).opacity);
endAnimation.toValue = #(1);
endAnimation.duration = 1.0;
[colourbutton.titleLabel.layer addAnimation:endAnimation forKey:#"end"];
[colourbutton.titleLabel.layer removeAnimationForKey:#"opacity"];
The key here is to use the presentation layer to get the current state. Don't forget to set the actual end state of the layer, because the animation will be removed on completion.
In NKorotov's answer, he uses the presentationLayer to find out where you are in the animation. That is the correct way to go.
You could go with this solution, although IMO you would also have to calculate the duration animation correctly (based on the duration of the original animation and on how far you are along the animation path currently).
If you find it "silly" to add a new animation, you could perhaps call removeAllAnimations using dispatch_after at the correct time.
I have a simple animation setup in a Mac App where a page control swaps out some views. I am very close to getting the paging-like animation I want, but there is a slight problem. The paging works and the new page animates into place correctly, but the new page also replaces the initial page before the animation. I wish to ultimately have the view that is being pushed out remain the same instead of changing to the new page early. Here is a video showing the problem, slowed down to a 3 second animation duration. Here is the code:
UASourceView *previousSourceView = [self.sourceViewContainer.subviews objectAtIndex:0];
NSInteger previousIndex = [self.sourceViews indexOfObjectIdenticalTo:previousSourceView];
CATransition *pushTransition = [CATransition animation];
[pushTransition setType:kCATransitionPush];
[pushTransition setSubtype:(previousIndex < index) ? kCATransitionFromRight : kCATransitionFromLeft];
[pushTransition setDuration:PANEL_PAGE_SWIPE_ANIMATION_DURATION];
[pushTransition setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[pushTransition setRemovedOnCompletion:YES];
[CATransaction begin];
[self.sourceViewContainer replaceSubview:previousSourceView with:sourceView];
[CATransaction commit];
[sourceView.layer addAnimation:pushTransition forKey:#"push"];
There is a fixed container view self.sourceViewContainer that gets a new subview replaced on every animation. The problem, as seen above is that the previousSourceView gets replaced immediately by the sourceView and gets pushed in as well. Please help me stop the immediate replacement. Where am I going wrong?
*Note, iOS tag is added because this code is platform independent.
I solved similar issues using CAAnimationBlocks (this is my fork of the original, which the author has moved in a direction I didn't much care for).
The idea is to set removedOnCompletion to NO and fillMode to kCAFillModeForwards and perform the actual switch (without animation) in the completion block. This has been tested under OSX only (you might need a different solution for iOS, which already supports completion blocks).
Here's an example usage from my code, which moves a piece on a chess board, and back again:
CABasicAnimation *moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
moveAnimation.fromValue = [NSValue valueWithPoint:[self _pointForSquare:move.from()]];
moveAnimation.toValue = [NSValue valueWithPoint:[self _pointForSquare:move.to()]];
moveAnimation.duration = _animateSpeed / 1000.0;
moveAnimation.autoreverses = YES;
moveAnimation.removedOnCompletion = NO;
moveAnimation.fillMode = kCAFillModeForwards;
moveAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
moveAnimation.completion = ^(BOOL finished)
{
[pieceLayer removeAnimationForKey:#"movePiece"];
[self _setPieceLayer:pieceLayer toPiece:piece];
[pieceLayer setNeedsDisplay];
};
[pieceLayer addAnimation:moveAnimation forKey:#"movePiece"];
I am creating some animation on my application and the code below zooms out an object till it disappears. I can't figure out how to make the object to disappear and keep that way, ie. how to make the animation stay put after it finishes. Any gotchas on that? Cheers!
CABasicAnimation* zoomOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
zoomOut.duration = 1;
zoomOut.toValue = [NSNumber numberWithFloat:0];
[draggedObject addAnimation:zoomOut forKey:nil];
I found it. It also needs the two methods below:
zoomOut.removedOnCompletion = NO;
zoomOut.fillMode = kCAFillModeForwards;
Ok so this happens because the animation doesn't actually change the underlying property, which is why it jumps back after the animation is complete.
Try adding this line before the line starting the animation -
zoomOut.removedOnCompletion = NO;
I have a problem I don't understand regarding UIViews and Core Animation. Here's what I want to do:
A small view is presented above a bigger view by putting it as one of its subviews.
When I click a button inside this view, the view should minimize and move to a specified CGRect.
Then the view is removed from its superview.
The first time I present, minimize-and-move and remove the view, everything works fine. But when I present the view again, it displays at the modified position (even though it's supposed to be set at the original position by a call to theView.frame = CGRectMake(600.0, 160.0, 339.0, 327.0);), while all the different responder elements (buttons, textviews, etc.) contained in the view act as if they were at the original position. It's like the view and the layer gets dissynchronized by the animation, and I do not know how to get them back in sync.
Having something like self.view.layer.frame = CGRectMake(600.0, 160.0, 339.0, 327.0); does not get anything right.
My minimize-and-move animation code is given below:
[CATransaction flush];
CABasicAnimation *scale, *translateX, *translateY;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.delegate = delegate;
group.duration = duration;
scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
translateX = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
translateY = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
scale.toValue = [NSNumber numberWithFloat:0.13];
translateX.toValue = [NSNumber numberWithFloat:137.0];
translateY.toValue = [NSNumber numberWithFloat:-290.0];
group.animations = [NSArray arrayWithObjects: scale, translateX, translateY, nil];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[theView.layer addAnimation:group forKey:#"MyAnimation"];
How to get the layer back to the view after the animation?
What happens if you remove these two lines?
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
What you are telling core animation with those lines is that you want it to continue to display in the forward (final) state of the animation. Meanwhile, you didn't actually set the transform on the layer to have the properties you used for the animation. This would make things appear to be out of sync.
Now, the issue you're going to run into is that removing those lines will cause your transforms to revert back to the starting state when the animation has completed. What you need to do is actually set the transforms on the layer in order for them to hold their position when the animation completes.
Another option is to leave the two lines in and then actually explicitly remove the animation from the layer instead of setting the layer frame as you mentioned when you are ready to revert back to the original state. You do this with:
[theView.layer removeAnimationForKey:#"MyAnimation"];
The -removedOnCompletion property told the layer not to remove the animation when it finished. Now you can explicitly remove it and it should revert back.
HTH.