Change CALayer color while animating - objective-c

APPROACH 1
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
[CATransaction begin];
{
[CATransaction setAnimationDuration:15];//Dynamic Duration
[CATransaction setCompletionBlock:^{
}];
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
animation.autoreverses = NO;
animation.removedOnCompletion = NO;
animation.fromValue = #0;
animation.toValue = #1;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[self.pathLayer addAnimation:animation forKey:animationKey];
}
[CATransaction commit];
I have added CAShapeLayer (pathLayer) in my view and I want it to animate around the view with stroke effect, the code above does the job but my problem is to change color in 3 equal proportions. So what I am assuming is to repeat the above code 3 times and change the following lines in respective order.
for 1st
animation.fromValue = #0;
animation.toValue = #(1/3);
animation.timeOffset = 0;
for 2nd
animation.fromValue = #(1/3);
animation.toValue = #(2/3);
animation.timeOffset = 0;// I don't know how to exactly set this active local
time since the duration which is currently 15 is dynamic can be 30 or 10.
for 3rd
animation.fromValue = #(2/3);
animation.toValue = #(3);
animation.timeOffset = 0;// Active local time- Not sure how and which value to set
APPROACH 2
Instead of 3 transactions with offset technique lets start 2nd transaction when 1st completes and 3rd when 2nd. But the fraction of time that is taken to start the new animation when one is completed a lag/jerk is visible.
APPROACH 3
SubClass CAShapeLayer
By doing SubClass, the drawInContext method is called only once, and if some extra property is added and it is changed the drawInContext method is called repeatedly and this way the layer color can be changed after specific progress period of time.
But overriding the drawInContext method doesn't serve the purpose.
Any Suggestions ? I don't want to implement NSTimer separately.

