I am drawing a circle, with an initial radius of 200
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.lineWidth = 7;
self.circle.bounds = CGRectMake(0, 0, 2 * radius, 2 * radius);
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.radius, self.radius)
Can anyone please tell me how to animate a change to a radius of 100?
This is how I ended up doing it:
UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CGRect newBounds = CGRectMake(0, 0, 2 * newRadius, 2 * newRadius);
CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath: #"path"];
pathAnim.toValue = (id)newPath.CGPath;
CABasicAnimation* boundsAnim = [CABasicAnimation animationWithKeyPath: #"bounds"];
boundsAnim.toValue = [NSValue valueWithCGRect:newBounds];
CAAnimationGroup *anims = [CAAnimationGroup animation];
anims.animations = [NSArray arrayWithObjects:pathAnim, boundsAnim, nil];
anims.removedOnCompletion = NO;
anims.duration = 2.0f;
anims.fillMode = kCAFillModeForwards;
[self.circle addAnimation:anims forKey:nil];
This is, I believe, the simpler and preferred way to do it:
UIBezierPath *newPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius) radius:newRadius startAngle:(-M_PI/2) endAngle:(3*M_PI/2) clockwise:YES];
CABasicAnimation* pathAnim = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnim.fromValue = (id)self.circle.path;
pathAnim.toValue = (id)newPath.CGPath;
pathAnim.duration = 2.0f;
[self.circle addAnimation:pathAnim forKey:#"animateRadius"];
self.circle.path = newPath.CGPath;
I got this approach from the Apple Documentation here (See listing 3-2). The important things here are that you're setting the fromValue and also setting the final value for self.circle's path to newPath.CGPath right after you set up the animation. An animation does not change the underlying value of the model, so in the other approach, you're not actually making a permanent change to the shape layer's path variable.
I've used a solution like the chosen answer in the past (using removedOnCompletion and fillMode etc) but I found that on certain versions of iOS they caused memory leaks, and also sometimes that approach just doesn't work for some reason.
With this approach, you're creating the animation explicitly (both where it starts from and where it ends), and you're specifying the final permanent value for the path at the end (with that last line), so that there's no need to mess with not removing the animation when it finishes (which I believe is what leads to memory leaks if you do this animation a lot of times... the unremoved animations build up in memory).
Also, I removed the animation of the bounds because you don't need it to animate the circle. Even if the path is drawn outside the bounds of the layer, it'll still be fully shown (see the docs for CAShapeLayer about that). If you do need to animate the bounds for some other reason, you can use the same approach (making sure to specify the fromValue and the final value for bounds).
You can use a CAAnimationGroup to animate the bounds and path of your CAShapeLayer:
- (void)animateToRadius:(CGFloat)newRadius {
CAShapeLayer *shapeLayer = ...;
UIBezierPath *newBezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(newRadius, newRadius)];
NSValue *newBounds = [NSValue valueWithCGRect:CGRectMake(0,0,2*newRadius,2*newRadius);
CAAnimationGroup *group = [CAAnimationGroup animation];
NSMutableArray *animations = [#[] mutableCopy];
NSDictionary *animationData = #{#"path":newBezierPath.CGPath, #"bounds":newBounds};
for(NSString *keyPath in animationData) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
animation.toValue = animationData[keyPath];
[animations addObject:animation];
}
group.animations = [animations copy];
group.duration = 2;
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
[shapeLayer addAnimation:group forKey:nil];
}
Here is a direct swift conversion of Jonathan's code should anyone need it.
func scaleShape(shape : CAShapeLayer){
let newRadius : CGFloat = 50;
let newPath: UIBezierPath = UIBezierPath(arcCenter: CGPointMake(newRadius, newRadius), radius: newRadius, startAngle: (CGFloat(-M_PI) / 2.0), endAngle: (3 * CGFloat(M_PI) / 2), clockwise: true);
let newBounds : CGRect = CGRect(x: 0, y: 0, width: 2 * newRadius, height: 2 * newRadius);
let pathAnim : CABasicAnimation = CABasicAnimation(keyPath: "path");
pathAnim.toValue = newPath.CGPath;
let boundsAnim : CABasicAnimation = CABasicAnimation(keyPath: "bounds");
boundsAnim.toValue = NSValue(CGRect: newBounds);
let anims: CAAnimationGroup = CAAnimationGroup();
anims.animations = [pathAnim, boundsAnim];
anims.removedOnCompletion = false;
anims.duration = 2.0;
anims.fillMode = kCAFillModeForwards;
shape.addAnimation(anims, forKey: nil);
}
Related
I just updated to Xcode 8 and I had problem with Blur (UIVisualEffectView) on iOS 10, but I solve it. But after I fixed the blur layer mask second problem is appears, when I getting the reference of maskView from UIVisualEffectView and apply the new UIBezierPath nothing happens.
Code example:
-(void)prepareEffectView{
//Frame
CGRect frame = self.view.bounds;
UIView *maskView = [[UIView alloc] initWithFrame:frame];
maskView.backgroundColor = [UIColor blackColor];
maskView.layer.mask = ({ // This mask draws a rectangle and crops a circle inside it.
UIBezierPath *maskLayerPath = [UIBezierPath bezierPath];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:frame]];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(self.btnScan.center.x, self.btnScan.center.y, 0, 0)]];
[maskLayer setPath:maskLayerPath.CGPath];
[maskLayerPath setUsesEvenOddFillRule:YES];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = maskLayerPath.CGPath;
mask.fillRule = kCAFillRuleEvenOdd;
mask;
});
self.visualEffectView.maskView = maskView;
}
-(void)openAperture{
//Blur layer
CGRect frame = self.view.bounds;
CAShapeLayer *maskLayer = (CAShapeLayer *)self.visualEffectView.maskView.layer.mask;
//New path value
UIBezierPath *maskLayerPath = [UIBezierPath bezierPath];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:frame]];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRoundedRect:UIEdgeInsetsInsetRect(frame, UIEdgeInsetsMake(58, 15, 66, 15)) cornerRadius:1]];
[maskLayer setPath:maskLayerPath.CGPath];
//Animation
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.delegate = self;
animation.duration = 0.44;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = (id)maskLayer.path;
animation.toValue = (id)maskLayerPath.CGPath;
[maskLayer addAnimation:animation forKey:#"animatePath"];
}
Apple Note About VisualEffectView issue on iOS 10
Transitions are tough in this realm, as maskView is the only way to
ensure that the effect works, but maskView also isn't animatable.
https://forums.developer.apple.com/message/184810#184810
The only the way to use "CAShapeLayer" as a mask is to apply your shape to UIView.layer.mask and then put it to UIVisualEffectView.maskView.
The maskView of UIVisualEffectView isn't animatable.
I have the same problem.
I tried to do next: (Swift code)
visualEffectView.layer.mask = maskView.layer.mask
instead of visualEffectView.mask = maskView
If so, path animation works fine, but then blur effect is gone.
If you find a solution, please update the post.
i wanna make an arc with the use of CAGradientLayer in such a way:-
20% of the arc have same single color then other 80% of arc has a gradient of two colors.
I already tried by hand on locations
startPoint endPoint property of CAGradientLayer but couldn't get the success and already go through from tutorials but somehow couldn't understand the concept properly.please solve my problem with clear description of it.
// for beizier path
- (UIBezierPath *)samplePath
{
UIBezierPath *path = [UIBezierPath bezierPath];
path = [UIBezierPath bezierPath];
[path addArcWithCenter:CGPointMake(200, 200) radius:30.0f startAngle:(3*M_PI)/4 endAngle:M_PI/4 clockwise:YES];
path.lineWidth = 30;
[[UIColor redColor] setStroke];
// [[UIColor colorWithRed:arc4random() green:arc4random() blue:arc4random() alpha:1.0] setFill];
[path stroke];
return path;
}
- (void)startAnimation
{
CAShapeLayer *shapeLayer;
if (self.pathLayer == nil)
{
shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self samplePath] CGPath];
shapeLayer.strokeColor = [[UIColor redColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 30;
self.pathLayer = shapeLayer;
}
[self animationBasic];
[self gradientLayer:shapeLayer];
}
-(void)animationBasic
{
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = #(0.0f);
pathAnimation.toValue = #(1.0f);
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
-(void)gradientLayer:(CAShapeLayer *)shapelayer
{
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.frame;
gradientLayer.colors = #[(__bridge id)[UIColor yellowColor].CGColor,(__bridge id)[UIColor redColor].CGColor ];
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.5f],
[NSNumber numberWithFloat:1.0f],
nil];
// gradientLayer.startPoint = CGPointMake(0,1);
// gradientLayer.endPoint = CGPointMake(1,1);
[self.layer addSublayer:gradientLayer];
gradientLayer.mask = shapelayer;
}
The locations property of a CAGradientLayer instance should be populated with the starting points (I know, the word stops makes it sound the opposite) of each color as specified in the colors property, from 0 to 1. These stops are rendered vertically from top to bottom.
So, this should work:
gradientLayer.locations = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0f],
[NSNumber numberWithFloat:0.5f],
nil];
Now, at the same time, if you want to make the gradient horizontal (or any other direction), you can make use of the startPoint and endPoint properties.
gradientLayer.startPoint = CGPointMake(0.0, 0.5); //Default is (0.5, 0,0)
gradientLayer.endPoint = CGPointMake(1.0, 0.5); //Default is (0.5, 1.0)
//The gradientLayer will be rendered horizontally.
The startingPoint and endingPoint properties determine the direction and layout of the gradient in the layer. These are defined in the unit coordinate system, where each point is defined as a set of unit coordinates and the top left corner being the origin. So, (0,0) corresponds to the origin, (1,1) corresponds to the bottom right corner. You can check this excellent answer for a better explanation.
So to summarize, the gradient is rendered in the direction and region as specified by the startPoint and endPoint properties, with the colors rendered in the ratio specified in the locations property.
At the end, you can only understand this fully by tinkering with the properties and using various combinations to see what effect is rendered.
I'm animating the drawing of a basic circle. This works fine, except the animation begins drawing at the 3 o'clock position. Does anyone know how I can make it start at 12 o'clock?
self.circle = [CAShapeLayer layer];
self.circle.fillColor = nil;
self.circle.lineWidth = 7;
self.circle.strokeColor = [UIColor blackColor].CGColor;
self.circle.bounds = CGRectMake(0, 0, 200, 200);
self.circle.path = [UIBezierPath bezierPathWithOvalInRect:self.circle.bounds].CGPath;
[self.view.layer addSublayer:self.circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
You can use bezierPathWithArcCenter instead of bezierPathWithOvalInRect, because that allows to specify a start and end angle:
CGFloat radius = self.circle.bounds.size.width/2; // Assuming that width == height
self.circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:(-M_PI/2)
endAngle:(3*M_PI/2)
clockwise:YES].CGPath;
See the bezierPathWithArcCenter documentation for the meaning of the angles.
I've been unsuccessful at animating a flashing stroke on a CAShapeLayer using the answer from this previous thread, and after many searches I can find no other examples of animating the stroke using CABasicAnimation.
What I want to do is have the stroke of my CAShapeLayer pulse between two colors. Using CABasicAnimation for opacity works fine, but the [CABasicAnimation animationWithKeyPath:#"strokeColor"] eludes me, and I'd appreciate any advice on how to successfully implement.
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) [UIColor blackColor].CGColor;
strokeAnim.toValue = (id) [UIColor purpleColor].CGColor;
strokeAnim.duration = 1.0;
strokeAnim.repeatCount = 0.0;
strokeAnim.autoreverses = NO;
[shapeLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
// CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
// opacityAnimation.fromValue = [NSNumber numberWithFloat:0.0];
// opacityAnimation.toValue = [NSNumber numberWithFloat:1.0];
// opacityAnimation.duration = 1.0;
// opacityAnimation.repeatCount = 0.0;
// opacityAnimation.autoreverses = NO;
// [shapeLayer addAnimation:opacityAnimation forKey:#"animateOpacity"];
Uncommenting the opacity animation results in an expected opacity fade. The stroke animation produces no effect. An implicit strokeColor change animates as expected, but I would like documented confirmation that strokeColor can be explicitly animated using CABasicAnimation.
Update: The specific problem was that shapeLayer.path was NULL. Correcting that fixed the problem.
The code below works great for me. What is the lineWidth of your shapeLayer stroke path? Could that be the issue?
- (void)viewDidLoad
{
[super viewDidLoad];
UIBezierPath * circle = [UIBezierPath bezierPathWithOvalInRect:self.view.bounds];
CAShapeLayer * shapeLayer = [CAShapeLayer layer];
shapeLayer.path = circle.CGPath;
shapeLayer.strokeColor =[UIColor blueColor].CGColor;
[shapeLayer setLineWidth:15.0];
[self.view.layer addSublayer:shapeLayer];
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) [UIColor redColor].CGColor;
strokeAnim.toValue = (id) [UIColor greenColor].CGColor;
strokeAnim.duration = 3.0;
strokeAnim.repeatCount = 0;
strokeAnim.autoreverses = YES;
[shapeLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
}
Let me know if it works for you...
I'm trying to animate the appearance of a segment of a circle. To archive this I use a CABasicAnimations which works quiet fine.
The animation starts on top and moves quiet nicely to one third of the whole circle. But when the animation finishes, the circle is being drawn completely immediately.
How can I prevent that?
Here is the source code of my custom UIView:
- (void)drawRect:(CGRect)rect
{
int radius = 100;
int strokeWidth = 10;
CGColorRef color = [UIColor redColor].CGColor;
int timeInSeconds = 5;
CGFloat startAngle = 0;
CGFloat endAngle = 0.33;
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius) cornerRadius:radius].CGPath;
circle.position = CGPointMake(CGRectGetMidX(self.frame)-radius, CGRectGetMidY(self.frame)-radius);
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = color;
circle.lineWidth = strokeWidth;
[self.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = timeInSeconds;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fromValue = [NSNumber numberWithFloat:startAngle];
drawAnimation.toValue = [NSNumber numberWithFloat:endAngle];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
When you apply an animation to a layer, Core Animation creates a copy of the layer and animates the properties of the copy. The original layer is called the model layer and the copy is called the presentation layer. An animation never changes the properties of the model layer.
You tried to fix this by setting removedOnCompletion to NO. You would also have to set the fillMode of the animation to make this work, but it's not really the correct way to animate a property.
The correct way is to change the property on your model layer, then apply the animation.
// Change the model layer's property first.
circle.strokeEnd = endAngle;
// Then apply the animation.
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = timeInSeconds;
drawAnimation.fromValue = [NSNumber numberWithFloat:startAngle];
drawAnimation.toValue = [NSNumber numberWithFloat:endAngle];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
This is explained in the Core Animation Essentials video from WWDC 2011. I highly recommend watching it.