I want to create something like this, just consider single loop and how it completes a circle and reverse it on completion:
This piece of code does half of what I want:
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
drawAnimation.duration = 1;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circleLayer addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
I tried it to reverse but does not work:
[CATransaction begin];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
drawAnimation.duration = 1;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
//[circleLayer addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction setCompletionBlock:^{
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation2.duration = 1;
animation2.fromValue = [NSNumber numberWithFloat:0.0f];
animation2.toValue = [NSNumber numberWithFloat:1.0f];
animation2.removedOnCompletion = NO;
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circleLayer addAnimation:animation2 forKey:#"circelBack"];
}];
[circleLayer addAnimation:drawAnimation forKey:#"circleFront"];
[CATransaction commit];
The problem is I cannot reverse the animation.
The Problem
First of all, I suspect you've got the key paths of your animations round the wrong way. You should first be animating the stroke end from 0 to 1, and then the stroke start from 0 to 1.
Secondly, you're never updating the model layer with your new values - so when the animation is complete, the layer will 'snap back' to its original state. For you, this means that when the first animation is done - the strokeStart will snap back to 0.0 - therefore the reverse animation will look weird.
The Solution
To update the model layer values, you can simply set disableActions to YES in your CATransaction block to prevent an implicit animations from being generated on layer property changes (won't affect explicit animations). You'll then want to update the model layer's property after you add the animation to the layer.
Also, you can re-use CAAnimations – as they are copied when added to a layer. Therefore you can define the same animation for both the forward and reverse animation, just changing the key path.
If you're repeating the animation, you'll probably want to define your animation as an ivar - and simply update the key path before adding it.
For example, in your viewDidLoad:
#implementation ViewController {
CABasicAnimation* drawAnimation;
}
- (void)viewDidLoad {
[super viewDidLoad];
// define your animation
drawAnimation = [CABasicAnimation animation];
drawAnimation.duration = 1;
// use an NSNumber literal to make your code easier to read
drawAnimation.fromValue = #(0.0f);
drawAnimation.toValue = #(1.0f);
// your timing function
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// kick off the animation loop
[self animate];
}
You'll then want to create an animate method in order to perform one iteration of the animation. Unfortunately, since Core Animation doesn't support repeating multiple animations in a sequence, you'll have to use recursion to achieve your effect.
For example:
-(void) animate {
if (self.circleLayer.superlayer) { // check circle layer is the layer heirachy before attempting to animate
// begin your transaction
[CATransaction begin];
// prevent implicit animations from being generated
[CATransaction setDisableActions:YES];
// reset values
self.circleLayer.strokeEnd = 0.0;
self.circleLayer.strokeStart = 0.0;
// update key path of animation
drawAnimation.keyPath = #"strokeEnd";
// set your completion block of forward animation
[CATransaction setCompletionBlock:^{
// weak link to self to prevent a retain cycle
__weak typeof(self) weakSelf = self;
// begin new transaction
[CATransaction begin];
// prevent implicit animations from being generated
[CATransaction setDisableActions:YES];
// set completion block of backward animation to call animate (recursive)
[CATransaction setCompletionBlock:^{
[weakSelf animate];
}];
// re-use your drawAnimation, just changing the key path
drawAnimation.keyPath = #"strokeStart";
// add backward animation
[weakSelf.circleLayer addAnimation:drawAnimation forKey:#"circleBack"];
// update your layer to new stroke start value
weakSelf.circleLayer.strokeStart = 1.0;
// end transaction
[CATransaction commit];
}];
// add forward animation
[self.circleLayer addAnimation:drawAnimation forKey:#"circleFront"];
// update layer to new stroke end value
self.circleLayer.strokeEnd = 1.0;
[CATransaction commit];
}
}
To stop the animation, you can remove the layer from the superlayer - or implement your own boolean check for whether the animation should continue.
Full Project: https://github.com/hamishknight/Circle-Pie-Animation
Related
I programmed my own view containing an imageview which should be rotating. Here is my rotation animation:
- (void)startPropeller
{
//_movablePropeller = [[UIImageView alloc] initWithFrame:self.frame];
//_movablePropeller.image = [UIImage imageNamed:#"MovablePropeller"];
//[self addSubview:self.movablePropeller];
self.hidden = NO;
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotation.fromValue = [NSNumber numberWithFloat:0.0f];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.cumulative = true;
rotation.duration = 1.2f; // Speed
rotation.repeatCount = INFINITY; // Repeat forever. Can be a finite number.
[self.movablePropeller.layer removeAllAnimations];
[self.movablePropeller.layer addAnimation:rotation forKey:#"Spin"];
}
And here is how I start it:
self.loadingPropeller = [[FMLoadingPropeller alloc] initWithFrame:self.view.frame andStyle:LoadingPropellerStyleNoBackground];
self.loadingPropeller.center=self.view.center;
[self.view addSubview:self.loadingPropeller];
[self.loadingPropeller startPropeller];
Problem is: Without any further code. The propeller is not rotating. So I was able to solve it by adding this code into my class implementing to rotating propeller spinner:
-(void)viewDidAppear:(BOOL)animated
{
if(!self.loadingPropeller.hidden){
[self.loadingPropeller startPropeller];
}
}
But I don't like that too much. Isn't it possible to add some code within the Propeller class to solve this issue automatically, without having to add also code in every class in the viewDidAppear method?
The code that doesn't work does two essential things: adding the spinner to the view hierarchy and positioning it. My guess is that the failure is due to positioning it before layout has happened. Try this:
// in viewDidLoad of the containing vc...
self.loadingPropeller = [[FMLoadingPropeller alloc] initWithFrame:CGRectZero andStyle:LoadingPropellerStyleNoBackground];
[self.view addSubview:self.loadingPropeller];
// within or after viewDidLayoutSubviews...
// (make sure to call super for any of these hooks)
self.loadingPropeller.frame = self.view.bounds;
self.loadingPropeller.center = self.view.center;
// within or after viewDidAppear (as you have it)...
[self.loadingPropeller startPropeller];
I want to rotate the image around the x-axis from left to right. The problem is that when you rotate the image covers the button located on the top
Run animation
[AnimationUtil rotationRightToLeftForView:image andDuration:1];
Animation metod
+(void) rotationRightToLeftForView:(UIView *)flipView andDuration:(NSTimeInterval)duration{
// Remove existing animations before stating new animation
[flipView.layer removeAllAnimations];
// Make sure view is visible
flipView.hidden = NO;
// show 1/2 animation
//flipView.layer.doubleSided = NO;
// disable the view so it’s not doing anythign while animating
flipView.userInteractionEnabled = NO;
// Set the CALayer anchorPoint to the left edge and
// translate the button to account for the new
// anchorPoint. In case you want to reuse the animation
// for this button, we only do the translation and
// anchor point setting once.
if (flipView.layer.anchorPoint.x != 0.0f) {
flipView.layer.anchorPoint = CGPointMake(0.0f, 0.5f);
flipView.center = CGPointMake(flipView.center.x-flipView.bounds.size.width/2.0f, flipView.center.y);
}
// create an animation to hold the page turning
CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
transformAnimation.removedOnCompletion = NO;
transformAnimation.duration = duration;
transformAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// start the animation from the current state
transformAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
// this is the basic rotation by 180 degree along the y-axis M_PI
CATransform3D endTransform = CATransform3DMakeRotation(radians(180.0), 0.0f, -1.0f, 0.0f);
transformAnimation.toValue = [NSValue valueWithCATransform3D:endTransform];
// Create an animation group to hold the rotation
CAAnimationGroup *theGroup = [CAAnimationGroup animation];
// Set self as the delegate to receive notification when the animation finishes
theGroup.delegate = self;
theGroup.duration = duration;
// CAAnimation-objects support arbitrary Key-Value pairs, we add the UIView tag
// to identify the animation later when it finishes
[theGroup setValue:[NSNumber numberWithInt:flipView.tag] forKey:#"viewFlipTag"];
// Here you could add other animations to the array
theGroup.animations = [NSArray arrayWithObjects:transformAnimation, nil];
theGroup.removedOnCompletion = NO;
// Add the animation group to the layer
[flipView.layer addAnimation:theGroup forKey:#"flipView"];
}
The decision follows:
Create mask (image) Color = black, make region Size = Button.size Color = transparent
image name = "mask2.png"
button = ...;
ImageView = ...;
parent_view = ...;
UIImage *_maskingImage = [UIImage imageNamed:#"mask2"];
CALayer *_maskingLayer = [CALayer layer];
_maskingLayer.frame = parent_View.bounds;
[_maskingLayer setContents:(id)[_maskingImage CGImage]];
CALayer *layerForImage = [[CALayer alloc] init];
layerForImage.frame = parent_View.bounds;
[layerForImage setMask:_maskingLayer];
[layerForImage addSublayer:ImageView.layer];
[parent_View.layer addSublayer:layerForImage];
[parent_View addSubView:button];
this is a simple code form Brad Larson u-tunes course ;)
CABasicAnimation *move = [CABasicAnimation animationWithKeyPath:#"position"];
move.duration = 1.0f;
move.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
move.removedOnCompletion = NO;
move.fillMode = kCAFillModeForwards;
CGPoint currentPosition = l.position;
CGPoint newPosition = CGPointMake(currentPosition.x + 60.0f, currentPosition.y + 60.0f);
move.toValue = [NSValue valueWithCGPoint:newPosition];
[l addAnimation:move forKey:#"position"];
l.position = newPosition;
in the last row i change the position to reflect the final state of layer because animation does not.
But when i execute this code the animation isn't executed and layer move (in 1/4 of sec) to newposition.
someone can explain me how to animate layer's position correctly?
a second question...when i run this code...every subsequent access to property "position" will perform the same animation?
thanks.
Ditch the last line, the l.position = newPosition;. Your animation will already take care of that, and by using that property setter, you’re implicitly giving the layer Core Animation’s default .25-second action.
Also, no, subsequent changes in the position of your layer will not use your 1-second animation. The properties you’re using look pretty much identical to the default animation, though, aside from the duration; a quicker way to accomplish what you’re doing would be something like this.
CGPoint currentPosition = l.position;
CGPoint newPosition = CGPointMake(currentPosition.x + 60.0f, currentPosition.y + 60.0f);
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
l.position = newPosition;
[CATransaction commit];
The View Programming Guide for iOS tells us that block-based animations are the way forward, as opposed to the now almost deprecated begin/commit style animations:
Note: If you are writing an application for iOS 4 or later, you should use the block-based methods for animating your content instead. For information on how to use those methods, see “Starting Animations Using the Block-Based Methods.”
But now I'm in a situation where I need to use custom timing functions CAMediaTimingFunction so I've resorted to using CATransactions and CABasicAnimations. These classes uses the same semantical language as the deprecated UIView animations style with methods like [CATransaction begin] and [CATransaction commit]. It just feels odd in the middle of apps where everything else is block-based.
Is there a way to combine concepts like the CAMediaTimingFunctions with block-based animations?
Update 1:
A piece of example code that I would like to 'blockify' looks like this:*
[CATransaction begin];
{
[CATransaction setValue:[NSNumber numberWithFloat:3.0f] forKey:kCATransactionAnimationDuration];
CGPoint low = CGPointMake(0.150, 0.000);
CGPoint high = CGPointMake(0.500, 0.000);
[CATransaction begin];
{
CAMediaTimingFunction* perfectIn = [CAMediaTimingFunction functionWithControlPoints:low.x :low.y :1.0 - high.x :1.0 - high.y];
[CATransaction setAnimationTimingFunction: perfectIn];
CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeIn.fromValue = [NSNumber numberWithFloat:0];
fadeIn.toValue = [NSNumber numberWithFloat:1.0];
[viewB.layer addAnimation:fadeIn forKey:#"animateOpacity"];
}
[CATransaction commit];
}
[CATransaction commit];
Update 2
I've made an example project for another question of mine that contains the code above. It's on github.
But now I'm in a situation where I need to use custom timing functions CAMediaTimingFunction so I've resorted to using CATransactions and CABasicAnimations. These classes uses the same semantical language as the deprecated UIView animations style with methods like [CATransaction begin] and [CATransaction commit]. It just feels odd in the middle of apps where everything else is block-based.
I think you are misreading the documentation.
Block based animations are the way to do UIView animations. Period. Full stop.
This statement DOES NOT correspond to CoreAnimation. You still have to use begin/commit for CoreAnimation. Don't make the assumption that CA begin and commit are bad, just because a higher level construct (UIView) deprecated begin/commit.
Is there a way to combine concepts like the CAMediaTimingFunctions with block-based animations?
If you need the advanced capabilities of Core Anmiation, such as custom timings, you should use CoreAnimation the way it is intended (with begin/commit, etc.)
If you are trying to animate CALayers, use Core Animation.
If you are doing high-level UIView based animations, use the UIView block-based animations.
Now I'm going to go ahead and admit this looks pretty pointless but it's the quickest thing I could think of to get you a block interface and it does stop you form accidentally leaving off the being/commit
.h
+ (void)transactionWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations;
.m
+ (void)transactionWithDuration:(NSTimeInterval)duration
animations:(void (^)(void))animations;
{
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:duration] forKey:kCATransactionAnimationDuration];
animations();
[CATransaction commit];
}
Usage with your code (assuming you made it a category on UIView)
[UIView transactionWithDuration:3 animations:^{
CGPoint low = CGPointMake(0.150, 0.000);
CGPoint high = CGPointMake(0.500, 0.000);
CAMediaTimingFunction* perfectIn =
[CAMediaTimingFunction functionWithControlPoints:low.x
:low.y
:1.0 - high.x
:1.0 - high.y];
[CATransaction setAnimationTimingFunction: perfectIn];
CABasicAnimation *fadeIn = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeIn.fromValue = [NSNumber numberWithFloat:0];
fadeIn.toValue = [NSNumber numberWithFloat:1.0];
[viewB.layer addAnimation:fadeIn forKey:#"animateOpacity"];
}];
I have a NSWindow containing a NSView with 'Wants Core Animation Layer' enabled. The view then contains many NSImageView that use are initially animated into position. When I run the animation, it is extremely sluggish and drops most of the frames. However, if I disable 'Wants Core Animation Layer' the animation works perfectly. I'm going to need the core animation layer but can't figure out how to get it to perform adequately.
Can I do anything to fix the performance issues?
Here is the code:
// AppDelegate
NSRect origin = ...;
NSTimeInterval d = 0.0;
for (id view in views)
{
[view performSelector:#selector(animateFrom:) withObject:origin afterDelay:d];
d += 0.05f;
}
// NSImageView+Animations
- (void)animateFrom:(NSRect)origin
{
NSRect original = self.frame;
[self setFrame:origin];
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.20f];
[[self animator] setFrame:original];
[NSAnimationContext endGrouping];
}
It's possible that the NSTimer is killing your performance. Core Animation has rich support for controlling the timing of animations through the CAMediaTiming protocol, and you should take advantage of that in your app. Instead of using the animator proxy and NSAnimationContext, try using Core Animation directly. If you create a CABasicAnimation for each image and set its beginTime, it will delay the start of the animation. Also, for the delay to work the way you want, you must wrap each animation in a CAAnimationGroup with its duration set to the total time of the entire animation.
Using the frame property could also be contributing to the slowdown. I really like to take advantage of the transform property on CALayer in situations like this where you're doing an "opening" animation. You can lay out your images in IB (or in code) at their final positions, and right before the window becomes visible, modify their transforms to the animation's starting position. Then, you just reset all of the transforms to CATransform3DIdentity to get the interface into its normal state.
I have an example in my <plug type="shameless"> upcoming Core Animation book </plug> that's very similar to what you're trying to do. It animates 30 NSImageViews simultaneously with no dropped frames. I modified the example for you and put it up on github. These are the most relevant bits of code with the extraneous UI stuff stripped out:
Transform the layers to their start position
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// ... SNIP ... //
//Start with all of the images at the origin
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (CALayer *imageLayer in [[[self imageContainer] layer] sublayers]) {
CGPoint layerPosition = [layer position];
CATransform3D originTransform = CATransform3DMakeTranslation(20.f - layerPosition.x, -layerPosition.y, 0.f);
[imageLayer setTransform:originTransform];
}
[CATransaction commit];
}
Animate the transform back to the identity
- (IBAction)runAnimation:(id)sender {
CALayer *containerLayer = [[self imageContainer] layer];
NSTimeInterval delay = 0.f;
NSTimeInterval delayStep = .05f;
NSTimeInterval singleDuration = [[self durationStepper] doubleValue];
NSTimeInterval fullDuration = singleDuration + (delayStep * [[containerLayer sublayers] count]);
for (CALayer *imageLayer in [containerLayer sublayers]) {
CATransform3D currentTransform = [[imageLayer presentationLayer] transform];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.beginTime = delay;
anim.fromValue = [NSValue valueWithCATransform3D:currentTransform];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.fillMode = kCAFillModeBackwards;
anim.duration = singleDuration;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:anim];
group.duration = fullDuration;
[imageLayer setTransform:CATransform3DIdentity];
[imageLayer addAnimation:group forKey:#"transform"];
delay += delayStep;
}
}
I also have a video on YouTube of the example in action if you want to check it out.
Did you try to batch everything in a CATransaction?
[CATransaction begin];
for {...}
[CATransaction commit];
CATransaction is the Core Animation mechanism for batching multiple layer-tree operations into atomic updates to the render tree.