I have a CircleView : UIView class that animates (shrinks and grows) when you touch it (via the pop function). I would also like for a stroked circle to appear and grow out (like a basic water ripple) when you touch it too. (The effect I am talking about is shown here in CSS)
How do I go about doing this?
Some more information:
Ideally both animations would be controllable from the single CircleView class
CircleView inherits off UIView
Update
Based on answers I have added a new object to via a subLayer. This displays ok BUT it doesn't animate during the pop. Can anyone help me understand why?
Here is my current code (the pertinent bits anyway)
- (void)layoutSubviews
{
[self setLayerProperties];
}
- (void)setLayerProperties {
//The view’s Core Animation layer used for rendering.
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:self.frame.size];
layer.path = bezierPath.CGPath;
layer.fillColor = _Color.CGColor;
rippleLayer = [[CALayer alloc] init];
layer.path = bezierPath.CGPath;
layer.strokeColor = [UIColor blueColor].CGColor;
[layer addSublayer:rippleLayer];
}
// Does the animated "pop"
- (void)pop{
CABasicAnimation *animation = [self animationWithKeyPath:#"transform.scale"];
[animation setFromValue:[NSNumber numberWithFloat:1.0f]];
[animation setToValue:[NSNumber numberWithFloat:0.8f]];
[animation setRemovedOnCompletion:YES];
[animation setDuration:0.15];
[animation setAutoreverses:YES];
[self.layer addAnimation:animation forKey:animation.keyPath];
rippleLayer.anchorPoint = CGPointMake(1, 1);
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
[scale setFromValue:[NSNumber numberWithFloat:1.0f]];
[scale setToValue:[NSNumber numberWithFloat:2.0f]];
[scale setRepeatCount:1];
[scale setDuration:1.0f];
//r[scale setRemovedOnCompletion:YES];
[scale setFillMode:kCAFillModeForwards];
[rippleLayer addAnimation:scale forKey:scale.keyPath];
}
Just add the other objects you want to animate to your layer and set their alpha to 0.0. You can then play with that value in or immediately before your animation.
I noticed that you did not follow the convention of naming variables with small initial. _color would be preferable to _Color.
Also, if you want to put the layer into its original state you usually use the identify transform matrix:
self.transform = CGAffineTransformIdentity;
Related
When animating a CAShapeLayer, is it necessary to constantly keep updating the path of the layer, or is there a way to rely on CABasicAnimation alone?
In the example below, I set up four circles paths and draw them.
Then I want to animate them down to disappear. After the animation has completed however, they spring back to their original paths.
int radius = halfWidth-30; //the radius is the distance out from the centre
trackActive.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
circleActive.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
radius = halfWidth - 10;
trackNew.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
circleNew.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(circleEnd) clockwise:true].CGPath;
NSArray * layers = #[trackActive, circleActive, trackNew, circleNew];
for (CAShapeLayer * layer in layers){
[CATransaction begin];
CABasicAnimation * animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 1.0f;
animation.removedOnCompletion = false;
animation.fromValue = #(1.0);
animation.toValue = #(0.0);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[layer addAnimation:animation forKey:#"circleAnimation"];
[CATransaction commit];
}
I understand that I can add this to set the new path:
[CATransaction setCompletionBlock:^{
//set the new path
layer.path = [UIBezierPath bezierPathWithArcCenter:cicleCenter radius:radius startAngle:degreesToRadians(circleStart) endAngle:degreesToRadians(arcEnd) clockwise:true].CGPath;
}];
But would prefer avoid keep having to update the path, and instead rely on the animation layer exclusively. Is this possible?
Something like: layer.path = animation.path or animation.updatesOriginalPath=true; could be wishful thinking, not sure.
You can simplify your code and omit the spring effect:
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction: kCAMediaTimingFunctionEaseInEaseOut];
layer.strokeEnd = 0.0;
[CATransaction commit];
The transaction will create and add an implicit animation object for you.
You can set a flag in a an animation so it stays where it was in the end and starts where it should start like this:
[animation setRemovedOnCompletion:NO];
[animation setFillMode:kCAFillModeBoth];
check also this answer:
What exactly does removedOnCompletion = NO do?
I'd like to implement transition animation between two UIView according to the material design as shown on screenshot below (first part of animation only):
But I'd like to make it more realistic with applying animated mask to UIView. Here is a code I've developed:
- (IBAction)onButtonTapped:(UIButton *)sender
{
// 1. make a hole in self.view to make visible another view
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRect:self.view.bounds];
[maskPath appendPath:[UIBezierPath bezierPathWithArcCenter:sender.center
radius:sender.frame.size.width/2.0f
startAngle:0.0f
endAngle:2.0f*M_PI
clockwise:NO]];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.fillColor = [UIColor blackColor].CGColor;
maskLayer.path = maskPath.CGPath;
// 2. add scale animation for resizing hole
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = [NSValue valueWithCGRect:sender.frame];
animation.toValue = [NSValue valueWithCGRect:self.view.bounds];
animation.duration = 3.0f;
[maskLayer addAnimation:animation forKey:#"scaleAnimation"];
self.view.layer.mask = maskLayer;
}
My idea is adding a "hole" into modal UIView and then scale wit animation.
The problem is that it doesn't work properly. First part of code make a hole well. But scaling animation of a hole doesn't work at all.
You can't add an animation to a layer if the layer isn't in some window's layer tree. You need to do things in this order:
self.view.layer.mask = maskLayer;
[CATransaction flush];
[maskLayer addAnimation:animation forKey:#"scaleAnimation"];
I am trying to animate the the appearance and then the disappearance of a UIBezierPath in a CAShapeLayer.
How I would like to do it is that at start, the path would be invisible by setting strokeStart and strokeEnd to 0. Then, in my animate method I would do a CABasicAnimation with setting the strokeEnd to 1, and after this is done, another animation where I set the strokeStart to 1, so the path would disappear again. Here is what I've tried:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = _duration / 2.0;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = _duration / 2.0;
disappearingAnimation.duration = _duration / 2.0;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = _duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
But the effect I am seeing is that the appearingAnimation is working properly, but as soon as it is done, the path instantly disappears without any animation. After this (_duration/2.0 time later) the whole thing starts over, so it would appear that the disappearingAnimation has no effect.
I tried another approach, but that only worked if the path was on my view.layer. As soon as I added it to a sublayer and tried to animate that one, the second (disappearing) animation had a glitch in it. Here is my second approach:
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
layer.strokeStart = 0;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
layer.strokeStart = 1;
layer.strokeEnd = 1;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self animate];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation1"];
[CATransaction commit];
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration / 2.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:1];
[layer addAnimation:animation forKey:#"animation"];
[CATransaction commit];
}
Any idea why the first solution doesn't work at all, or why the second is only working properly on the view.layer and not on a sublayer?
When animating a layer property, you will want to set the layer property to the end value before adding the animation, as the animation changes are only applied to the presentation, not the model layer. Once the animation is completed it will be removed, leaving the layer in the state it was before.
Both the first and second methods you have outlined worked for me once a layer had been created (missing from your snippet):
- (void)animate
{
CAShapeLayer *layer = [CAShapeLayer layer];
CGFloat _duration = 2.0;
CGFloat radius = self.view.frame.size.width/2.0;
CGFloat startAngle = -(M_PI_2);
CGFloat endAngle = (3*M_PI_2);
CGPoint center = CGPointMake(self.view.frame.size.width/2.0, self.view.frame.size.height/2.0);
layer.path = [[UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES] CGPath];
layer.fillColor = [[UIColor clearColor] CGColor];
layer.strokeColor = [[UIColor blackColor] CGColor];
[self.view.layer addSublayer:layer];
... {animation code}
}
I think it is the problem of the value of _duration / 2.0. It may be the problem of floating number calculation.
Try not to halve the total duration but to make the durations of appearingAnimation and disappearingAnimation as the base duration:
#define DURATION_BASE 1
// You can choose the DURATION_BASE value whaterver you like
- (void)animate
{
CAShapeLayer *layer = (CAShapeLayer *)self.layer;
CABasicAnimation *appearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
appearingAnimation.duration = DURATION_BASE;
appearingAnimation.fromValue = #0;
appearingAnimation.toValue = #1;
CABasicAnimation *disappearingAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
disappearingAnimation.beginTime = appearingAnimation.duration;
disappearingAnimation.duration = DURATION_BASE;
disappearingAnimation.fromValue = #0;
disappearingAnimation.toValue = #1;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = appearingAnimation.duration+disappearingAnimation.duration;
animationGroup.animations = #[appearingAnimation, disappearingAnimation];
animationGroup.repeatCount = MAXFLOAT;
[layer addAnimation:animationGroup forKey:#"test"];
}
When you want to change durations of appearingAnimation and disappearingAnimation, you just need modify the code like
appearingAnimation.duration = DURATION_BASE*1.2;
disappearingAnimation.duration = DURATION_BASE*1.2;
there are a lot of examples how to animate along a UIBezierPath. But i can't figure it out, how to animate only the appendPath of an UIBezierPath.
In my case i have the following example:
// Create path from view controller
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:
CGRectMake(0, 0, tutorialController.bounds.size.width,
tutorialController.bounds.size.height) cornerRadius:0];
// Create circle path
UIBezierPath *circlePath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(x, y, 2.0*radius, 2.0*radius) cornerRadius:radius];
[path appendPath:circlePath];
[path setUsesEvenOddFillRule:YES];
// Create spot layer
spotLayer = [CAShapeLayer layer];
spotLayer.path = path.CGPath;
spotLayer.fillRule = kCAFillRuleEvenOdd;
spotLayer.fillColor = [UIColor blackColor].CGColor;
spotLayer.opacity = 0.90;
So basically its a circle inside a rect and i now want to animate only the circle. In all attempts the rect was also moved.
Here is my example, how i can animate "along" a path
CGPoint pathStart = CGPointMake(96.000000, 226.000000);
CGPoint pathEnd = CGPointMake(120.000000, 300.000000);
[circlePath moveToPoint:pathStart];
[circlePath addLineToPoint:pathEnd];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = circlePath.CGPath;
anim.duration = 5.0;
anim.calculationMode = kCAAnimationPaced;
[spotLayer addAnimation:anim forKey:#"bezierPathAnim"];
But that is not what i want...
What i want to do is, to move my circle path (which is an appended path of a rectangle path) via animation
Does anybody has some ideas?
Regards
I solved my Problem :-)
Here is the solution...
// If there is an old path animate to the new path
if(oldPath != nil){
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"path"];
[anim setFromValue:(__bridge id)oldPath];
[anim setToValue:(__bridge id)[path CGPath]];
[anim setDuration:0.3];
[anim setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[spotLayer addAnimation:anim forKey:#"path"];
}
First of all i kept the oldPath (starting point) and then animate to the new setted path.
My first Core Animation is just an image moving from point a to b. For some reason when the animation is called a red box appears and moves in place of my image while it just sits there.
Here is my code:
-(IBAction)preform:(id)sender{
CALayer *layer = [CALayer layer];
[layer setPosition:CGPointMake(100.0, 100.0)];
[layer setBounds:CGRectMake(0.0, 0.0, 50.0, 60.0)];
[layer setBackgroundColor:[[UIColor redColor] CGColor]];
[imView.layer addSublayer:layer];
CGPoint point = CGPointMake(imView.frame.origin.x, imView.frame.origin.y);
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.fromValue = [NSValue valueWithCGPoint:point];
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(point.x + 50, point.y)];
anim.duration = 1.5f;
anim.repeatCount =1;
anim.removedOnCompletion = YES;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[layer addAnimation:anim forKey:#"position"];
}
If you don't need that red box for your app's UI, don't create it and instead apply the animation to the view's layer. If you do need that red box, do create, but apply the animation to the view's layer, not the red box layer
Also note that unless your change your layer's anchorPoint, the position property actually is the center of the layer, not the corner. You need to take that in account for making smooth animations.
Finally, if you don't want your view to snap back after the animation is done, you will need to set it's new position. Don't worry, while the animation is in play, the view's relocation isn't visible.
Suggested (and untested) code:
-(IBAction)preform:(id)sender{
CGPoint point = CGPointMake(imView.frame.size.width/2.0 , imView.frame.size.height/2.0 );
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"position"];
anim.fromValue = [NSValue valueWithCGPoint:point];
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(point.x + 50, point.y)];
anim.duration = 1.5f;
anim.repeatCount =1;
anim.removedOnCompletion = YES;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[imView.layer addAnimation:anim forKey:#"position"];
imView.center = CGPointMake( imView.center.x + 50, imView.center.y
}
This would all be easier if you used the UIView animation helper functions instead, but I figure you are trying to learn how to use CAAnimation