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!

Drawing with CADisplayLink

For 90% of the animations you’ll ever need to do CoreAnimation or UIView animations are enough. Sometimes, the thing you want to animate can’t be handled by simple property based transitions or by moving an object along a path. Enter CADisplayLink:

From the CADisplayLink class reference:

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

The purposes that I need, I want to animate a circle similarly to how flex charts draws pie charts (a “swipe open” kind of animation). For the demo, though, I’ll simply use the CADisplayLink to animate a line extending across the screen on a CALayer using it’s drawInContext: method.

First the main view that contains my CALayer subclass:

//  MainView.h

#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import "AnimationLayer.h"

@interface MainView : UIView 
{
    AnimationLayer *alayer;
}

@end
//  MainView.m

#import "MainView.h"

@implementation MainView

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code
    }
    return self;
}

- (void)didMoveToSuperview
{
    if ([self superview])
    {
                
        alayer = [[AnimationLayer alloc] init];
        alayer.frame = self.frame;
        [self.layer addSublayer:alayer];
        
    }
}

And next the CALayer that will be animated:

//  AnimationLayer.h

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface AnimationLayer : CALayer 
{
    CADisplayLink *displayLink;
}

@end
//  AnimationLayer.m
#import "AnimationLayer.h"

static bool _running;

@implementation AnimationLayer

- (id)init
{
    self = [super init];
    if (self)
    {
        displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(setNeedsDisplay)] retain];
        [self setNeedsDisplayOnBoundsChange:YES];
    }
    
    return self;
}

static CGPoint lastPoint = {0, 0};
- (void)drawInContext:(CGContextRef)ctx
{
    if (!_running)
    {
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
        _running = YES;
        return;
    }

    CGContextSetStrokeColorWithColor(ctx, [[UIColor blackColor] CGColor]);
    
    CGRect rect = CGContextGetClipBoundingBox(ctx);
    
    CGContextMoveToPoint(ctx, 0, 0);
    lastPoint.x = lastPoint.y += 1;
    CGContextAddLineToPoint(ctx, lastPoint.x, lastPoint.y);
    CGContextStrokePath(ctx);
    
    if (lastPoint.x == rect.size.width)
    {
        [displayLink invalidate];
        _running = NO
    }
}

@end

The main view is pretty uninteresting as it only creates a new CALayer and adds it as a sublayer of itself.

The animation layer holds the CADisplayLink as an Ivar and initializes it with the layer’s drawInContext: method.

We need some kind of flag to tell the CADisplayLink when to stop updating, for this I used a static BOOL _running. On the first pass of drawInContext, we’ll add the display link to the main run loop, at which point it will start calling the specified selector. We’ll set the _ruinning flag to true and return. On every subsequent pass of drawInContext: we’ll draw a longer line running diagonally. At the end we’ll check the condition to invalidate the CADisplayLink; in this case I’ll check to see whether the line has fully crossed the screen. Once we invalidate the display link the animation will stop.

This is a simple example, but it shows how you can animate an object using pure drawing for the times when property animations simply won’t do.