Animate CALayer border change - ios

I'm setting a border width and color to a UIView subclass this way:
- (void) setViewBorder
{
self.layer.borderColor = [UIColor greenColor].CGColor;
self.layer.borderWidth = 3.0f;
}
My question is: How can I animate this? Thanks.

Both borderColor and borderWidth are animatable properties but since you are doing this in a view subclass outside of an UIView animation block, implicit animations (those that happen automatically when you change a value) are disabled.
If you want to animate these properties then you can do an explicit animation with a CABasicAnimation. Since you are animating two properties on the same layer, you can add them both to an animation group and only configure the duration, timing, etc. once. Note that the explicit animations are purely visual, the model value (the actual property) doesn't change when you add them. That is why you both configure the animation and set the model value.
CABasicAnimation *color = [CABasicAnimation animationWithKeyPath:#"borderColor"];
// animate from red to blue border ...
color.fromValue = (id)[UIColor redColor].CGColor;
color.toValue = (id)[UIColor blueColor].CGColor;
// ... and change the model value
self.layer.borderColor = [UIColor blueColor].CGColor;
CABasicAnimation *width = [CABasicAnimation animationWithKeyPath:#"borderWidth"];
// animate from 2pt to 4pt wide border ...
width.fromValue = #2;
width.toValue = #4;
// ... and change the model value
self.layer.borderWidth = 4;
CAAnimationGroup *both = [CAAnimationGroup animation];
// animate both as a group with the duration of 0.5 seconds
both.duration = 0.5;
both.animations = #[color, width];
// optionally add other configuration (that applies to both animations)
both.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.layer addAnimation:both forKey:#"color and width"];
If you look at the documentation for CABasicAnimation under the section "Setting Interpolation values", you will see that it's not necessary to specify both the toValue and the fromValue like I did, so the code could be made slightly shorter. However, for clarity and readability (especially when you are starting with Core Animation) being more explicit can help you (and your coworker) understand the code.

Here's a swift solution for #David answer :
let colorAnimation = CABasicAnimation(keyPath: "borderColor")
colorAnimation.fromValue = UIColor.red.cgColor
colorAnimation.toValue = UIColor.blue.cgColor
view.layer.borderColor = UIColor.blue.cgColor
let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = 2
widthAnimation.toValue = 4
widthAnimation.duration = 4
view.layer.borderWidth = 4
let bothAnimations = CAAnimationGroup()
bothAnimations.duration = 0.5
bothAnimations.animations = [colorAnimation, widthAnimation]
bothAnimations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
view.layer.add(bothAnimations, forKey: "color and width")

If you just want to fade in the border you can also do this:
UIView.transition(with: self, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.layer.borderColor = UIColor.green.cgColor
self.layer.borderWidth = 3
}, completion: nil)

You can create a keyframe animation add it to your view.
//Create animation
CAKeyframeAnimation *colorsAnimation = [CAKeyframeAnimation animationWithKeyPath:#"borderColor"];
colorsAnimation.values = [NSArray arrayWithObjects: (id)[UIColor greenColor].CGColor,
(id)[UIColor yellowColor].CGColor, nil];
colorsAnimation.keyTimes = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.25],[NSNumber numberWithFloat:1.0], nil];
colorsAnimation.calculationMode = kCAAnimationLinear;
colorsAnimation.removedOnCompletion = NO;
colorsAnimation.fillMode = kCAFillModeForwards;
colorsAnimation.duration = 3.0f;
//Create animation
CAKeyframeAnimation *sizeAnimation = [CAKeyframeAnimation animationWithKeyPath:#"borderWidth"];
sizeAnimation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:5.0], nil];
sizeAnimation.keyTimes = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],[NSNumber numberWithFloat:3.0], nil];
sizeAnimation.calculationMode = kCAAnimationLinear;
sizeAnimation.removedOnCompletion = NO;
sizeAnimation.fillMode = kCAFillModeForwards;
sizeAnimation.duration = 3.0f;
//Add animation
[yoursubview.layer addAnimation:colorsAnimation forKey:nil];
[yoursubview.layer addAnimation:sizeAnimation forKey:nil];

