Layer size/bounds during a core animation - macos

I am working on a Mac app for Mac OS X 10.6+ and need to redraw the contents of a CAOpenGLLayer while an animation is occurring. I think I've read up on all the necessary parts, but it is just not working for me. I set up the animation as follows:
[CATransaction setAnimationDuration:SLIDE_DURATION];
CABasicAnimation *drawAnim = [CABasicAnimation animationWithKeyPath:#"drawIt"];
[drawAnim setFromValue:[NSNumber numberWithFloat:0]];
[drawAnim setToValue:[NSNumber numberWithFloat:1]];
[drawAnim setDuration:SLIDE_DURATION];
[chartView.chartViewLayer addAnimation:drawAnim forKey:#"drawIt"];
chartFrame.origin.x += controlFrame.size.width;
chartFrame.size.width -= controlFrame.size.width;
chartView.chartViewLayer.frame = CGRectMake(chartFrame.origin.x,
chartFrame.origin.y,
chartFrame.size.width,
chartFrame.size.height);
The drawIt property is a custom property whose only purpose is to be called to draw the layer during the successive animation frames. It order to get this to work, you have to add this to the chartViewLayer's class:
+ (BOOL)needsDisplayForKey:(NSString *)key
{
if ([key isEqualToString:#"drawIt"])
{
return YES;
}
else
{
return [super needsDisplayForKey:key];
}
}
So this seems to all be working fine. However, I need to get the current (animated) size of the layer before drawing it. I've found various conflicting information on how to get this out of the presentation layer. Here is what I've tried when the layer's:
drawInCGLContext:(CGLContextObj)glContext
pixelFormat:(CGLPixelFormatObj)pixelFormat
forLayerTime:(CFTimeInterval)timeInterval
displayTime:(const CVTimeStamp *)timeStamp
method is called during the animation. I've tried getting the size using KVC, and by querying either the frame or the bounds.
CALayer *presentationLayer = [chartViewLayer presentationLayer];
//bounds.size.width = [[[chartViewLayer presentationLayer]
// valueForKeyPath:#"frame.size.width"] intValue];
//bounds.size.height = [[[chartViewLayer presentationLayer]
// valueForKeyPath:#"frame.size.height"] intValue];
//bounds.size = presentationLayer.bounds.size;
bounds.size = presentationLayer.frame.size;
NSLog(#"Size during animation: %f, %f", bounds.size.width, bounds.size.height);
In all cases, the returned value is the end result of the animation rather than the transitional values. My understanding is that using the presentationLayer should give the transitional values.
So is this just broken or am I missing some critical step? Thanks for any help.

Related

Layer backed NSView problems in viewDidLoad

I set off to transform an NSImageView. My initially attempt was
self.imageView.wantsLayer = YES;
self.imageView.layer.transform = CATransform3DMakeRotation(1,1,1,1);
Unfortunately I noticed that the transform only happens sometimes (maybe once every 5 runs). Adding an NSLog between confirmed that on some runs self.imageView.layer is null. State of the whole project is shown on the image below.
It's an incredibly simple 200x200 NSImageView with an outlet to a generated NSViewController. Some experimentation showed settings wantsDisplay doesn't fix the problem, but putting the transform on an NSTimer makes it work every-time. I'd love an explanation why this happens (I presume it's due to some race condition).
I'm using Xcode 8 on the macOS 10.12 but I doubt this is the cause of the problem.
Update
Removing wantsLayer and madly enabling Core Animation Layers in Interface Builder did not fix the problem.
Neither did attempts to animate it (I wasn't sure what I was hoping for)
// Sometimes works.. doesn't animate
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.duration = 1;
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
} completionHandler:^{
NSLog(#"Done");
}];
or
// Animates but only sometimes
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(1,1,1,1)];
animation.duration = 1;
[self.imageView.layer addAnimation:animation forKey:nil];
After experimenting with allowsImplicitAnimation I realised I might be trying to animate too early.
Moving the transform code into viewDidAppear made it work every time.
- (void)viewDidAppear {
[super viewDidAppear];
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
}

IKImageBrowserView with background and wantsLayer

In my app I use IKImageBrowserView with background. If wantsLayer NO - all fine and IKImageBrowserView look like nice. But if I enabled wantsLayer (in parent view) the background in IKImageBrowserView is corrupt. (Sorry English is not my native language and I can't find the correct word).
If I understand correctly, problem in this fragment. But I can't see where.
NSRect visibleRect = [owner visibleRect];
NSRect bounds = [owner bounds];
CGImageRef image = NULL;
NSString *path = [[NSBundle mainBundle] pathForResource:[#"metal_background.tif" stringByDeletingPathExtension] ofType:[#"metal_background.tif" pathExtension]];
if (!path) {
return;
}
CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)[NSURL fileURLWithPath:path], NULL);
if (!imageSource) {
return;
}
image = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
if (!image) {
CFRelease(imageSource);
return;
}
float width = (float) CGImageGetWidth(image);
float height = (float) CGImageGetHeight(image);
//compute coordinates to fill the view
float left, top, right, bottom;
top = bounds.size.height - NSMaxY(visibleRect);
top = fmod(top, height);
top = height - top;
right = NSMaxX(visibleRect);
bottom = -height;
// tile the image and take in account the offset to 'emulate' a scrolling background
for (top = visibleRect.size.height-top; top>bottom; top -= height){
for(left=0; left<right; left+=width){
CGContextDrawImage(context, CGRectMake(left, top, width, height), image);
}
}
CFRelease(imageSource);
CFRelease(image);
Image with problem
Image without problem
Thanks
I tried using IKImageView a few weeks ago and ran in to a number if problems. It's a nice class but it doesn't seem to have been updated in a long time. I doesn't support retina screen and, as you point out goes crazy when you add a layer.
I wanted to do some custom drawing with CALayer over the top of an image. The first thing I tried was to add a layer to the image view, which gave me problems. The solution I went for want to add a custom NSView subview to the image view, add auto layout constraints so that it always the same size, then add a layer hosting view to the subview. This worked fine, so move your drawing code to a custom subview and it should work.
IKImageView
LayerHostingView (<--- add CALayer here)
I ran in to this distorted background image problem as well and it turned out to be that I had set up the CALayer for the background incorrectly. I suspect that you are misunderstanding the cause to be setting wantsLayer to false because when that is set the layer does not immediately redraw new layers that you set.
I reached this conclusion as if I set up the layer correctly, then set wantsLayer false immediately before setting a bad CALayer in the next line of code I see the correct, undistorted image first and only when I scroll (and it redraws) do I see the buggy effects in your screenshot. However if I set wantsLayer true at the same point in code I see the buggy graphics immediately when the browser loads, without having to redraw first.
You can move the wantsLayer false statement further down your code to find the line that's breaking it, it'll be where the layer is reset/updated somewhere.

Resizing an NSView mid-animation

I have a normal NSView that is resizable by dragging the window edges.
If the view is resized during an [NSView animator] animation, it continues to animate to the final size of the original animation, but does not take into account the new window size.
Here is a simple example project. Double click to begin the animation, then resize the window before it finishes.
What is the best way to make the animation take account of the new frame size?
IMHO, the best way would be to stop the animation as soon as the resizing phase begins.
During the resize phase, the user is in control and sets the size of the window manually.
When the resizing phase ends, the window is already set to the desired size, so there is non need to do more.
This kind of problem is best solved with an NSTimer instead of the animator function:
Let the timer call a function repeatedly, until the animation is "complete".
Once complete, end the timer (invalidate).
The function to be called repeatedly in each loop grabs the actual framesize of the window and the actual framesize of your view and simply adds the third of the difference of the two to the frame of the view, like:
frame.size.height += diffHeight/3.0;
So, no matter what happens, the view grows or shrinks closer and closer to its destination.
Once the abs(of the difference) is less then e.g. 0.2 you set the view directly to the desired size and end the timer.
This is direct, uses only little code and you need not listen to any events while it performs pretty well. :-)
Here are the critical codes to initiallize the animation (timer must be an instance of your class):
if(timer)return;
timer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(resizeView:) userInfo:[NSNumber numberWithBool:status] repeats:YES];
[timer setTolerance:0.02];
I use the word status instead of your word closed, the function to be repeatedly called might then look somewhat like:
- (void)resizeView:(id)userInfo;
{
BOOL status = [(NSNumber *)[userInfo userInfo] boolValue];
double startwid,stopwid;
NSRect newSizeRect = [[self window] frame];
stopwid = newSizeRect.size.width;
if(status){
stopwid -= 100.0;
}
NSRect cbgRect = [self frame];
startwid = cbgRect.size.width;
double diff = stopwid-startwid;
if(fabs(diff)<0.2){
diff = 0;
startwid = stopwid;
[timer invalidate];
timer = nil;
//NSLog(#"stop");
}
//NSLog(#"%f - %f = %f /10 = %f",stopwid,startwid,diff,diff/3.0);
cbgRect.size.width = startwid+diff/3.0;
[self setFrame:cbgRect];
}

Explicit animation of NSView using core animation

I'm trying to slide in a NSView using core animation. I think I need to use explicit animation rather than relying on something like [[view animator] setFrame:newFrame]. This is mainly because I need to set the animation delegate in order to take action after the animation is finished.
I have it working just fine using the animator, but as I said, I need to be notified when the animation finishes. My code currently looks like:
// Animate the controlView
NSRect viewRect = [controlView frame];
NSPoint startingPoint = viewRect.origin;
NSPoint endingPoint = startingPoint;
endingPoint.x += viewRect.size.width;
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
CABasicAnimation *controlPosAnim = [CABasicAnimation animationWithKeyPath:#"position"];
[controlPosAnim setFromValue:[NSValue valueWithPoint:startingPoint]];
[controlPosAnim setToValue:[NSValue valueWithPoint:endingPoint]];
[controlPosAnim setDelegate:self];
[[controlView layer] addAnimation:controlPosAnim forKey:#"controlViewPosition"];
This visually works (and I get notified at the end) but it looks like the actual controlView doesn't get moved. If I cause the window to refresh, the controlView disappears. I tried replacing
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
with
[controlView setFrame:newFrame];
and that does cause the view (and layer) to move, but it is corrupting something such that my app dies with a seg fault soon afterwards.
Most of the examples of explicit animation seem to only be moving a CALayer. There must be a way to moving the NSView and also being able to set a delegate. Any help would be appreciated.
Changes made to views take effect at the end of the current run loop. The same goes for any animations applied to layers.
If you animate a view's layer, the view itself is unaffected which is why the view appears to jump back to its original position when the animation completes.
With these two things in mind, you can get the effect you want by setting the view's frame to what you want it to be when the animation is done and then adding an explicit animation to the view's layer.
When the animation begins, it moves the view to the starting position, animates it to the end position and when the animation is done, the view has the frame you specified.
- (IBAction)animateTheView:(id)sender
{
// Calculate start and end points.
NSPoint startPoint = theView.frame.origin;
NSPoint endPoint = <Some other point>;
// We can set the frame here because the changes we make aren't actually
// visible until this pass through the run loop is done.
// Furthermore, this change to the view's frame won't be visible until
// after the animation below is finished.
NSRect frame = theView.frame;
frame.origin = endPoint;
theView.frame = frame;
// Add explicit animation from start point to end point.
// Again, the animation doesn't start immediately. It starts when this
// pass through the run loop is done.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithPoint:startPoint]];
[animation setToValue:[NSValue valueWithPoint:endPoint]];
// Set any other properties you want, such as the delegate.
[theView.layer addAnimation:animation forKey:#"position"];
}
Of course, for this code to work you need to make sure both your view and its superview have layers. If the superview doesn't have a layer, you'll get corrupted graphics.
I think you need to call the setPosition at the end (after setting the animation).
Also, I don't think you should animate explicitely the layer of the view, but instead the view itself by using animator and setting the animations. You can use delegates too with animator :)
// create controlPosAnim
[controlView setAnimations:[NSDictionary dictionaryWithObjectsAndKeys:controlPosAnim, #"frameOrigin", nil]];
[[controlView animator] setFrame:newFrame];

CALayer scroll view slowdown with many items

Hey, I'm having a performance problem with CALayers in a layer backed NSView inside an NSScrollView. I've got a scroll view that I populate with a bunch of CALayer instances, stacked one on top of the next. Right now all I am doing is putting a border around them so I can see them. There is nothing else in the layer.
Performance seems fine until I have around 1500 or so in the scroll view. When I put 1500 in, the performance is great as I scroll down the list, until I get to around item 1000. Then very suddenly the app starts to hang. It's not a gradual slowdown which is what I would expect if it was just reaching it's capacity. It's like the app hits a brick wall.
When I call CGContextFillRect in the draw method of the layers, the slowdown happens around item 300. I'm assuming this has something to do with maybe the video card memory filling up or something? Do I need to do something to free the resources of the CALayers when they are offscreen in my scroll view?
I've noticed that if I don't setNeedsDisplay on my layers, I can get to the end of 1500 items without slowdowns. This is not a solution however, as I have some custom drawing that I must perform in the layer. I'm not sure if that solves the problem, or just makes it show up with a greater number of items in the layer. Ideally I would like this to be fully scalable with thousands of items in the scroll view (within reason of course). Realistically, how many of these empty items should I expect to be able to display in this way?
#import "ShelfView.h"
#import <Quartz/Quartz.h>
#implementation ShelfView
- (void) awakeFromNib
{
CALayer *rootLayer = [CALayer layer];
rootLayer.layoutManager = self;
rootLayer.geometryFlipped = YES;
[self setLayer:rootLayer];
[self setWantsLayer:YES];
int numItemsOnShelf = 1500;
for(NSUInteger i = 0; i < numItemsOnShelf; i++) {
CALayer* shelfItem = [CALayer layer];
[shelfItem setBorderColor:CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0)];
[shelfItem setBorderWidth:1];
[shelfItem setNeedsDisplay];
[rootLayer addSublayer:shelfItem];
}
[rootLayer setNeedsLayout];
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
float y = 10;
int totalItems = (int)[[layer sublayers] count];
for(int i = 0; i < totalItems; i++)
{
CALayer* item = [[layer sublayers] objectAtIndex:i];
CGRect frame = [item frame];
frame.origin.x = self.frame.size.width / 2 - 200;
frame.origin.y = y;
frame.size.width = 400;
frame.size.height = 400;
[CATransaction begin];
[CATransaction setAnimationDuration:0.0];
[item setFrame:CGRectIntegral(frame)];
[CATransaction commit];
y += 410;
}
NSRect thisFrame = [self frame];
thisFrame.size.height = y;
if(thisFrame.size.height < self.superview.frame.size.height)
thisFrame.size.height = self.superview.frame.size.height;
[self setFrame:thisFrame];
}
- (BOOL) isFlipped
{
return YES;
}
#end
I found out it was because I was filling each layer with custom drawing, they seemed to all be cached as separate images, even though they shared a lot of common data, so I switched to just creating a dozen CALayers, filling their "contents" property, and adding them as sublayers to a main layer. This seemed to make things MUCH zippier.

Resources