The CAMediaTiming protocol defines property timeOffset which is supposed to set additional time offset of an animation or animated layer. By setting layer.speed = 0 it's possible to manually control animation timing by setting layer.timeOffset to a given value.
And I managed to do it in a regular view, however when I try to do it (set time offset of a layer) when the layer is a descendant of UITableViewCell's layer it has no effect.
Here's a quick snippet so you can see what I am trying to achieve and what doesn't work.
-(void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
[tableView deselectRowAtIndexPath:indexPath animated:NO];
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(0, 0, 80, 80);
layer.backgroundColor = [UIColor redColor].CGColor;
layer.opacity = .1f;
[cell.contentView.layer addSublayer:layer];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.toValue = (id) [NSNumber numberWithFloat:1.f];
animation.duration = 1.f;
[layer addAnimation:animation forKey:nil];
layer.timeOffset = 0.7f;
layer.speed = 0.f;
}
You must allow the animation to start before you can control its timeOffset. Use a block with very small delay to do so:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"opacity"];
ba.removedOnCompletion = NO;
ba.duration = kAnimationDuration;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
ba.fromValue = [NSNumber numberWithDouble:kInitialAlpha];
ba.toValue = [NSNumber numberWithDouble:kFinalAlpha];
[self.layer addAnimation:ba forKey:#"opacity"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
double offset = self.alphaOffset * ( kAnimationDuration / kNumAlphaOffsets );
self.layer.timeOffset = offset;
});
Related
I'm trying to make a breathing circle animation. Only breathInAnimation (grow circle) seems to animate. breathOutAnimation (shrink circle) gets called but doesn't seem to do anything. I'm guessing it immediately reverts back to the starting state but I don't understand why.
- (void)viewDidLoad
animationView = [[UIView alloc] initWithFrame:CGRectMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0, 200, 200)];
animationView.backgroundColor = [UIColor blueColor];
animationView.layer.cornerRadius = 100;
animationView.center = self.view.center;
[self.view addSubview: animationView];
[self drawCircleEdge];
[self breathInAnimation];
[NSTimer timerWithTimeInterval:7.0 target:self selector:#selector(breathOutAnimation) userInfo:nil repeats:YES];
- (void)breathInAnimation
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
[scaleAnimation setValue:#"breathIn" forKey:#"id"];
scaleAnimation.duration = 4;
scaleAnimation.fromValue = [NSNumber numberWithFloat:0.1];
scaleAnimation.toValue = [NSNumber numberWithFloat:1];
[animationView.layer addAnimation:scaleAnimation forKey:#"scale"];
- (void)breathOutAnimation
CABasicAnimation *breathOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
breathOut.duration = 8;
breathOut.fromValue = [NSNumber numberWithFloat:1];
breathOut.toValue = [NSNumber numberWithFloat:0.1];
[animationView.layer removeAllAnimations];
[animationView.layer addAnimation: breathOut forKey:#"scale"];
I also tried using the delegate of scaleAnimation.
- (void)animationDidStop:(CABasicAnimation *)theAnimation2 finished:(BOOL)flag
This works but after the animation finished the circle goes back to the state it was after ending the first animation. Shrinking -> animation ends -> fullsize again.
I'm not sure what I'm missing here.
CAAnimations doesn't apply the transformation to your layer, it's animating on a presentation layer and then switch back to your layer when animation is finished.
You should apply your transform when you are playing the animation.
-(void)breathOutAnimation
{
CABasicAnimation *breathOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
breathOut.duration = 8;
breathOut.fromValue = [NSNumber numberWithFloat:1];
breathOut.toValue = [NSNumber numberWithFloat:0.1];
[animationView.layer removeAllAnimations];
[animationView.layer addAnimation: breathOut forKey:#"scale"];
animationView.layer.transform = CATransform3DMakeScale(0.1, 0.1, 0.1);
}
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.
I implemented corePlot in xcode and I'm using the pie chart. I'm trying to create a 3d flip animation while the chart reloads. Here is the code:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.5];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
[self.pieChart addAnimation:scaleAnimation forKey:#"scale"];
[self.pieChart reloadData];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
animation.fromValue = [NSNumber numberWithFloat:0.5];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 1.0f;
[self.pieChart addAnimation:animation forKey:#"scale"];
I didn't get desirable effects. I think what's happening is, both animations are happening at once. (Though I'm not sure that is what's happening.)
Also, is it possible to add z-depth? If so, how?
Update
I tried the follwoing:
CABasicAnimation *currentAnition = (CABasicAnimation *)anim;
if (currentAnition == self.scaleAnimation {...}
And it didn't work.
CAAnimation is KVC compliant, so we can store whether you're starting or finishing for lookup in the delegate methods:
{
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
[scaleAnimation setValue:#(YES) forKey:#"scaleAnimation"];
// set the delegate
scaleAnimation.delegate = self;
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.5];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
[self.pieChart addAnimation:scaleAnimation forKey:#"scale"];
[self.pieChart reloadData];
}
- (void)runSecondAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
[animation setValue:#(YES) forKey:#"secondAnimation"];
animation.delegate = self;
animation.fromValue = [NSNumber numberWithFloat:0.5];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 1.0f;
[self.pieChart addAnimation:animation forKey:#"scale"];
}
/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
BOOL scaleAnimation = [[anim valueForKey:#"scaleAnimation"] boolValue];
if (scaleAnimation) {
[self runSecondAnimation];
}
BOOL secondAnimation = [[anim valueForKey:#"secondAnimation"] boolValue];
if (secondAnimation) {
[self runThirdAnimation];
}
}
And remember you have also:
/* Called when the animation begins its active duration. */
- (void)animationDidStart:(CAAnimation *)anim;
there.
in iOS app, Core animation callback don't work.
- (void)startAnim {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
[self.target addAnimation:anim forKey:nil]; // self.target is CALayer instance, it's sublayer of Custom UIView
}
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
[self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
}
But animationDidStop never be called.
If I change the code like as following, completion blocked is called.
- (void)startAnim {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
}];
[self.target addAnimation:anim forKey:nil];
[CATransaction commit];
}
But I don't want to use CATransaction.
Why is not animationDidStop called?
Update:
There is a way to set final value like as
- (void)startAnim {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
[self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
[self.target addAnimation:anim forKey:nil];
}
But final assignment should be done when the animation is finished. Because there are multiple dynamic animations of layer, so I don't know final value.
I found the reason why animationDidStop is not called.
Because animation was added in loop of other thread,
So I fixed like as following.
- (void)startAnim {
dispatch_async(dispatch_get_main_queue(), ^{
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
[self.target addAnimation:anim forKey:nil];
});
}
To me it sounds like you dont want the animation to reset its position, this is quite simple and achieved with a couple lines of code when setting up the animation.
It can be easily placed in your code like such:
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
What this means is when your animation has finished it will remain at the end and any further animations will be from that state.
Last year I use animation of UIView instead CABasicAnimation, but if my memory does not fail you have to set endAngle befor add animation, so try this:
- (void)startAnim {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
[self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
[self.target addAnimation:anim forKey:nil];
}
UPD:
You are setting anim.toValue = endAngle; before start animation, so its not good that end value changes after animation complete. Anyway you can set it again in animationDidStop
- (void)startAnim {
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
anim.fromValue = startAngle;
anim.toValue = endAngle;
anim.duration = 2;
anim.delegate = self;
intermediateAngle = endAngle;
[self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
[self.target addAnimation:anim forKey:nil];
}
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
if (intermediateAngle != endAngle) {
startAngle = intermediateAngle;
[self startAnim]; // or just [self.target setValue:#(endAngle) forKeyPath:#"transform.rotation.z"];
}
}
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.