Related

Glitches when queuing CAAnimations

I have a CAShapeLayer where I animate a circle. The animation is to first "undraw" the circle clockwise and then redraw the circle clockwise. Sort of a "rotating circle". Another way to put it: Move path stroke end point to start, then move the start point to the end.
The animation itself works, but it produces glitches now and then. It manifests in a short glimpse of the entire circle when it is supposed to be "undrawn".
Why does this occur and how can you fix it?
Thanks,
// Shape creation
layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.width - 2 * OUTER_BORDER_WIDTH, self.width - 2* OUTER_BORDER_WIDTH)].CGPath;
// Animation queuing
-(void) applyNextAnimation
{
CABasicAnimation* animation;
if (self.animatingOpening)
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
animation.delegate = self;
animation.removedOnCompletion = YES;
[self.outerCircleLayer addAnimation:animation forKey:#"stroke"];
}
// Animation stop callback
-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (self.isAnimating)
{
[self applyNextAnimation];
}
}
It blinks becuase you are not setting the corresponding property on the layer. So when the animation completes, the layer's model is still in the pre-animated state and that is what you see momentarily between the two animations.
This will get you towards what you want...
if (self.animatingOpening)
{
self.outerCircleLayer.strokeStart = 0.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
self.outerCircleLayer.strokeStart = 1.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
that almost works, but you will notice a more subtle glitch as you transition from the undrawn state to start animating the drawing state. The beginning of the circle has a small reverse animation as it starts. This is an implicit animation triggered by setting strokeStart from 1.0 to 0.0: which you need to get rid of so that all of the animations effects are under your control. You can achieve that most simply by setting disableActions to YES on CATransaction:
[CATransaction setDisableActions:YES];
( add it just above if (self.animatingOpening))

Core animation returns layer to the original position when label's text is changed

I've got simple animation which moves my view's layer and remains in it's final state.
CGPoint originalPosition = self.redSquare.layer.position;
self.redSquare.layer.position = CGPointMake(100, 100);
CABasicAnimation *animation = [[CABasicAnimation alloc]init];
animation.keyPath = #"position.x";
animation.fromValue = #(originalPosition.x);
animation.toValue = #250;
animation.duration = 2;
[self.redSquare.layer addAnimation:animation forKey:#"basic"];
self.redSquare.layer.position = CGPointMake(250, originalPosition.y);
But when I change a label's text the layer jumps to it's original position.
CGPoint originalPosition = self.redSquare.layer.position;
self.redSquare.layer.position = CGPointMake(100, 100);
CABasicAnimation *animation = [[CABasicAnimation alloc]init];
animation.keyPath = #"position.x";
animation.fromValue = #(originalPosition.x);
animation.toValue = #250;
animation.duration = 2;
animation.delegate = self;
[self.redSquare.layer addAnimation:animation forKey:#"basic"];
self.redSquare.layer.position = CGPointMake(250, originalPosition.y);
self.label.text = #"Started";
Why does it happen?
Because,when you change your text,the view need to redraw,
If you enable autoLayout of this view(default is enabled), it will adjust frame to autolayout.
So there are to solutions
Option 1:
Disable auto layout of your view
Option 2
When it comes to frame changed animation, try to animate autolayout

How do I smoothly update the toValue/end point of an animation in Core Animation?

I'm trying to emulate the effect Apple has as a progress indicator when downloading an app in iOS 7, somewhat of a pie chart:
My code's coming along pretty well. I can give it a value to go to and it will update to that.
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
self.innerPie.hidden = YES;
}
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
if (animated) {
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:progress];
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEnd"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
self.progress = progress;
}
My issue lies in updating the progress while in the middle of an animation. For example, if I'm downloading a file and the progress jumps from 0.1 to 0.2 (10% to 20%), I want to animate that. Say I give it a 0.5 second animation. What if, 0.2 seconds into the 0.5 second animation it jumps to 0.4 (40%)?
With my current code, it jumps to where it should have ended (when it should have animated) in the first animation (20%) and then starts animating again toward 40%.
What I'd like is for it to almost update the toValue to 0.4. At least that's what I think I want. Conceptually I'd like it to continue the animation toward the new value without interrupting the previous value. Smoothness, to put it simply.
How would I accomplish this?
I know there's libraries to do this. I want to do it myself for learning purposes.
You should replace
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
with :
pathAnimation.fromValue = [NSNumber numberWithFloat:[self.innerPie.presentationLayer strokeEnd]];
Initially, [self.innerPie.presentationLayer strokeEnd]]will be 1, so you will need to set the initial value to 0. Add these 2 lines to place where you create your 'innerPie' :
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
I've tested and it works.

