Re: How to redraw a view in slow-motion
Re: How to redraw a view in slow-motion
- Subject: Re: How to redraw a view in slow-motion
- From: Graham Cox <email@hidden>
- Date: Mon, 20 Jun 2011 10:24:02 +1000
On 19/06/2011, at 10:46 PM, Matthias Arndt wrote:
> In a document-based app my custom view draws some thousand paths in drawRect: with a good performance. Now I'd like to offer a "slow-motion" animation, so the user can actually watch the paths being drawn (not each single one, but e. g. in steps of 100 paths per sec).
>
> I though of several approaches and all of them seem to be infeasible:
>
> 1. Sleeping the drawing loop in drawRect: (or make the runLoop wait for some time) and use [... flushGraphics]: Freezes the GUI, as the app is single-threaded
> 2. Moving the drawing in a 2nd thread and then pause this one: AFAIK is drawing in a second thread not allowed in Cocoa
> 3. Limit the drawing loop to an increasing high bound, and setup a timer to fire [self setNeedsDisplay:YES] periodically: Causes the first x paths being redrawn at each animation step, resulting in a bad performance
> 4. Same approach, but skipping the first x paths in the next animation step: Corrupted display, e. g. while resizing in an animation
>
> I'm racking my brains over this, any suggestions?
Ken's thoughts regarding Core Animation are good ones. I've started doing some stuff with it only recently and have found it really good for this sort of thing. But I also found you need to plan your design with it in mind - it's not so easy to retro-fit to an existing design.
Some general comments:
1. Don't do drawing in a thread.
2. Don't put any unnecessary delays in -drawRect:
3. Don't even think about using threads for this.
Your (4) is looking like the best approach - use a timer to schedule a periodic update of the view, and during that update, draw only some portion of the content on top of what you've drawn already. That leaves the issue of view resizing, which you'll need to take into account. You can get notified when the view is resizing, and modify your update schedule to ensure that it does what you want - typically, it will need to flag that the background needs to be erased and that the objects need to be redrawn from the beginning.
So let's say you have a total number of objects needing to be drawn. You can also select that a smaller range of them be drawn each time. As Ken suggested, using NSRange is a good way to track this. So, this should give you a rough idea (typed into mail, untested):
@interface DelayView : NSView
{
NSRange _animationRange; // range of objects to be drawn at each animation frame
BOOL _needsErase; // set to YES to erase the view's background, must be inited to YES
}
@end
#define OBJECTS_TO_BE_DRAWN_PER_FRAME 10 // number of objects drawn each frame
@implementation DelayView
- (void) drawRect:(NSRect) updateRect
{
if( _needsErase )
{
[[self backgroundColor] set];
NSRectFill( updateRect );
_needsErase = NO;
}
NSUInteger n;
for( n = _animationRange.location; n < NSMaxRange( _animationRange ); ++n )
[self drawObjectAtIndex:n];
}
- (void) timerCallback:(NSTimer*) timer
{
_animationRange.location += OBJECTS_TO_BE_DRAWN_PER_FRAME;
if( NSMaxRange( _animationRange ) > [self countOfObjects])
{
// warning: be careful about signed and unsigned arithmetic here
NSInteger len = [self countOfObjectsToBeDrawn] - _animationRange.location;
if( len > 0 )
_animationRange.length = len;
else
_animationRange.length = 0;
// after this subset has been drawn, all objects will have been drawn and there's no more to do for now
}
if( _animationRange.length > 0 )
[self setNeedsDisplay:YES];
}
- (void) setFrame:(NSRect) frame
{
[super setFrame:frame];
_animationRange.location = 0;
_animationRange.length = OBJECTS_TO_BE_DRAWN_PER_FRAME; // or maybe all objects?
_needsErase = YES;
[self setNeedsDisplay:YES];
}
To redraw the objects from the beginning, you need to do what -setFrame: does - reset the range of objects to be animated and flag the erase, which clears the background. Then, the timer (scheduled elsewhere, not shown here), calls the timer callback which schedules a new chunk of objects to be drawn and so on, but without erasing. When all objects have been drawn, no further animation is performed. By setting the _animationRange.length to the total number of objects, and .location to 0, you can draw everything in one go without animation, so the same mechanism can be used for both the animated and non-animated case.
--Graham
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden