Animation with CALayer?
-
Hi there,
Hopefully this is the right list for this question. I haven't found
what I was looking for after searching the archives for the "right"
way to animate some graphics using cocoa.
I'm trying to draw two different things. One of them is an audio
level meter and the other is an effect similar to what the iPhone
"slide" graphic does on the lock screen on the iPhone.
(I have a mask that I slide a white blob around behind.)
I'm able to do both with an NSTimer that fires and then I issue a
[setNeedsDisplay] from the timer to update the animation. This
works fine and produces the expected results but seems to use way too
much CPU compared to what I've been able to do "back in the day"
using straight CG or worse Quickdraw. (I am not porting this code
from QD- I threw that code away, this is all new code.)
I've been looking at using CALayers to implement the drawing so that
I can call [display] rather than [setNeedsDisplay] with the hopes
that this would make the animations use less CPU.
What is the right way to approach this problem? (and please don't
suggest using Shark to optimize my code as that is a given and will
be done after I determine what the best course of code will be).
thanks,
alex -
Hi Alex,
I have an example that was going to be part of the book but ended up
not fitting in the flow of things (although I might end up adding it
back) that shows how to animate something like your audio level meter.
The app animates the width of a line changing but the secret sauce can
be applied to the length of the audio meter. The secret sauce, 1 make
sure to call setNeedsDisplay: when the audio value changes, 2
implement the defaultAnimationForKey: method to return the animation
you want used with the audio value changes;
- (void)setDrawnLineWidth:(float)value {
[self willChangeValueForKey:@"drawnLineWidth"];
drawnLineWidth = value;
[self.path setLineWidth:drawnLineWidth];
[self setNeedsDisplay:YES];
[self didChangeValueForKey:@"drawnLineWidth"];
}
+ (id)defaultAnimationForKey:(NSString *)key {
static CABasicAnimation *drawnLineWidthBasicAnimation = nil;
if ([key isEqualToString:@"drawnLineWidth"]) {
if (drawnLineWidthBasicAnimation == nil) {
drawnLineWidthBasicAnimation = [[CABasicAnimation alloc]
init];
}
return drawnLineWidthBasicAnimation;
} else {
return [super defaultAnimationForKey:key];
}
}
Then when you change the audio level do something like this;
- (IBAction)setWidth:(id)sender {
[[self animator] setDrawnLineWidth:[sender floatValue]];
}
This example uses simple cocoa stuff that you already know (except for
the defaultAnimationForKey: method) so its quicker uptake than getting
your head wrapped around drawing with CA (which is conceptually
similar to appkit drawing but in details its quite different). If you
want or need to go that route keep in mind that layers do not clip
their sublayers by default and that you should call display only when
the layer needs to be redrawn not during animation. The whole
animation is done via the presentation and render tree layers and if
you call display a lot it will kill performance because its got to
push the content down onto the render layer too often.
Animating the slider could use a similar approach and look a lot like
the 'turn on time machine' button in the time machine prefs panel.
HTH,
-bd-
http://bill.dudney.net/roller/objc
On Feb 5, 2008, at 1:30 PM, alex wrote:
> Hi there,
>
> Hopefully this is the right list for this question. I haven't found
> what I was looking for after searching the archives for the "right"
> way to animate some graphics using cocoa.
>
> I'm trying to draw two different things. One of them is an audio
> level meter and the other is an effect similar to what the iPhone
> "slide" graphic does on the lock screen on the iPhone.
> (I have a mask that I slide a white blob around behind.)
>
> I'm able to do both with an NSTimer that fires and then I issue a
> [setNeedsDisplay] from the timer to update the animation. This
> works fine and produces the expected results but seems to use way
> too much CPU compared to what I've been able to do "back in the day"
> using straight CG or worse Quickdraw. (I am not porting this code
> from QD- I threw that code away, this is all new code.)
>
> I've been looking at using CALayers to implement the drawing so that
> I can call [display] rather than [setNeedsDisplay] with the hopes
> that this would make the animations use less CPU.
>
> What is the right way to approach this problem? (and please don't
> suggest using Shark to optimize my code as that is a given and will
> be done after I determine what the best course of code will be).
>
> thanks,
> alex
-
Hi there,
Thanks for this answer- From what I see below this looks like a
technique to animate the change from one value to another (and the
result being the width of the line animating).
I'm not sure if this will work for me- I was really looking for a
way to simply draw a rectangle that represents the meter level. I
don't want to animate this per se. Just the act of drawing the
rectangle repeatedly is the animation. This gets called periodically
and ideally should not interfere with user interaction. What I have
now works but the CPU usage is too high for me and I worry that this
will become more of an issue later. My testing has led me to blame
calling setNeedsDisplay, then waiting for the event loop, then the
drawRect call (and all the setup work around those calls).. All that
seems too slow and complex compared to what I "used to" do...
What I want to do is have a timer or thread that looks at a value and
draws a rectangle to represent it. Effectively I want to grab a
CGContextRef and draw to it periodically without all the repeated
setup work- for instance, create a transparent overlay view, grab
its CGContextRef and then draw when I need to (outside the event
loop). Is this still possible? Or recommended? I promise I won't
draw more often than the refresh rate. :)
How does Core Animation do its thing without hogging the CPU I wonder?
thanks again!
alex
On Feb 5, 2008, at 12:53 PM, Bill Dudney wrote:
> Hi Alex,
>
> I have an example that was going to be part of the book but ended
> up not fitting in the flow of things (although I might end up
> adding it back) that shows how to animate something like your audio
> level meter. The app animates the width of a line changing but the
> secret sauce can be applied to the length of the audio meter. The
> secret sauce, 1 make sure to call setNeedsDisplay: when the audio
> value changes, 2 implement the defaultAnimationForKey: method to
> return the animation you want used with the audio value changes;
>
> - (void)setDrawnLineWidth:(float)value {
> [self willChangeValueForKey:@"drawnLineWidth"];
> drawnLineWidth = value;
> [self.path setLineWidth:drawnLineWidth];
> [self setNeedsDisplay:YES];
> [self didChangeValueForKey:@"drawnLineWidth"];
> }
>
> + (id)defaultAnimationForKey:(NSString *)key {
> static CABasicAnimation *drawnLineWidthBasicAnimation = nil;
> if ([key isEqualToString:@"drawnLineWidth"]) {
> if (drawnLineWidthBasicAnimation == nil) {
> drawnLineWidthBasicAnimation = [[CABasicAnimation
> alloc] init];
> }
> return drawnLineWidthBasicAnimation;
> } else {
> return [super defaultAnimationForKey:key];
> }
> }
>
> Then when you change the audio level do something like this;
>
> - (IBAction)setWidth:(id)sender {
> [[self animator] setDrawnLineWidth:[sender floatValue]];
> }
>
> This example uses simple cocoa stuff that you already know (except
> for the defaultAnimationForKey: method) so its quicker uptake than
> getting your head wrapped around drawing with CA (which is
> conceptually similar to appkit drawing but in details its quite
> different). If you want or need to go that route keep in mind that
> layers do not clip their sublayers by default and that you should
> call display only when the layer needs to be redrawn not during
> animation. The whole animation is done via the presentation and
> render tree layers and if you call display a lot it will kill
> performance because its got to push the content down onto the
> render layer too often.
>
> Animating the slider could use a similar approach and look a lot
> like the 'turn on time machine' button in the time machine prefs
> panel.
>
> HTH,
>
> -bd-
> http://bill.dudney.net/roller/objc
>
> On Feb 5, 2008, at 1:30 PM, alex wrote:
>
>> Hi there,
>>
>> Hopefully this is the right list for this question. I haven't
>> found what I was looking for after searching the archives for the
>> "right" way to animate some graphics using cocoa.
>>
>> I'm trying to draw two different things. One of them is an audio
>> level meter and the other is an effect similar to what the iPhone
>> "slide" graphic does on the lock screen on the iPhone.
>> (I have a mask that I slide a white blob around behind.)
>>
>> I'm able to do both with an NSTimer that fires and then I issue a
>> [setNeedsDisplay] from the timer to update the animation. This
>> works fine and produces the expected results but seems to use way
>> too much CPU compared to what I've been able to do "back in the
>> day" using straight CG or worse Quickdraw. (I am not porting this
>> code from QD- I threw that code away, this is all new code.)
>>
>> I've been looking at using CALayers to implement the drawing so
>> that I can call [display] rather than [setNeedsDisplay] with the
>> hopes that this would make the animations use less CPU.
>>
>> What is the right way to approach this problem? (and please don't
>> suggest using Shark to optimize my code as that is a given and
>> will be done after I determine what the best course of code will be).
>>
>> thanks,
>> alex
>
-
On Feb 6, 2008, at 9:27 AM, alex wrote:
> Hi there,
>
> Thanks for this answer- From what I see below this looks like a
> technique to animate the change from one value to another (and the
> result being the width of the line animating).
>
> I'm not sure if this will work for me- I was really looking for a
> way to simply draw a rectangle that represents the meter level. I
> don't want to animate this per se. Just the act of drawing the
> rectangle repeatedly is the animation. This gets called periodically
> and ideally should not interfere with user interaction. What I have
> now works but the CPU usage is too high for me and I worry that this
> will become more of an issue later. My testing has led me to blame
> calling setNeedsDisplay, then waiting for the event loop, then the
> drawRect call (and all the setup work around those calls).. All
> that seems too slow and complex compared to what I "used to" do...
>
> What I want to do is have a timer or thread that looks at a value
> and draws a rectangle to represent it. Effectively I want to grab a
> CGContextRef and draw to it periodically without all the repeated
> setup work- for instance, create a transparent overlay view, grab
> its CGContextRef and then draw when I need to (outside the event
> loop). Is this still possible? Or recommended? I promise I won't
> draw more often than the refresh rate. :)
Yes, it's quite possible. If you just want a bar display that
immediately updates each time the value changes, you can lock focus on
the view and redraw immediately each time the value changes. e.g.:
if ([view lockFocusIfCanDraw]) {
NSRect bounds = [view bounds];
CGFloat y = bounds.origin.y + (level value from 0.0 to 1.0) *
bounds.size.height;
[[NSColor blackColor] set];
NSRectFill(bounds));
[[NSColor greenColor] set];
NSRectFill(NSMakeRect(bounds.origin.x, bounds.origin.y,
bounds.size.width, y - bounds.origin.y));
[view unlockFocus];
}
> How does Core Animation do its thing without hogging the CPU I wonder?
By virtue of a background render thread, that offloads much work to
the GPU.
Troy
> On Feb 5, 2008, at 12:53 PM, Bill Dudney wrote:
>
>> Hi Alex,
>>
>> I have an example that was going to be part of the book but ended
>> up not fitting in the flow of things (although I might end up
>> adding it back) that shows how to animate something like your audio
>> level meter. The app animates the width of a line changing but the
>> secret sauce can be applied to the length of the audio meter. The
>> secret sauce, 1 make sure to call setNeedsDisplay: when the audio
>> value changes, 2 implement the defaultAnimationForKey: method to
>> return the animation you want used with the audio value changes;
>>
>> - (void)setDrawnLineWidth:(float)value {
>> [self willChangeValueForKey:@"drawnLineWidth"];
>> drawnLineWidth = value;
>> [self.path setLineWidth:drawnLineWidth];
>> [self setNeedsDisplay:YES];
>> [self didChangeValueForKey:@"drawnLineWidth"];
>> }
>>
>> + (id)defaultAnimationForKey:(NSString *)key {
>> static CABasicAnimation *drawnLineWidthBasicAnimation = nil;
>> if ([key isEqualToString:@"drawnLineWidth"]) {
>> if (drawnLineWidthBasicAnimation == nil) {
>> drawnLineWidthBasicAnimation = [[CABasicAnimation alloc]
>> init];
>> }
>> return drawnLineWidthBasicAnimation;
>> } else {
>> return [super defaultAnimationForKey:key];
>> }
>> }
>>
>> Then when you change the audio level do something like this;
>>
>> - (IBAction)setWidth:(id)sender {
>> [[self animator] setDrawnLineWidth:[sender floatValue]];
>> }
>>
>> This example uses simple cocoa stuff that you already know (except
>> for the defaultAnimationForKey: method) so its quicker uptake than
>> getting your head wrapped around drawing with CA (which is
>> conceptually similar to appkit drawing but in details its quite
>> different). If you want or need to go that route keep in mind that
>> layers do not clip their sublayers by default and that you should
>> call display only when the layer needs to be redrawn not during
>> animation. The whole animation is done via the presentation and
>> render tree layers and if you call display a lot it will kill
>> performance because its got to push the content down onto the
>> render layer too often.
>>
>> Animating the slider could use a similar approach and look a lot
>> like the 'turn on time machine' button in the time machine prefs
>> panel.
>>
>> HTH,
>>
>> -bd-
>> http://bill.dudney.net/roller/objc
>>
>> On Feb 5, 2008, at 1:30 PM, alex wrote:
>>
>>> Hi there,
>>>
>>> Hopefully this is the right list for this question. I haven't
>>> found what I was looking for after searching the archives for the
>>> "right" way to animate some graphics using cocoa.
>>>
>>> I'm trying to draw two different things. One of them is an audio
>>> level meter and the other is an effect similar to what the iPhone
>>> "slide" graphic does on the lock screen on the iPhone.
>>> (I have a mask that I slide a white blob around behind.)
>>>
>>> I'm able to do both with an NSTimer that fires and then I issue a
>>> [setNeedsDisplay] from the timer to update the animation. This
>>> works fine and produces the expected results but seems to use way
>>> too much CPU compared to what I've been able to do "back in the
>>> day" using straight CG or worse Quickdraw. (I am not porting this
>>> code from QD- I threw that code away, this is all new code.)
>>>
>>> I've been looking at using CALayers to implement the drawing so
>>> that I can call [display] rather than [setNeedsDisplay] with the
>>> hopes that this would make the animations use less CPU.
>>>
>>> What is the right way to approach this problem? (and please don't
>>> suggest using Shark to optimize my code as that is a given and
>>> will be done after I determine what the best course of code will
>>> be).
>>>
>>> thanks,
>>> alex
>>
--
Troy Stephens
Cocoa Frameworks
Apple, Inc. -
This is the perfect answer!! Thank you.
One more question (sorry):
How would one approach doing this exact thing with Layers? Or is
that not recommended and it should be done in the view like below?
thanks,
alex
On Feb 6, 2008, at 4:46 PM, Troy Stephens wrote:
> On Feb 6, 2008, at 9:27 AM, alex wrote:
>> Hi there,
>>
>> Thanks for this answer- From what I see below this looks like a
>> technique to animate the change from one value to another (and the
>> result being the width of the line animating).
>>
>> I'm not sure if this will work for me- I was really looking for a
>> way to simply draw a rectangle that represents the meter level. I
>> don't want to animate this per se. Just the act of drawing the
>> rectangle repeatedly is the animation. This gets called
>> periodically and ideally should not interfere with user
>> interaction. What I have now works but the CPU usage is too high
>> for me and I worry that this will become more of an issue later.
>> My testing has led me to blame calling setNeedsDisplay, then
>> waiting for the event loop, then the drawRect call (and all the
>> setup work around those calls).. All that seems too slow and
>> complex compared to what I "used to" do...
>>
>> What I want to do is have a timer or thread that looks at a value
>> and draws a rectangle to represent it. Effectively I want to grab
>> a CGContextRef and draw to it periodically without all the
>> repeated setup work- for instance, create a transparent overlay
>> view, grab its CGContextRef and then draw when I need to (outside
>> the event loop). Is this still possible? Or recommended? I
>> promise I won't draw more often than the refresh rate. :)
>
> Yes, it's quite possible. If you just want a bar display that
> immediately updates each time the value changes, you can lock focus
> on the view and redraw immediately each time the value changes. e.g.:
>
> if ([view lockFocusIfCanDraw]) {
> NSRect bounds = [view bounds];
> CGFloat y = bounds.origin.y + (level value from 0.0 to 1.0) *
> bounds.size.height;
>
> [[NSColor blackColor] set];
> NSRectFill(bounds));
> [[NSColor greenColor] set];
> NSRectFill(NSMakeRect(bounds.origin.x, bounds.origin.y,
> bounds.size.width, y - bounds.origin.y));
>
> [view unlockFocus];
> }
>
>> How does Core Animation do its thing without hogging the CPU I
>> wonder?
>
> By virtue of a background render thread, that offloads much work to
> the GPU.
>
> Troy
>
-
On Feb 6, 2008, at 7:26 PM, alex wrote
> This is the perfect answer!! Thank you.
>
> One more question (sorry):
>
> How would one approach doing this exact thing with Layers? Or is
> that not recommended and it should be done in the view like below?
This could certainly be done using layers -- but if you aren't already
using Core Animation-based rendering in your UI for other reasons, I'd
recommend sticking with the plain NSView approach, as a CA renderer is
an awfully big hammer to bring in just to draw a simple set of moving
level bars.
If you did go the layer-based route, an efficient approach would be to
create a layer for each level bar, whose content was provided as a
CGImage or as a backgroundColor (which can be a solid or pattern
color), and change the layer's bounds height (or width) when the level
value changes. To suppress the default implicit layer bounds
animation so as to get an immediate response to level changes, you'd
make the change inside a CATransaction within which you disable actions:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithBool:YES]
forKey:kCATransactionDisableActions];
levelLayer.bounds = CGRectMake(0, 0, LEVEL_BAR_WIDTH,
heightForNewLevelValue);
[CATransaction commit];
Providing the layer content as described effectively allows the redraw
work to be offloaded to the GPU, which should result in good
performance.
Troy
> On Feb 6, 2008, at 4:46 PM, Troy Stephens wrote:
>
>> On Feb 6, 2008, at 9:27 AM, alex wrote:
>>> Hi there,
>>>
>>> Thanks for this answer- From what I see below this looks like a
>>> technique to animate the change from one value to another (and the
>>> result being the width of the line animating).
>>>
>>> I'm not sure if this will work for me- I was really looking for a
>>> way to simply draw a rectangle that represents the meter level. I
>>> don't want to animate this per se. Just the act of drawing the
>>> rectangle repeatedly is the animation. This gets called
>>> periodically and ideally should not interfere with user
>>> interaction. What I have now works but the CPU usage is too high
>>> for me and I worry that this will become more of an issue later.
>>> My testing has led me to blame calling setNeedsDisplay, then
>>> waiting for the event loop, then the drawRect call (and all the
>>> setup work around those calls).. All that seems too slow and
>>> complex compared to what I "used to" do...
>>>
>>> What I want to do is have a timer or thread that looks at a value
>>> and draws a rectangle to represent it. Effectively I want to grab
>>> a CGContextRef and draw to it periodically without all the
>>> repeated setup work- for instance, create a transparent overlay
>>> view, grab its CGContextRef and then draw when I need to (outside
>>> the event loop). Is this still possible? Or recommended? I
>>> promise I won't draw more often than the refresh rate. :)
>>
>> Yes, it's quite possible. If you just want a bar display that
>> immediately updates each time the value changes, you can lock focus
>> on the view and redraw immediately each time the value changes.
>> e.g.:
>>
>> if ([view lockFocusIfCanDraw]) {
>> NSRect bounds = [view bounds];
>> CGFloat y = bounds.origin.y + (level value from 0.0 to 1.0) *
>> bounds.size.height;
>>
>> [[NSColor blackColor] set];
>> NSRectFill(bounds));
>> [[NSColor greenColor] set];
>> NSRectFill(NSMakeRect(bounds.origin.x, bounds.origin.y,
>> bounds.size.width, y - bounds.origin.y));
>>
>> [view unlockFocus];
>> }
>>
>>> How does Core Animation do its thing without hogging the CPU I
>>> wonder?
>>
>> By virtue of a background render thread, that offloads much work to
>> the GPU.
>>
>> Troy
>>
-
Thank you- my guess at this was so far off!
While I may not use this for an 'audio level meter' I may use this
technique for something a bit more complex and just as realtime!
Thanks again,
alex
On Feb 7, 2008, at 11:25 AM, Troy Stephens wrote:
> On Feb 6, 2008, at 7:26 PM, alex wrote
>> This is the perfect answer!! Thank you.
>>
>> One more question (sorry):
>>
>> How would one approach doing this exact thing with Layers? Or is
>> that not recommended and it should be done in the view like below?
>
> This could certainly be done using layers -- but if you aren't
> already using Core Animation-based rendering in your UI for other
> reasons, I'd recommend sticking with the plain NSView approach, as
> a CA renderer is an awfully big hammer to bring in just to draw a
> simple set of moving level bars.
>
> If you did go the layer-based route, an efficient approach would be
> to create a layer for each level bar, whose content was provided as
> a CGImage or as a backgroundColor (which can be a solid or pattern
> color), and change the layer's bounds height (or width) when the
> level value changes. To suppress the default implicit layer bounds
> animation so as to get an immediate response to level changes,
> you'd make the change inside a CATransaction within which you
> disable actions:
>
> [CATransaction begin];
> [CATransaction setValue:[NSNumber numberWithBool:YES]
> forKey:kCATransactionDisableActions];
> levelLayer.bounds = CGRectMake(0, 0, LEVEL_BAR_WIDTH,
> heightForNewLevelValue);
> [CATransaction commit];
>
> Providing the layer content as described effectively allows the
> redraw work to be offloaded to the GPU, which should result in good
> performance.
>
> Troy
>