CAAnimationGroup reverts to original position on completion

In iOS I'm trying to create the effect of an icon shrinking in size, and flying across the screen in an arc while fading out, and then disappearing. I've achieved these 3 effects with an CAAnimationGroup, and it does what I want. The problem is when the animation ends, the view appears back at the original position, full size and full opacity. Can anyone see what I am doing wrong in the code below?
The animation should not revert to it's original position, but just disappear at the end.
UIBezierPath *movePath = [UIBezierPath bezierPath];
CGPoint libraryIconCenter = CGPointMake(610, 40);
CGPoint ctlPoint = CGPointMake(self.imgViewCropped.center.x, 22.0);
movePath moveToPoint:self.imgViewCropped.center];
[movePath addQuadCurveToPoint:libraryIconCenter
controlPoint:ctlPoint];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
scaleAnim.removedOnCompletion = NO;
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:#"alpha"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnim.toValue = [NSNumber numberWithFloat:0.0];
opacityAnim.removedOnCompletion = NO;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim,scaleAnim,opacityAnim, nil];
animGroup.duration = 0.6;
animGroup.delegate = self;
animGroup.removedOnCompletion = NO;
[self.imgViewCropped.layer addAnimation:animGroup forKey:nil];
I believe you need to set the fillMode property of your animations to kCAFillModeForwards. That should freeze the animations at their end time. Another suggestion (and honestly, this is what I'd usually do) is just se the properties of the layer itself to their final position after you've set up the animation. That way when the animation is removed, the layer will still have the final properties as part of its model.
As an aside, the removedOnCompletion flag of animations contained within a CAAnimationGroup is ignored. You should probably just remove those assignments since they're misleading. Replace them with assignments to fillMode as specified above.

Animating CALayer of UIView to round corners

I'm trying to animate the rounding of the corners of my view. The cornerRadius property is listed as animatible, but I can't seem to get it to work. I actually can't get any of the other properties to animate either, but the corners are what I'm interested in. Here's my code, and it's pretty darn simple:
[CATransaction begin];
[CATransaction setValue: [NSNumber numberWithFloat: 2.0f] forKey:kCATransactionAnimationDuration];
self.myView.layer.cornerRadius = 50.0f;
[CATransaction commit];
What am I missing here guys? The corners become rounded, but it's instantaneous instead of taking 2 seconds.
CABasicAnimation *anim1 = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
anim1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim1.fromValue = [NSNumber numberWithFloat:0.0f];
anim1.toValue = [NSNumber numberWithFloat:50.0f];
anim1.duration = 2.0;
[self.myView.layer addAnimation:anim1 forKey:#"cornerRadius"];
Swift 4.2/5:
let anim1 = CABasicAnimation(keyPath: "cornerRadius")
anim1.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
anim1.fromValue = 0
anim1.toValue = 50
anim1.duration = 2.0
layer.add(anim1, forKey: "cornerRadius")
Swift 3/4.0:
let anim1 = CABasicAnimation(keyPath: "cornerRadius")
anim1.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
anim1.fromValue = 0
anim1.toValue = 50
anim1.duration = 2.0
myView.layer.add(anim1, forKey: "cornerRadius")
Update:
As Mark noticed, it's cleaner to use #keyPath to describe the property to be animated:
let anim1 = CABasicAnimation(keyPath: #keyPath(CALayer.cornerRadius))
Seems like you're overthinking it. I was able to get by without using CoreAnimation. Here's what I did:
UIView.animate(withDuration: animationDuration) {
self.myView.layer.cornerRadius = 10.0
}

Resources