How can I reset CABasicAnimation? - ios

I'm trying to make a circle and animate it. When I click the button, I want to restart this animation from the same StartPoint. When I click the button I should stop, clear the image and start it again.
I'm trying with the code below, but it's not working.
-(IBAction)MakeCircle:(id)sender{
// Make a circular shape
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Positioning
circle.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
// Configure color and linewidth
circle.fillColor = [UIColor whiteColor].CGColor;
circle.lineWidth = 10;
// Add to parent layer
[self.view.layer addSublayer:circle];
// Configure animation
//CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0;
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add the animation
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
//Stop, "clean"screen and start again
-(IBAction)ButtonStopClearStart:(id)sender{
circle.speed = 0.0;
[circle removeAnimationForKey:#"drawCircleAnimation"];
[self MakeCircle:(sender)];
}

Lets say your animation looks something like this...
func runTimerMaskAnimation(duration: CFTimeInterval, fromValue : Double){
...
let path = UIBezierPath(roundedRect: circleBounds, cornerRadius:
circleBounds.size.width * 0.5)
maskLayer?.path = path.reversing().cgPath
maskLayer?.strokeEnd = 0
parentCALayer.mask = maskLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.fromValue = fromValue
animation.toValue = 0.0
maskLayer?.add(animation!, forKey: "strokeEnd")
}
If I wanted to restart my timer from the original position I would remove the animation, remove the maskLayer and then run the animation again.
maskLayer?.removeAnimation(forKey: "strokeEnd")
maskLayer?.removeFromSuperlayer()
runTimerMaskAnimation(duration: 15, fromValue : 1)

Related

Create coloured circle based on value(360) or percentage(100%)

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.

UIView circle mask animation

I have an abstract strange shaped UIView.
And I need to display a smooth appearance animation.
I assume I should apply that animation to the mask above that view.
The mask is going to be circle shaped.
So user will see the 1'2'3'4' sequence.
How can I implement that?
I suppose I should apply some affine transformation
to the mask view. In that case, what should be the original shape of the mask?
A vertical line?
A simple example,suppose I have a 100*100 imageview,
Note:to make it simple,I use hard code numbers
CAShapeLayer * maskLayer = [CAShapeLayer layer];
maskLayer.bounds = CGRectMake(0, 0, 100, 100);
maskLayer.position = CGPointMake(50, 50);
UIBezierPath * path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(50.0, 50.0) radius:50 startAngle:0 endAngle:M_PI *2 clockwise:true];
maskLayer.path = path.CGPath;
maskLayer.fillColor = [UIColor clearColor].CGColor;
maskLayer.strokeColor = [UIColor blackColor].CGColor;
maskLayer.strokeEnd = 0.0;
maskLayer.lineWidth = 100;
self.imageview.layer.mask = maskLayer;
CABasicAnimation * animation = [CABasicAnimation animation];
animation.keyPath = #"strokeEnd";
animation.toValue = #(1.0);
animation.duration = 2.0;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
[maskLayer addAnimation:animation forKey:#"keyFrame"];

CAAnimation not working as expected on CALayer

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;

Piechart animation completed event

CGFloat radius = 100;
CAShapeLayer *pieShape = [CAShapeLayer layer];
pieShape.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 300, radius, radius)
cornerRadius:radius].CGPath;
pieShape.fillColor = [UIColor clearColor].CGColor;
pieShape.strokeColor = [UIColor orangeColor].CGColor;
pieShape.lineWidth = 100;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 15.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.removedOnCompletion = NO;
pathAnimation.fillMode = kCAFillModeForwards;
[pieShape addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
Above code is working fine.
But my problem is how can know that the duration 15 second is completed.
You can tell the animation has completed using the superclass CAAnimation delegate method:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
If the duration is specified as 15.0 seconds and the animationDidStop has been invoked with finished set to YES, the you know that the time has elapsed.

CABasicAnimation - transform scale keep in center

Trying to animatie an ellipse masked on a UIView to be scale transformed remaining in the center position.
I have found CALayer - CABasicAnimation not scaling around center/anchorPoint, and followed by adding a bounds property to the maskLayer CAShapeLayer however, this ends with the mask being positioned in the left corner with only 1/4 of it showing. I would like the mask to remain within the center of the screen.
#synthesize maskedView;
- (void)viewDidLoad
{
[super viewDidLoad];
UIViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"Next"];
vc.view.frame = CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height);
vc.view.layer.bounds = self.view.layer.bounds;
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGRect maskRect = CGRectMake((self.view.frame.size.width/2)-50, (self.view.frame.size.height/2)-50, 100, 100);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddEllipseInRect(path, nil, maskRect);
[maskLayer setPath:path];
CGPathRelease(path);
vc.view.layer.mask = maskLayer;
[self.view addSubview:vc.view];
maskedView = vc.view;
[self startAnimation];
}
Animation...
-(void)startAnimation
{
maskedView.layer.mask.bounds = CGRectMake((self.view.frame.size.width/2)-50, (self.view.frame.size.height/2)-50, 100, 100);
maskedView.layer.mask.anchorPoint = CGPointMake(.5,.5);
maskedView.layer.mask.contentsGravity = #"center";
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
animation.duration = 1.0f;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation.fromValue = [NSNumber numberWithFloat:1.0f];
animation.toValue = [NSNumber numberWithFloat:5.0f];
[maskedView.layer.mask addAnimation:animation forKey:#"animateMask"];
}
Update
I seem to have fixed this with a second animation on the position key. Is this correct or is there a better way of doing this?
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"position"];
animation2.duration = 1.0f;
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation2.fromValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
animation2.toValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
[toBeMask.layer.mask addAnimation:animation2 forKey:#"animateMask2"];
You can create a scale around the center of the object instead of (0, 0) like this:
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform"];
CATransform3D tr = CATransform3DIdentity;
tr = CATransform3DTranslate(tr, self.bounds.size.width/2, self.bounds.size.height/2, 0);
tr = CATransform3DScale(tr, 3, 3, 1);
tr = CATransform3DTranslate(tr, -self.bounds.size.width/2, -self.bounds.size.height/2, 0);
scale.toValue = [NSValue valueWithCATransform3D:tr];
So we start out with the "identity" transform (which means 'no transform'). Then we translate (move) to the center of the object. Then we scale. Then we move back to origo so we're back where we started (we only wanted to affect the scale operation).
I seem to have fixed this with a second animation on the position key. Is this correct or is there a better way of doing this?
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"position"];
animation2.duration = 1.0f;
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
animation2.fromValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
animation2.toValue = [NSValue valueWithCGPoint:CGPointMake((self.view.frame.size.width/2), (self.view.frame.size.height/2))];
[toBeMask.layer.mask addAnimation:animation2 forKey:#"animateMask2"];
I am not sure why, but CAShapeLayer does not set the bounds property to the layer. In my case, that's was the problem.
Try setting the bounds. That should work.
I did something like this for building the initial circle
self.circle = [CAShapeLayer layer];
CGRect roundedRect = CGRectMake(0, 0, width, width);
self.circle.bounds = roundedRect;
UIBezierPath *start = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:width/2];
[self.circle setPath:start.CGPath];
self.circle.position = CGPointMake(whatever suits you);
self.circleView.layer.mask = self.circle;
[self.circleView.layer.mask setValue: #(1) forKeyPath: #"transform.scale"];
And something like this for scaling it
CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
scale.fromValue = [self.circleView.layer.mask valueForKeyPath:#"transform.scale"];
scale.toValue = #(100);
scale.duration = 1.0;
scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.circleView.layer.mask setValue:scale.toValue forKeyPath:scale.keyPath];
[self.circleView.layer.mask addAnimation:scale forKey:scale.keyPath];
PD: AnchorPoint is by default (.5,.5) according to apple documentations...but we all know that does not mean anything right?
Hope it helps!!
I have written following function with many added tweaks and options, could be useful for many others.
func layerScaleAnimation(layer: CALayer, duration: CFTimeInterval, fromValue: CGFloat, toValue: CGFloat) {
let timing = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
let scaleAnimation: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
CATransaction.begin()
CATransaction.setAnimationTimingFunction(timing)
scaleAnimation.duration = duration
scaleAnimation.fromValue = fromValue
scaleAnimation.toValue = toValue
layer.add(scaleAnimation, forKey: "scale")
CATransaction.commit()
}
Note: Don't forget to update size after animation completion, because CATransaction reverts back to actual size.

Resources