Blog Archives

More CADisplayLink

Hello to everyone coming from iDevBlogADay!

Since this is my first post to be pulled into the site, I’ll take just a quick moment to introduce myself.  My name is Pat Zearfoss and I’m one of the lead iOS developers at Mindgrub Technologies, a development shop in Baltimore, MD, USA.  We’re primarily a work-for-hire shop, but we do have a couple products we’ve released on our own.  I’ve been working on iOS since the SDK became public and because of the nature of my work at Mindgrub, I’ve had the opportunity to work on a variety of projects, large and small.

This site is a collection of thoughts on iOS development, usually spurned from a project I’m working on.  Lately I’ve been spending a lot of time in CoreAnimation, so as a result you’ll find a lot on that topic.  I usually try to jot down things that weren’t immediately obvious to me as I work through a project, usually figuring that if I had to really dig to find a solution, someone else probably will to.

I was rattling my brain to try and come up with a good introductory topic for my first iDevBlogADay post and I eventually settled upon doing another post about CADisplayLink.  Not that this site gets a ton of traffic, but CADisplayLink tops my search terms daily.  Documentation around the web on it is scarce as it’s a pretty niche topic, although highly useful when the animation you want to create in an iOS app is something not easily handled by transitions or stock animations.

So what is a CADisplayLink.  Directly from Apple:

A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display.

CADisplayLink works very much like an NSTimer.  You can set it to call a method on every tick, but instead of setting a time interval, the time interval is determined by the screen’s refresh rate.  When you’re ready to use the display link you add it to the run loop, and the method you handed to it will fire until you call the “invalidate” method on it.  CADisplayLink lives in the QuartzCore framework.

In an earlier post, I showed how to draw a simple line on a CALayer using CADisplayLink, drawing an additional pixel every time.  Today I’m going to show you how to draw a circle fanning out (similar to how the pie charts render in Mint.com) and setting a desired time duration for that animation. To get started, we’re going to create a new project with a single view controller and UIView subclass.  Here’s my project layout:

We’ll also need to add the QuartzCore framework to the project.  (Target settings -> Build Phases):

Next in viewDidLoad of our view controller, we need to create a new instance of our UIView subclass and add it to the view:


- (void)viewDidLoad
{
    [super viewDidLoad];
    CircleDisplayView *circleView =
        [[CircleDisplayView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:circleView];
    [circleView release];
}

Now onto our custom view. This is where we’ll be using the CADisplayLink. Here’s my .h file:

#import <QuartzCore/QuartzCore.h>

@interface CircleDisplayView : UIView
{
    CADisplayLink *displayLink;

    BOOL animationRunning;
    NSTimeInterval drawDuration;
    CFTimeInterval lastDrawTime;
    CGFloat drawProgress;
}
@end

Our view only needs to keep track of a couple things. First we need the CADisplayLink. We’ll need to keep track of it during drawing.  We’ll also need a flag to know whether our animation is running (animationRunning) and some variables to keep track of timestamps(lastDrawTime), the drawing progress (drawProgress), as well as the amount of time we want the animation to run for (drawDuration)

In the initializer, all we’ll need to do is get an instance of CADisplayLink and set the amount of time we want for the animation:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        displayLink = [CADisplayLink displayLinkWithTarget:self
                                                  selector:@selector(setNeedsDisplay)];
        drawDuration = 3.0;
    }
    return self;
}

The method we’ll be using in the CADisplayLink is simply setNeedsDisplay.  So, while the animation is running, on every tick of the display link we’ll be asking the view to redraw itself.  All the magic then happens in drawRect:

- (void)drawRect:(CGRect)rect
{
    if (!animationRunning)
    {
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        animationRunning = YES;
        return;
    }

    if (lastDrawTime == 0)
    {
        lastDrawTime = displayLink.timestamp;
        return;
    }

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(ctx, [[UIColor blueColor] CGColor]);

    CFTimeInterval elapsedTime = displayLink.timestamp - lastDrawTime;
    NSLog(@"elapsed %f", elapsedTime);

    CGFloat radiansToDraw = drawProgress + ((2 * M_PI) / drawDuration) * elapsedTime;

    NSLog(@"drawing %f radians", radiansToDraw);

    CGContextMoveToPoint(ctx, self.center.x, self.center.y);
    CGContextAddLineToPoint(ctx, self.center.x + 100, self.center.y);
    CGContextAddArc(ctx, self.center.x, self.center.y, 100, 0, radiansToDraw, 0);
    CGContextClosePath(ctx);
    CGContextFillPath(ctx);

    lastDrawTime = displayLink.timestamp;
    drawProgress = radiansToDraw;

    if (radiansToDraw > 2 * M_PI)
    {
        NSLog(@"Invalidate display link");
        [displayLink invalidate];
        animationRunning = NO;
        lastDrawTime = 0;
    }

}

Most of this code is standard drawing code: adding lines and arcs, so I’m not going to go into great detail on those, but rather focus on the bits having to do with making the circle drawing occur progressively.

The first thing to notice is that on the first (and second) pass through drawRect, no actual drawing occurs.  The first pass occurs when the view naturally tries to render itself.  On this pass all we need to do is add the displayLink to the run loop and return.

Second pass we get caught in the second if statement, ensuring that we seed lastDrawTime.  We need only know about the differences in two display link timestamps, so this is necessary.

Finally on the third pass we begin drawing.  It’s important to remember that the whole view gets refreshed every pass of drawRect, so all we essentially need to do is add some radians to the arc drawn every pass.  When we notice that the amount of radians drawn exceeds 2π we know the circle is complete and reset the display link.

And that’s it!  If you run that code, you should see the circle fan out at a constant rate.  You could clearly make this slicker by seeding the display link on didAddToSuperview or adding an easing function to the calculation for radiansToDraw.  I hope this has been useful to some folks out there.  As always I’ll post the code to github so you can check it out.

Thanks and happy coding!

Advertisements