I am aiming to reveal a menu with a circular reveal scaling animation. For this I first set the mask to a radius with a radius of 0 at the beginning.
#define DEGREES_TO_RADIANS(angle) ((angle) / 180.0 * M_PI)
CAShapeLayer *mask = [CAShapeLayer new];
mask.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(25, 25) radius:0 startAngle:0 endAngle:DEGREES_TO_RADIANS(360) clockwise:YES].CGPath;
self.layer.mask = mask;
Then I animate the new mask with a CABasicAnimation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.toValue = (id)[UIBezierPath bezierPathWithArcCenter:CGPointMake(25, 25) radius:self.frame.size.height*1.2 startAngle:0 endAngle:DEGREES_TO_RADIANS(360) clockwise:YES].CGPath;
animation.removedOnCompletion = NO;
animation.duration = 0.45f;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.layer.mask addAnimation:animation forKey:nil];
On the first look this is working but the problem is that the animation itself is more oval than circular: http://i.stack.imgur.com/fqPYJ.png
The code is inspired by Animate circular mask of UIImageView in iOS.
Your code looks reasonable. (Although if you're always using a full circle you could probably use the simpler bezierPathWithOvalInRect method instead of bezierPathWithArcCenter).
It's hard to tell what I'm looking at in the image you linked. The two layer are white so we can't tell what is what. You might want to set one of the layers to a colored background while you're testing.
As to why you would get an oval: Do you have a transform on any of your layers? Specifically a scale transform? That would cause your distortion.
Related
I have a few rects that I draw a circle in with UIBezierPath and CAShapeLayer. They should act like buttons, in which when they are tapped they are being selected and when a different button is tapped they are being deselected, where each of these states should change button appearance between these two conditions:
deselected: selected:
This is the general code (which is called when selection changes):
CGFloat radius = isSelected ? 9.8 : 13. ;
CGFloat strokeWidth = isSelected ? 10.5 : 2;
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
[bezierPath addArcWithCenter:center radius:radius startAngle:0 endAngle:2 * M_PI clockwise:YES];
CAShapeLayer *progressLayer = [[CAShapeLayer alloc] init];
[progressLayer setPath:bezierPath.CGPath];
[progressLayer setStrokeColor:[UIColor redColor].CGColor];
[progressLayer setLineWidth:strokeWidth]];
[self.layer addSublayer:progressLayer];
When this rect is selected, I would like to change the strokeWidth of the circle only inward, and keep the original radius. When it's deselected, the opposite should happen.
As you can see I change both the radius and the lineWidth, because when I make strokeWidth bigger, the outer radius is growing as well. And as I said, I want to keep it the same.
The problem is that I want it animated in a way that the width grows inward when selected, and out when deselected, without changing the outer radius at all.
What I have so far, is an animation of the change of strokeWidth together with the radius in order to keep the result with the same appeared radius. But it's not what I need. As you can see, only the radius change is animated here, and the result is a growing circle, which I don't want.
This is the code of the animation (in addition to the original code):
CABasicAnimation *changeCircle = [CABasicAnimation animationWithKeyPath:#"path"];
changeCircle.duration = 0.6;
changeCircle.fromValue = (__bridge id)oldPath; // current bezier path
changeCircle.toValue = (__bridge id)newPath; // new bezier path
[progressLayer addAnimation:changeCircle forKey:nil];
I had also the opposite code, that animates only the change of strokeWidth. It also doesn't do what I need.
Is there a way to the strokeWidth grow or get small without changing the outer radius?
Like this:
Thanks to #Chris, I managed to work it out.
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
pathAnimation.fromValue = (__bridge id) oldPath;
pathAnimation.toValue = (__bridge id) newPath;
CABasicAnimation *lineWidthAnimation = [CABasicAnimation animationWithKeyPath:#"lineWidth"];
lineWidthAnimation.fromValue = isSelected ? #2. : #10.5;
lineWidthAnimation.toValue = isSelected ? #10.5 : #2.;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = 0.3;
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
group.animations = #[pathAnimation, lineWidthAnimation];
CAShapeLayer *progressLayer = [[CAShapeLayer alloc] init];
[progressLayer setPath:bezierPath.CGPath];
[progressLayer setStrokeColor:self.color.CGColor];
[progressLayer setLineWidth:isSelected ? 10.5 : 2];
[progressLayer setFillColor:[UIColor clearColor].CGColor];
[progressLayer addAnimation:group forKey:#"selectedAnimations"];
[self.layer addSublayer:progressLayer];
This code creating a complete circle but I want to create a circle based on value either 360 or percentage 100%. IF percentage is 56% I want 56% circle. Like this based on percentage/value, I want circle.
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(29, 29) radius:27 startAngle:-M_PI_2 endAngle:2 * M_PI - M_PI_2 clockwise:YES].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor greenColor].CGColor;
circle.lineWidth = 4;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 10;
animation.removedOnCompletion = NO;
animation.fromValue = #(0);
animation.toValue = #(1);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[circle addAnimation:animation forKey:#"drawCircleAnimation"];
[_colouredCircle.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
[_colouredCircle.layer addSublayer:circle];
That means that your toValue should match your percentage.
In your case, animation.toValue = #(0.56); would end the stroke at 56% of the circle (0...1 range).
See this answer for information about keeping the final animated value after completion.
TLDR: You have to also update the model layer.
circle.strokeStart = 0;
circle.strokeEnd = 0.56; // Your target value
...
// Your animation
Check out this circular progress bar and set colour as you want to set at particular position.
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'm using the following code to draw part of a circle. I'm a bit confused, however, because at the end of the animation it seems to immediately fill in the rest of the circle. Why is this happening?
// Set up the shape of the circle
int radius = 62;
CAShapeLayer *circle = [CAShapeLayer layer];
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Center the shape in self.view
circle.position = CGPointMake(18, 92);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor whiteColor].CGColor;
circle.lineWidth = 5;
// Add to parent layer
[cell.layer addSublayer:circle];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = .8; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:0.4f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
This is happening because by default a CAAnimation is removed from the layer when it completes. This means that it will remove the value that you animated to and set it to the default of 1.0 right after the animation finished. Since you are using a CAAnimation the layer is never actually changing its properties it just only looks like it is which is why when the animation is removed it gets set to 1.0 because that is what the layer's value for strokeEnd is since you never changed it. Try to see this yourself by printing the value of strokeEnd while the animation is happening.
This can be solved in 2 ways, setting the final storkeEnd value before you start the animation so that when it is removed it is still 0.4 or by adding certain properties to your CABasicAnimation. The properties you need to set to achieve what you are looking for are:
drawAnimation.fillMode = kCAFillModeForwards
and
drawAnimation.removedOnCompletion = false
Hope that helps
For the whole stroke to be animated drawAnimation.fromValue should be 0.0 and drawAnimation.toValue should be 1.0. These values determine what percentage of the stroke is animated.
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
fromValue : Defines the value the receiver uses to start interpolation.
toValue : Defines the value the receiver uses to end interpolation.
What you used was 0.4 for drawAnimation.toValue. So it ends at 40% of the animation and draws the rest in one go without animating.
For example if you set drawAnimation.fromValue = 0.5 and drawAnimation.toValue = 1.0,
animation will start from half a circle.
if you set drawAnimation.fromValue = 0.0 and drawAnimation.toValue = 0.5,
animation will end at half a circle and draw the rest without animation.
If you only wanted to draw 40% of the circle, you can use a different function to create the circle path.
CGFloat startAngle = (-M_PI/2);
CGFloat endAngle = startAngle + 0.4*(2*M_PI);
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES].CGPath;
The above code actually generates 40% of a circle path from startAngle = -M_PI/2. You can vary the angle according to your needs.
I'm trying to shrink a circular CAShapeLayer but using the key path 'path' does not seem to be working.
CGPathRef startPath = [UIBezierPath bezierPathWithOvalInRect:startRect].CGPath;
CGPathRef endPath = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(startRect, 15, 15)].CGPath;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
animation.duration = 1.0;
animation.fromValue = (__bridge id)startPath;
animation.toValue = (__bridge id)endPath;
animation.autoreverses = YES;
animation.repeatCount = CGFLOAT_MAX;
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.6 :0.3 :0.8 :0.45];
//circleLayer is a CAShapeLayer
[self.circleLayer addAnimation:animation forKey:#"pathAnimation"];
I think i must be misunderstanding about how a path animation works because pretty much identical code seems to work fine if i am try to animate, say, the opacity or the transform.
Any ideas?
I recently had the same question and ended up solving it by creating the circle, then animating the scale CGAffineTransform. In the view whose layer is the circle, I simply used
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformScale(transform, scaleX, scaleY);
[UIView animateWithDuration: 0.5 animations: ^{
self.transform = transform;
}];
Hope this helps!