AVVideoCompositionCoreAnimationTool not adding all CALayers - objective-c

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];
}

Related

iOS: I want to continuously rotate an image, but it doesn't work

Have the code from several examples on stackoverflow. But my image doesn't rotate..any idea why? It is displayed on the screen, but fixed..
-(UIImageView *)movablePropeller
{
if(!_movablePropeller){
_movablePropeller = [[UIImageView alloc] initWithFrame:self.frame];
_movablePropeller.image = [UIImage imageNamed:#"MovablePropeller"];
[self addSubview:_movablePropeller];
}
return _movablePropeller;
}
- (void)startPropeller
{
CABasicAnimation *rotation;
rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotation.fromValue = [NSNumber numberWithFloat:0.0f];
rotation.toValue = [NSNumber numberWithFloat:(2 * M_PI)];
rotation.cumulative = true;
rotation.duration = 0.7f; // Speed
rotation.repeatCount = INFINITY; // Repeat forever. Can be a finite number.
[self.movablePropeller.layer removeAllAnimations];
[self.movablePropeller.layer addAnimation:rotation forKey:#"Spin"];
}
(and of course I would love to have it in a seperate thread, not to block the UI)

Change CALayer color while animating

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.

Animate Mask in Objective C

I am making an application where I have to show 3 images in a single screen, and when User touch one image then that Image should be shown by Animating and resizing.
So for 1st Step I did Masking of 3 images with 3 different Transparent Mask Image from
How to Mask an UIImageView
And my method is like below
- (UIImageView*) maskedImage:(UIImage *)image withMasked:(UIImage *)maskImage {
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
CALayer *mask1 = [CALayer layer];
mask1.contents = (id)[maskImage CGImage];
mask1.frame = CGRectMake(0, 0, 1024, 768);
imageView.layer.mask = mask1;
imageView.layer.masksToBounds = YES;
return imageView;
}
And I am calling this as
self.image2 = [UIImage imageNamed:#"screen2Full.png"];
self.mask2 = [UIImage imageNamed:#"maskImage.png"];
self.imageView2 = [self maskedImage:self.image2 withMasked:self.mask2];
[self.completeSingleView addSubview:self.imageView2];
This is working perfectly.
Now for Step 2. Animate the Mask, so that Full Image can be shown. I have googled a lot about this but didn't get success. So Please help me. I just want to know how can we Animate and Resize the Mask Image So that I can view a full Image. My concept is as below.
So finally i got the solution from multiple sites and some own logic and hard work obviously ;). So here is the solution of this
-(void)moveLayer:(CALayer*)layer to:(CGPoint)point
{
// Prepare the animation from the current position to the new position
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [layer valueForKey:#"position"];
animation.toValue = [NSValue valueWithCGPoint:point];
animation.duration = .3;
layer.position = point;
[layer addAnimation:animation forKey:#"position"];
}
-(void)resizeLayer:(CALayer*)layer to:(CGSize)size
{
CGRect oldBounds = layer.bounds;
CGRect newBounds = oldBounds;
newBounds.size = size;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"bounds"];
animation.fromValue = [NSValue valueWithCGRect:oldBounds];
animation.toValue = [NSValue valueWithCGRect:newBounds];
animation.duration = .3;
layer.bounds = newBounds;
[layer addAnimation:animation forKey:#"bounds"];
}
- (UIImageView*) animateMaskImage:(UIImage *)image withMask:(UIImage *)maskImage width:(int )wd andHeight:(int )ht andSize:(CGSize)size andPosition:(CGPoint)pt {
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
CALayer *mask = [CALayer layer];
mask.contents = (id)[maskImage CGImage];
mask.frame = CGRectMake(0, 0, wd, ht);
//This needs to be place before masking. I don't knwo why. But its working :)
[self resizeLayer:mask to:size];
[self moveLayer:mask to:pt];
imageView.layer.mask = mask;
imageView.layer.masksToBounds = YES;
return imageView;
}
You just need to call this as
[self.imageView1 removeFromSuperview];
self.imageView1 = [self animateMaskedImage:self.image1 withMasked:self.mask1 width:1024 andHeight:768 andSize:CGSizeMake(1024*4, 768*4) andPosition:CGPointMake(1024*2, 768*2)];
[self.completeSingleView addSubview:self.imageView1];

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:.