Search code examples
ioscore-graphicscalayercaanimation

Smoothly animated hole in CALayer?


Ok, I figured out how to do this based on various posts here on SO, and it works great. I'm working on an overlay which will basically mask the whole window except for a small region. This is for drawing attention to a specific area of my app. I'm using a bunch of calls to moveToPoint: and addLineToPoint: like so (this is in my CALayer subclass' drawInContext:):

....

// inner path (CW)
[holePath moveToPoint:CGPointMake(x, y)];
[holePath addLineToPoint:CGPointMake(x + w, y)];
[holePath addLineToPoint:CGPointMake(x + w, y + h)];
[holePath addLineToPoint:CGPointMake(x, y+h)];

// outer path (CCW)
[holePath moveToPoint:CGPointMake(xBounds, yBounds)];
[holePath addLineToPoint:CGPointMake(xBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds + hBounds)];
[holePath addLineToPoint:CGPointMake(xBounds + wBounds, yBounds)];

// put the path in the context
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 0, 0);
CGContextAddPath(ctx, holePath.CGPath);
CGContextClosePath(ctx);

// set the color
CGContextSetFillColorWithColor(ctx, self.overlayColor.CGColor);

// draw the overlay
CGContextDrawPath(ctx, kCGPathFillStroke);

(holePath is an instance of UIBezierPath.)

So far so good. The next step is animation. In order to do this (I also found this technique here on SO) I made a method as follows

-(CABasicAnimation *)makeAnimationForKey:(NSString *)key {
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:key];
    anim.fromValue = [[self presentationLayer] valueForKey:key];
    anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    anim.duration = 0.5;

    return anim;
}

and overrode actionForKey:, initWithLayer: and needsDisplayForKey: (returning the result of makeAnimationForKey: in actionForKey:. Now, I get a nice "hole layer", which has a property holeRect which is animatable using implicit CAAnimations! Unfortunately, it's SUPER choppy. I get something like 2 or 3 frames per second. I thought that perhaps the problem was the background, and tried replacing it with a snapshot, but no dice. Then, I used Instruments to profile, and discovered that the HUGE hog here is the call to CGContextDrawPath().

tl;dr I guess my question comes down to this: is there a simpler way to create this layer with a hole in it which will redraw faster? My hunch is that, if I could simplify the path I'm using, drawing the path would be lighter. Or possibly masking? Please help!


Solution

  • Ok, I tried phix23's suggestion, and it totally did the trick! At first, I was subclassing CALayer and adding a CAShapeLayer as a sublayer, but I couldn't get it to work properly and I'm pretty tired at this point, so I gave up and just replaced my subclass completely with CAShapeLayer! I used the above code in its own method, returning the UIBezierPath, and animated like so:

    UIBezierPath* oldPath = [self pathForHoleRect:self.holeRect];
    UIBezierPath* newPath = [self pathForHoleRect:rectToHighlight];
    self.holeRect = rectToHighlight;
    
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"path"];
    animation.duration = 0.5;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    animation.fromValue = (id)oldPath.CGPath;
    animation.toValue = (id)newPath.CGPath;
    [self.holeLayer addAnimation:animation forKey:@"animatePath"];
    self.holeLayer.path = newPath.CGPath;
    

    Interesting side note -- path is NOT implicitly animatable. I guess that makes sense.