Animate previously added CALayer animations - ios

I'm working on a component that is supposed to render several images and animate their position while changing also the contents. I've been inspired by this post: http://ronnqvi.st/controlling-animation-timing/ and thought of using speed to control the animations.
My ideia is to add the CALayers with the animations to a top-level layer whose speed is 0 and then set this layer's speed to 1.0 when it's time to animate.
Here's the code for it:
...
#property (nonatomic,strong) CALayer *animationLayer;
...
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
self.animationLayer = [CALayer layer];
self.animationLayer.frame = frame;
self.animationLayer.speed = 0.0;
[self.layer addSublayer:self.animationLayer];
}
return self;
}
- (void)addLayerData:(LayerData*)layerData
{
CALayer *aLayer = [CALayer layer];
aLayer.contents = (id) layerData.image.CGImage;
aLayer.frame = CGRectMake(0,0,96,96);
aLayer.position = layerData.position;
aLayer.name = layerData.name;
CGRect pathLayerFrame = layerData.path.bounds;
pathLayerFrame.origin.x = 0.0;
pathLayerFrame.origin.y = 0.0;
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = pathLayerFrame;
pathLayer.path = layerData.path.CGPath;
pathLayer.strokeColor = layerData.strokeColor.CGColor;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.lineWidth = 10;
pathLayer.lineCap = kCALineCapRound;
[self.animationLayer addSublayer:pathLayer];
CAKeyframeAnimation *positionAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
[positionAnimation setPath: layerData.path.CGPath];
[positionAnimation setDuration: 10.0];
[positionAnimation setRemovedOnCompletion: NO];
[positionAnimation setFillMode: kCAFillModeBoth];
[positionAnimation setKeyTimes:layerData.keyTimes];
CAKeyframeAnimation *contentsAnimation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
[contentsAnimation setValues:[self buildContentImagesFromLayerData:layerData];
[contentsAnimation setCalculationMode:kCAAnimationDiscrete];
contentsAnimation.calculationMode = kCAAnimationLinear;
[contentsAnimation setDuration:0.1];
[contentsAnimation setDelegate:self];
[contentsAnimation setAutoreverses:YES];
[contentsAnimation setRepeatCount:HUGE_VALF];
[contentsAnimation setRemovedOnCompletion:NO];
[contentsAnimation setFillMode:kCAFillModeBoth];
[aLayer addAnimation:positionAnimation forKey:kPostionAnimationKey];
[aLayer addAnimation:contentsAnimation forKey:kContentsAnimationKey];
[self.animationLayer addSublayer:aLayer];
}
- (void)animate
{
self.animationLayer.speed = 1.0;
}
My issue is that although the animationLayer contains all the layers to animate and these have all the animations correctly defined I can't seem to get the layers moving around the screen.
The addLayerData is called several times by external client code, at different times (that is when user taps a button to add an image). The animate is called after that when the user taps a button.
Maybe I've missed something in the article.
Can anyone help me out on this one?
Thanks in advance.
UPDATE
If I drop the animationLayer and add the layers to be animated directly on the view's layer (self.layer) it works correctly.

Related

Circular Progress Bar with CAShapeLayer and CABasicAnimation

I am trying to make Circular Progress View using the idea from here
This is my code for the CircleView:
#import "CircleView.h"
#interface CircleView()
#property (nonatomic, strong) CAShapeLayer *circleLayer;
#end
#implementation CircleView
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.clearColor;
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2., self.frame.size.height / 2.0) radius: (self.frame.size.width - 10) / 2 startAngle:0.0 endAngle:M_PI * 2 clockwise:YES];
CAShapeLayer *downLayer = [[CAShapeLayer alloc] init];
downLayer.path = circlePath.CGPath;
downLayer.fillColor = UIColor.clearColor.CGColor;
downLayer.strokeColor = UIColor.lightGrayColor.CGColor;
downLayer.lineWidth = 10.0;
downLayer.strokeEnd = 1.0;
self.circleLayer = [[CAShapeLayer alloc] init];
self.circleLayer.path = circlePath.CGPath;
self.circleLayer.fillColor = UIColor.clearColor.CGColor;
self.circleLayer.strokeColor = UIColor.redColor.CGColor;
self.circleLayer.lineWidth = 10.0;
self.circleLayer.strokeEnd = 0.0;
[self.layer addSublayer:downLayer];
[self.layer addSublayer: self.circleLayer];
}
return self;
}
-(void)animateCircle:(NSTimeInterval)duration fromPart:(CGFloat)fromPart toPart: (CGFloat)toPart {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = duration;
animation.fromValue = #(fromPart);
animation.toValue = #(toPart);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
self.circleLayer.strokeEnd = toPart;
[self.circleLayer addAnimation:animation forKey:#"animateCircle"];
}
#end
The problem is when I try to make this animation more than once. Then it shows only the animation from the last call and as if the previous calls have already finished animating. For example if I make the following two calls:
[self.circleView animateCircle:1 fromPart:0.0 toPart:0.25];
[self.circleView animateCircle:1 fromPart:0.25 toPart:0.5];
It will show only animating the colouring from 0.25 percentage of the circle to 0.5 percentage of the circle. Can I change that? I need to show all animations one after another. (Need to show the progress of a download)
You need to remove these 2 below lines and it will work as expected.
animation.fromValue = #(fromPart);
animation.toValue = #(toPart);
Because you use CircleView to display progress of a download so your method don't need fromPart value when animating circle. You can update animateCircle method like below code.
-(void)animateCircle:(NSTimeInterval)duration toPart: (CGFloat)toPart {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = duration;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
self.circleLayer.strokeEnd = toPart;
[self.circleLayer addAnimation:animation forKey:#"animateCircle"];
}
Result

How to get coordinates of view during animation?

I've created CAShapeLayer and added Bezier Cubic path(with addCurveToPoint:controlPoint1:controlPoint2: function) and added a little view on start point of that curve. After that I've created CAKeyFrameAnimation and moved that little view from start point of curve to the end point with animation. I want to get the center point of that view during animation. Any ideas?
CGPoint start, p, p1,p2;
start = CGPointMake(0, 150);
p = CGPointMake(300, 150);
p1 = CGPointMake(50, 225);
p2 = CGPointMake(250, 75);
[self.bezierPath moveToPoint:start];
[self.bezierPath addCurveToPoint:p
controlPoint1:p1
controlPoint2:p2];
NSLog(#"self bezierpath = %#",self.bezierPath);
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.bezierPath.CGPath;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.lineWidth = 3.f;
[self.yellowView.layer addSublayer:shapeLayer];
self.sliderThumb = [[UIView alloc]initWithFrame:(CGRect){0,0,20,20}];
self.sliderThumb.backgroundColor = [UIColor blackColor];
self.sliderThumb.center = CGPointMake(0, 150);
[self.yellowView addSubview:self.sliderThumb];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = #"position";
animation.duration = 4.f;
animation.path = self.bezierPath.CGPath;
[self.sliderThumb.layer addAnimation:animation forKey:nil];
CALayer *layer = self.sliderThumb.layer.presentationLayer;
NSLog(#"layer coordinates %f", layer.frame.origin.x);
`
The presentationLayer of the view's layer is a CALayer which captures where the layer is mid-animation.
For example, if you want to get updates frame by frame, set up a CADisplayLink and examine the presentationLayer in the display link's handler:
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.duration = 4.0f;
animation.path = self.bezierPath.CGPath;
animation.delegate = self; // so we know when animation finished
[self startDisplayLink]; // start display link
[self.sliderThumb.layer addAnimation:animation forKey:nil];
With:
#property (nonatomic, weak) CADisplayLink *displayLink;
And:
// Called when animation stops (assuming you set the `delegate` for the animation).
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
[self stopDisplayLink:self.displayLink];
}
/// Start the display link.
- (void)startDisplayLink {
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
self.displayLink = displayLink;
}
/// Stop the display link.
///
/// #parameter displayLink The display link to be stopped.
- (void)stopDisplayLink:(CADisplayLink *)displayLink {
[displayLink invalidate];
}
/// The display link handler.
///
/// Called once for each frame of the animation.
///
/// #parameter displayLink The display link being handled.
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
CALayer *layer = self.sliderThumb.layer.presentationLayer;
NSLog(#"layer coordinates %f", layer.frame.origin.x);
}

Flickering in simple animation chain

I'm trying to animate a pulsting circle. But after the downward scale, there's something of a flickering effect. Any ideas to why that is? Here's my code so far:
- (instancetype)init
{
self = [super init];
if (self) {
if (!self.path) {
self.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:7 startAngle:0.0*(M_PI/180.0) endAngle:360.0*(M_PI/180.0) clockwise:YES].CGPath;
}
[self animateCircleUpward];
}
return self;
}
-(void)animateCircleUpward {
[CATransaction begin];
[CATransaction setCompletionBlock:^{[self animateCircleDownward];}];
CABasicAnimation * scaleUpwardAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.xy"];
scaleUpwardAnimation.fromValue = #(0.7);
scaleUpwardAnimation.toValue = #(1.0);
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacityAnimation.fromValue = #(1.0);
opacityAnimation.toValue = #(0.6);
self.fillColor = [UIColor greenColor].CGColor;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
NSArray *animations = #[scaleUpwardAnimation, opacityAnimation];
animationGroup.duration = 0.4;
animationGroup.animations = animations;
[self addAnimation:animationGroup forKey:#"pulse"];
[CATransaction commit];
}
-(void)animateCircleDownward {
[CATransaction begin];
[CATransaction setCompletionBlock:^{[self animateCircleUpward];}];
CABasicAnimation * scaleDownwardAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.xy"];
scaleDownwardAnimation.fromValue = #(1.0);
scaleDownwardAnimation.toValue = #(0.7);
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacityAnimation.fromValue = #(0.6);
opacityAnimation.toValue = #(1.0);
self.fillColor = [UIColor greenColor].CGColor;
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
NSArray *animations = #[scaleDownwardAnimation, opacityAnimation];
animationGroup.duration = 0.4;
animationGroup.animations = animations;
[self addAnimation:animationGroup forKey:#"pulse"];
[CATransaction commit];
}
#end
Thanx in advance!
In regards to your actual question you likely need to set your CAAnimationGroup's fillMode property to kCAFillModeForward. The default value is kCAFillModeRemoved, so you are likely glimpsing the non-presentation layer between calls back and forth from your Down and Up methods.
That said, it appears that what you're trying to create is a cyclic animation through back and forth calls between your Up and Down methods via CATransaction's completionBlock. This seems an incredibly inefficient approach; why not try something more like:
-(void)scaleAndOpacityAnimations
{
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.xy”];
scaleAnimation.fromValue = #(0.7f);
scaleAnimation.toValue = #(1.0f);
CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:#“opacity”];
opacityAnimation.fromValue = #(1.0f);
opacityAnimation.toValue = #(0.6f);
CAAnimationGroup *scaleAndOpacity = [CAAnimationGroup animation];
scaleAndOpacity.animations = #[scaleAnimation, opacityAnimation];
scaleAndOpacity.autoReverses = YES;
scaleAndOpacity.repeatCount = HUGE_VALF;
//OP had no value indicated for the duration in posted code, I’m including it here for completion
scaleAndOpacity.duration = 0.5f;
[self addAnimation:scaleAndOpacity forKey:#“pulse”];
}
Which will simply repeat infinitely many times without having to instantiate new CAAnimations on each cycle.

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.

Why when I set a low duration value for my CABasicAnimation does it jump?

Sample Project: http://cl.ly/1W3V3b0D2001
I'm using CABasicAnimation to create a progress indicator that is like a pie chart. Similar to the iOS 7 app download animation:
The animation is set up as follows:
- (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.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
}
The animation is triggered by setting the progress of the view:
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.progress = progress;
if (animated) {
CGFloat totalDurationForFullCircleAnimation = 0.25;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.innerPie.strokeEnd = progress;
pathAnimation.delegate = self;
pathAnimation.fromValue = #([self.innerPie.presentationLayer strokeEnd]);
pathAnimation.toValue = #(progress);
pathAnimation.duration = totalDurationForFullCircleAnimation * ([pathAnimation.toValue floatValue] - [pathAnimation.fromValue floatValue]);
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
}
However, in cases where I set the progress to something small, such as 0.25, there's a jump in the animation. It goes a little forward clockwise, jumps back, then keeps going forward as normal. It's worth nothing that this does not happen if the duration or progress is set higher.
How do I stop the jump? This code works well in every case except when the progress is very low. What am I doing wrong?
Ah! We should have seen this earlier. The problem is this line in your if (animated) block:
self.innerPie.strokeEnd = progress;
Since innerPie is a Core Animation layer, this causes an implicit animation (most property changes do). This animation is fighting with your own animation. You can prevent this from happening by disabling implicit animation while setting the strokeEnd:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
(Note how setDisableActions: is within the begin/commit.)
Alternatively, you can remove your own animation and just use the automatic one, using something like setAnimationDuration: to change its length.
Original Suggestions:
My guess is that your drawRect: is being called, which resets your strokeEnd value. Those layers should probably not be set up in drawRect: anyway. Try moving that setup to an init method or didMoveToWindow: or similar.
If that's not effective, I would suggest adding log statements to track the value of progress and [self.innerPie.presentationLayer strokeEnd] each time the method is called; perhaps they're not doing what you expect.
Why are you using drawRect:? There should be no drawRect: method in your class.
You should not be using drawRect if you are also using self.layer. Use one or the other, never both.
Change it to something like:
- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])
return nil;
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.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
return self;
}
When travelling within same distance, the smaller the duration, the higher the speed. When you set the duration too small, the speed will be very high, and that's why it looks like jumping.

Resources