I'm not 100% clear on what you want here, but if the goal is just for the whole stroke to change color as it draws, but in three discrete stages, then I would propose adding the following. I cooked up these examples in a default "Single View Application" template. I've got a button set up with its action pointing at -doStuff:. If the whole stroke color were to change, it might look something like this:
To produce that, the code looked like:
#implementation MyViewController
{
CAShapeLayer* mLayer;
}
- (IBAction)doStuff:(id)sender
{
const NSUInteger numSegments = 3;
const CFTimeInterval duration = 2;
[mLayer removeFromSuperlayer];
mLayer = [[CAShapeLayer alloc] init];
mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
mLayer.fillColor = [[UIColor purpleColor] CGColor];
mLayer.lineWidth = 12.0;
mLayer.lineCap = kCALineCapSquare;
mLayer.strokeEnd = 0.0;
mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.
[self.view.layer addSublayer: mLayer];
[CATransaction begin];
{
[CATransaction setAnimationDuration: duration];// Dynamic Duration
[CATransaction setCompletionBlock:^{ NSLog(#"Done"); }];
const double portion = 1.0 / ((double)numSegments);
NSMutableArray* values = [NSMutableArray arrayWithCapacity: numSegments];
NSMutableArray* times = [NSMutableArray arrayWithCapacity: numSegments + 1];
for (NSUInteger i = 0; i < numSegments; i++)
{
[values addObject: (__bridge id)[[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor]];
[times addObject: #(i * portion)];
}
[times addObject: #(1.0)]; // Have to add this, otherwise the last value wont get used.
{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeColor"];
animation.keyTimes = times;
animation.values = values;
animation.calculationMode = kCAAnimationDiscrete;
animation.removedOnCompletion = NO;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[mLayer addAnimation: animation forKey: #"strokeColor"];
}
{
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath: #"strokeEnd"];
animation.fromValue = #(0);
animation.toValue = #(1);
animation.removedOnCompletion = NO;
animation.timeOffset = 0;
animation.fillMode = kCAFillModeForwards;
[mLayer addAnimation: animation forKey: #"strokeEnd"];
}
}
[CATransaction commit];
}
#end
Alternately, if the goal is to have three different segments of the stroke, all with different colors, that's a little more complicated, but can still be done with the same basic principals. One thing to note is that, without custom drawing, your CAShapeLayers can't have more than one stroke color (AFAIK), so you'll need to break this up into several sublayers.
This next example puts a shape layer into the view and then adds the sublayers for each part of the stroke and sets up the animation such that it appears theres a single, multi-color stroke being drawn, where each segment is a separate color. Here's roughly what it looked like:
Here's the code:
#implementation MyViewController
{
CAShapeLayer* mLayer;
}
- (IBAction)doStuff:(id)sender
{
const NSUInteger numSegments = 3;
const CFTimeInterval duration = 2;
[mLayer removeFromSuperlayer];
mLayer = [[CAShapeLayer alloc] init];
mLayer.frame = CGRectInset(self.view.bounds, 100, 200);
mLayer.fillColor = [[UIColor purpleColor] CGColor];
mLayer.lineWidth = 12.0;
mLayer.lineCap = kCALineCapSquare;
mLayer.path = [[UIBezierPath bezierPathWithRect: mLayer.bounds] CGPath]; // This can be whatever.
[self.view.layer addSublayer: mLayer];
[CATransaction begin];
{
[CATransaction setAnimationDuration: duration];//Dynamic Duration
[CATransaction setCompletionBlock:^{ NSLog(#"Done"); }];
const double portion = 1.0 / ((double)numSegments);
for (NSUInteger i = 0; i < numSegments; i++)
{
CAShapeLayer* strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = mLayer.bounds;
strokePart.path = mLayer.path;
strokePart.lineCap = mLayer.lineCap;
strokePart.lineWidth = mLayer.lineWidth;
// These could come from an array or whatever, this is just easy...
strokePart.strokeColor = [[UIColor colorWithHue: i * portion saturation:1 brightness:1 alpha:1] CGColor];
strokePart.strokeStart = i * portion;
strokePart.strokeEnd = (i + 1) * portion;
[mLayer addSublayer: strokePart];
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
NSArray* times = #[ #(0.0), // Note: This works because both the times and the stroke start/end are on scales of 0..1
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
animation.keyTimes = times;
animation.values = values;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[strokePart addAnimation: animation forKey: #"whatever"];
}
}
[CATransaction commit];
}
#end
I'm not sure I've exactly understood what you were going for, but hopefully one of these is helpful.

Related

how to Zoom image using CABasicanimation

[UIView animateWithDuration:0.5 animations:^{
imageView.frame = self.view.bounds;
} completion:^(BOOL finished) {}];
how can I write this using CABasicanimation.
below is the simple implementation
-(void)StartAnmation {
[subview.layer addAnimation:[self ZoomAnimation] forKey:#"Zoom"];
}
-(CAAnimationGroup *)ZoomAnimation {
CAAnimationGroup *ZoomAnimation = [CAAnimationGroup animation];
CABasicAnimation *In = [self zoomIn];
ZoomAnimation.animations = [NSArray arrayWithObjects: In, nil];
ZoomAnimation.duration = 2.0f;
return ZoomAnimation;
}
-(CABasicAnimation *)zoomIn {
CABasicAnimation *ZoomInAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
ZoomInAnimation.beginTime = 0.0f;
ZoomInAnimation.fromValue = [NSNumber numberWithFloat:20.0];
ZoomInAnimation.toValue = [NSNumber numberWithFloat:1.0];
ZoomInAnimation.duration = 2.0f;
return ZoomInAnimation;
}
For Zoom images you can done by using UIImageView itself.
Please refer the answer Pinch To Zoom Effect on UIImageView inside scrollView

CAShapeLayer animation flash when I set duration very small

I want to realize a round progress, and the progress value can be set dynamically. The code is followed:
- (CGPoint)center {
return CGPointMake(self.view.frame.origin.x + self.view.frame.size.width / 2.0,
self.view.frame.origin.y + self.view.frame.size.height / 2.0);
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CGMutablePathRef roundPath = CGPathCreateMutable();
CGPathAddArc(roundPath, NULL, self.center.x, self.center.y,
20,
2 * M_PI + M_PI_2,
M_PI_2,
YES);
CAShapeLayer *backgroundLayer = [CAShapeLayer layer];
backgroundLayer.frame = self.view.layer.bounds;
backgroundLayer.path = roundPath;
backgroundLayer.strokeColor = [[NSColor blueColor] CGColor];
backgroundLayer.fillColor = nil;
backgroundLayer.lineWidth = 10.0f;
backgroundLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:backgroundLayer];
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.frame = self.view.layer.bounds;
pathLayer.path = roundPath;
pathLayer.strokeColor = [[NSColor whiteColor] CGColor];
pathLayer.fillColor = nil;
pathLayer.lineWidth = 10.0f;
pathLayer.lineJoin = kCALineJoinBevel;
[self.view.layer addSublayer:pathLayer];
self.pathLayer = pathLayer;
[self start];
}
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}
I found when I set the duration be 0.1f or even bigger, it will work right.But if I set the duration be 0.01f, the animation will not start from the correct value, it will animate from a bigger value then decrease to the correct value. So the whole animation always flash, anybody got the same question or know why? Thanks very much!
originaluser2 is probably right about the specific cause, but this design is incorrect.
At 60fps, one frame is 0.0167s. You're asking to animate the change in less than a single frame. Each of those animations has its own media timing (kCAMediaTimingFunctionDefault), which means your creating complicated ramp up/ramp down velocities through your animation. And your timing is going to be erratic anyway because you're picking up a little error in every step (animationDidStop can take slightly different amounts of time to run depending on many factors). The whole point of animations is that you don't need to do this kind of stuff. That's why you have an animation engine.
Just animate to progress over the time you want it to take. Don't try to inject many extra animation steps. Injecting steps is what the animation engine does.
CALayer is designed to do most of this stuff for you anyway. You don't need to be creating explicit animations for this; just use implicit. Something like (untested, uncompiled):
- (void)setProgress: (CGFloat)progress {
double velocity = 1.0;
[CATransaction begin];
double delta = fabs(_progress - progress);
[CATransaction setAnimationDuration: delta * velocity];
[self.pathLayer setStrokeEnd: progress];
[CATransaction commit];
}
As Rob says, adding repeated animations isn't the best idea. His solution will probably work out nicely for you. However if you're still insistent on using repeated animations, here's the fix for your current code:
The problem is you're calling this line of code before you add your animation.
[self.pathLayer setStrokeEnd:self.progress + 0.01];
This will create an implicit animation on the layer, and therefore when you come to add your explicit animation – it will cause problems (the flashes you were observing).
The solution is to update the model layer within a CATransaction after you start the animation. You'll also need to set disableActions to YES in order to prevent an implicit animation from being generated.
For example:
- (void)start {
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 0.01;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:self.progress+0.01];
[pathAnimation setDelegate:self];
[self.pathLayer addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
Although, it's also worth noting you could just create your animation by just using a CATransaction, and the implicit animation for strokeEnd.
For example:
- (void)start {
[CATransaction begin];
[CATransaction setAnimationDuration:0.01];
[CATransaction setCompletionBlock:^{
self.progress += 0.01;
if (self.progress < 1.0) {
[self start];
}
}];
[self.pathLayer setStrokeEnd:self.progress + 0.01];
[CATransaction commit];
}
That way you don't have to create a new explicit animation on each iteration.

AVVideoCompositionCoreAnimationTool not adding all CALayers

Okay, this one has me completed stumped. I'm happy to post other code if you need it but I think this is enough. I cannot for the life of me figure out why things are going wrong. I'm adding CALayers, which contain images, to a composition using AVVideoCompositionCoreAnimationTool. I create an NSArray of all the annotations (see interface below) I want to add and then add them to the animation layer with an enumerator. No matter how many, as far as I can tell, annotations are in the array, the only ones that end up in the outputted video are the ones added by the last loop. Can someone spot what I'm missing?
Here's the interface for the annotations
#interface Annotation : NSObject// <NSCoding>
#property float time;
#property AnnotationType type;
#property CALayer *startLayer;
#property CALayer *typeLayer;
#property CALayer *detailLayer;
+ (Annotation *)annotationAtTime:(float)time ofType:(AnnotationType)type;
- (NSString *)annotationString;
#end
And here's the message that creates the video composition with the animation.
- (AVMutableVideoComposition *)createCompositionForMovie:(AVAsset *)movie fromAnnotations:(NSArray *)annotations {
AVMutableVideoComposition *videoComposition = nil;
if (annotations){
//CALayer *imageLayer = [self layerOfImageNamed:#"Ring.png"];
//imageLayer.opacity = 0.0;
//[imageLayer setMasksToBounds:YES];
Annotation *ann;
NSEnumerator *enumerator = [annotations objectEnumerator];
CALayer *animationLayer = [CALayer layer];
animationLayer.frame = CGRectMake(0, 0, movie.naturalSize.width, movie.naturalSize.height);
CALayer *videoLayer = [CALayer layer];
videoLayer.frame = CGRectMake(0, 0, movie.naturalSize.width, movie.naturalSize.height);
[animationLayer addSublayer:videoLayer];
// TODO: Consider amalgamating this message and scaleVideoTrackTime:fromAnnotations
// Single loop instead of two and sharing of othe offset variables
while (ann = (Annotation *)[enumerator nextObject]) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.duration = 3; // TODO: Three seconds is the currently hard-coded display length for an annotation, should this be a configurable option after the demo?
animation.repeatCount = 0;
animation.autoreverses = NO;
animation.removedOnCompletion = NO;
animation.fromValue = [NSNumber numberWithFloat:1.0];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.beginTime = time;
// animation.beginTime = AVCoreAnimationBeginTimeAtZero;
ann.startLayer.opacity = 0.0;
ann.startLayer.masksToBounds = YES;
[ann.startLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.startLayer];
ann.typeLayer.opacity = 0.0;
ann.typeLayer.masksToBounds = YES;
[ann.typeLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.typeLayer];
ann.detailLayer.opacity = 0.0;
ann.detailLayer.masksToBounds = YES;
[ann.detailLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.detailLayer];
}
videoComposition = [AVMutableVideoComposition videoCompositionWithPropertiesOfAsset:movie];
videoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:animationLayer];
}
return videoComposition;
}
I want to stress that the video outputs correctly and I do get the layers appearing at the right time, just not ALL the layers. Very confused and would greatly appreciate your help.
So I was fiddling around trying to figure out what might have caused this and it turns out that it was caused by the layers hidden property being set to YES. By setting it to NO, all the layers appear but then they never went away. So I had to change the animation's autoreverses property to YES and halve the duration.
So I changed the code to this:
while (ann = (Annotation *)[enumerator nextObject]){
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.duration = 1.5; // TODO: Three seconds is the currently hard-coded display length for an annotation, should this be a configurable option after the demo?
animation.repeatCount = 0;
animation.autoreverses = YES; // This causes the animation to run forwards then backwards, thus doubling the duration, that's why a 3-second period is using 1.5 as duration
animation.removedOnCompletion = NO;
animation.fromValue = [NSNumber numberWithFloat:1.0];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.beginTime = time;
// animation.beginTime = AVCoreAnimationBeginTimeAtZero;
ann.startLayer.hidden = NO;
ann.startLayer.opacity = 0.0;
ann.startLayer.masksToBounds = YES;
[ann.startLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.startLayer];
ann.typeLayer.hidden = NO;
ann.typeLayer.opacity = 0.0;
ann.typeLayer.masksToBounds = YES;
[ann.typeLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.typeLayer];
ann.detailLayer.hidden = NO;
ann.detailLayer.opacity = 0.0;
ann.detailLayer.masksToBounds = YES;
[ann.detailLayer addAnimation:animation forKey:#"animateOpacity"];
[animationLayer addSublayer:ann.detailLayer];
}

How to Change NSTableColumn width with Animation

I have a NSScrollView and a NSTableView with some NSTableColumn in it.
When I push a button,I change the NSScrollView'width with Animation.
Here is my code:
NSDictionary *myScrollViewDictionary = [[NSDictionary alloc] initWithObjectsAndKeys:
myScrollView,NSViewAnimationTargetKey,
[NSValue valueWithRect:myScrollViewStartFrame],NSViewAnimationStartFrameKey,
[NSValue valueWithRect:myScrollViewEndFrame],NSViewAnimationEndFrameKey, nil];
NSViewAnimation *animation = [[NSViewAnimation alloc] initWithViewAnimations:#[myScrollViewDictionary]];
animation.delegate = self;
animation.duration = 0.2;
[animation setAnimationBlockingMode:NSAnimationBlocking];
[animation startAnimation];
This works fine.
But I want to change each Column'width in the meanwhile with the same Animation,the problem is NSTableColumn is not a NSView, so I can not change its frame.Now I only can change Column'width without any Animation.
somebody have ideas?
I'm afraid you would probably have to create your own custom animation that does everything manually. To animate the column, here is a general idea:
- (IBAction)animateColumn:(id)sender
{
NSTableColumn *tableColumn; // = set your table column
CGFloat initialWidth = [tableColumn width];
CGFloat finalWidth; // = whatever
dispatch_async(dispatch_get_global_queue(0,0), {
double animationTime = 2; //seconds
double timeElapsed = 0; //approximate measure; probably good enough
while (timeElapsed<animationTime)
{
double t = timeElapsed/animationTime;
CGFloat width = t*finalWidth + (1-t)*initialWidth;
dispatch_async(dispatch_get_main_queue(), ^{
[tableColumn setWidth:width];
});
NSDate *future = [NSDate dateWithTimeIntervalSinceNow:0.02];
[NSThread sleepUntilDate:future];
timeElapsed += 0.02;
}
dispatch_async(dispatch_get_main_queue(), ^{
[tableColumn setWidth:finalWidth];
});
});
}
I guess you can adapt this to do what you want. Perhaps you can start this animation at the same time you start the other animation (using the same time for both, changing 2 to 0.2 in my example).

add an animation to bar plot does not take effect visually

I am a newer to core plot. And recently I decide to add bar growth animation to my plot. so I followed the instruction searched here to implement the animation. Now I come up with the problem: the animation seemed not to take effect.The delegate: didStopAnimate:(BOOL) returns no matter how long I set the duration of the animation with flag false.
One more thing to state: in the graph view, I put a scrollview under the real graph hosting view. does that affect Core animation to work?
the plot appeared after a navigation from the table view to itself. here is some of my codes:
if ([plot_type isEqualToString:#"bar"]) {
CPTBarPlot *plot = [[CPTBarPlot alloc] init];
//id
plot.identifier = [d objectForKey:#"title"];
plot.title = [d objectForKey:#"title"];
plot.lineStyle = barLineStyle;
plot.barCornerRadius = 1.2;
//set color
CPTGradient *gradient = [CPTGradient gradientWithBeginningColor:[CPTColor colorWithCGColor:color_begin] endingColor:[CPTColor colorWithCGColor:color_end]];
gradient.angle = 90.0f;
CPTFill *fill = [CPTFill fillWithGradient:gradient];
plot.fill = fill;
//bar width
double width = [[d objectForKey:#"width"] doubleValue]==0.0?0.6:[[d objectForKey:#"width"] doubleValue];
plot.barWidth = CPTDecimalFromFloat(width);
plot.barsAreHorizontal = NO;
//need manually stacked
if (need_stack && !is_first) {
plot.barBasesVary = YES;
} else {
plot.barBasesVary = NO;
}
//data source
plot.dataSource = self;
//delegate
plot.delegate = self;
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
[anim setDuration:2.0f];
anim.toValue = [NSNumber numberWithFloat:1];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
anim.removedOnCompletion = NO;
anim.delegate = self;
anim.fillMode = kCAFillModeForwards;
[plot addAnimation:anim forKey:(NSString *)plot.identifier];
[graph addPlot:plot toPlotSpace:plotSpace];
}
Thanks for your helps in advance.
The transform property of a CALayer is CATransform3D struct, not a scalar value. You should wrap it using the NSValue method +valueWithCATransform3D:.