Core Animation CALayer mask animation performance

13,053

Solution 1

Brent,

Why do you need to use a layer mask? Can't you just convert your mask layer to a sublayer? You would just need to make sure your image had the proper alpha and you would use it's CGImageRef as that layer's contents.

One other thing. I haven't figured out why yet, but I've also noticed performance issues when I apply shouldRasterize on each layer instead of just the top layer. You might see if removing the reference to setShouldRasterize:YES in your mask layer helps at all.

Solution 2

One approach would be to create a CAShapeLayer to use as your mask—you'd have a fair bit of work cut out for you making a version of your “sync” icon with Bézier paths, but shape layers incur a much lower performance cost per-transformation than bitmap ones. Unfortunately, you can't be sure that the rotation is where the performance issue is coming from—it could very well be the masking causing most of the lag, in which case you'll have done all that vectorizing for little benefit.

I think the best solution is to use UIImage's animation capabilities: create a filmstrip of every frame of the icon-rotation animation and simply display that animated UIImage in your tab bar. It's not the most elegant solution, but a number of animations across the system—the Mail and Notes trash-can “delete” icon and the various forms of activity indicator, for instance—are implemented the same way.

Share:
13,053
user2484801
Author by

user2484801

Updated on June 03, 2022

Comments

  • user2484801
    user2484801 almost 2 years

    We wanted to use a UITabBar in our iPhone app, but with one exception: we have a "sync" button which I wanted to rotate while the sync operation is happening.

    alt text

    Unfortunately this meant having to create a custom tab bar, but that's neither here nor there: the animation I implemented using Core Animation looks awesome. The problem is that while animating, it adversely affects the performance of everything else using animation on the screen: UITableView scrolling, MKMapView panning and pin drops, etc. My test device is an iPhone 4.

    The problem seems to be how I've implemented the tab bar - I wanted to achieve something very similar to UITabBar, where you just supply a PNG for the icon and it uses the alpha channel to create the normal and highlighted states by masking a background image. I accomplished this with CALayer's mask property:

    // Inside a UIView subclass' init method...
    
    // Create the mask layer by settings its contents as our PNG icon.
    CALayer *maskLayer = [CALayer layer];
    maskLayer.frame = CGRectMake(0, 0, 31, 31);
    maskLayer.contentsGravity = kCAGravityCenter;
    maskLayer.contentsScale = [[UIScreen mainScreen] scale];
    maskLayer.rasterizationScale = [[UIScreen mainScreen] scale];
    maskLayer.contents = (id)symbolImage.CGImage;
    maskLayer.shouldRasterize = YES;
    maskLayer.opaque = YES;
    
    fgLayer = [[CALayer layer] retain];
    fgLayer.frame = self.layer.frame;
    fgLayer.backgroundColor = [UIColor colorWithImageNamed:@"tabbar-normal-bg.png"].CGColor;
    fgLayer.mask = maskLayer; // Apply the mask
    fgLayer.shouldRasterize = YES;
    fgLayer.opaque = YES;
    
    [self.layer addSublayer:fgLayer];
    

    (Note: in the screenshot above you can see I've also added a shadow layer, but for simplicity I removed that from the code. I remove the shadow layer from the sync icon when it is animating, so it shouldn't be relevant.)

    To animate, I simply rotate the mask layer:

    - (void)startAnimating {
        CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath: @"transform"];
        CATransform3D transform = CATransform3DMakeRotation(RADIANS(179.9), 0.0, 0.0, 1.0);
        animation.toValue = [NSValue valueWithCATransform3D:transform];
        animation.duration = 5;
        animation.repeatCount = 10000;
        animation.removedOnCompletion = YES;
        [fgLayer.mask addAnimation:animation forKey:@"rotate"]; // Add animation to the mask
    }
    

    So this all works great except for performance. You can see I've already tried tips that turned up on Google about rasterizing layers / making them opaque - hasn't helped.

    I think I've identified the mask layer as being the culprit. When I take out the mask layer and just rotate the fgLayer instead of its mask, performance is wonderful, though it's certainly not the affect I'm going for:

    alt text

    Performance is also just as bad as before if I rotate the fgLayer instead of the mask while the mask is applied.

    So if having to recomposite the mask each frame of the animation is the slow down, are there any other techniques I can use to achieve a similar affect that will have better performance? Using a path instead of an image for the mask layer? Or am I going to have to drop down to OpenGL or something to get good performance?

    UPDATE: further reinforcing the idea that the mask is the slowdown, my coworker suggested trying to rotate a CALayer with just the image as the contents -- so similar to my example above w/o a mask -- and performance was also good that way. So I can really only do a solid color like that (no gradient), but it may be a good interim solution. I'd still love to achieve rotating a mask with good performance though, so suggestions welcome :)

  • user2484801
    user2484801 over 13 years
    The second part of your answer was the solution - I should have been setting shouldRasterize on the top layer instead of the sublayers. This seems to have made performance just as good as without the mask. The reason I "need" a mask is so I can "fill" the simple PNG icons with a nice gradient to add visual interest. The default UITabBar does this, so I wanted to do the same (but with yellow).
  • user2484801
    user2484801 over 13 years
    Thanks for the answer - using a filmstrip of the animation was definitely something I was considering, but sounded like a lot of work + file space. Luckily shouldRasterize on the superlayer seems to have achieved the performance I expected.
  • Matt Long
    Matt Long over 13 years
    Cool, Brent. Glad it was that simple. If you gain any insight into the why of the matter, I would love to know. ;-)
  • user2484801
    user2484801 over 13 years
    I'd love to know why as well. Furthermore, I'd love to know how rasterizing the top layer actually improves performance of a mask animation. It's still doing exactly what I want - the backing gradient image is staying stationary, while the mask is rotating. Is it actually precomputing the bitmap for each frame before animation begins? Seems like magic. In this case, I like magic. :)
  • Nathan Eror
    Nathan Eror over 13 years
    I'd also like to know what's going on here. Assuming that I read your comment correctly and you're only setting shouldRasterize on the containing view's layer, maybe CA takes the shouldRasterize hint and consolidates the masking operations for each sublayer into one operation keeping it from needing to swap buffers for every masked layer. Have you tried turning on the "Color Offscreen" option in Instruments in both cases to see how many rendering passes CA is making? If I get some time, I'm going to test it, but let me know if you beat me to it. I want to demystify this black magic. :)
  • Shuo
    Shuo over 10 years
    From the answer, seems "set shouldRasterize to NO" solved your problem.
  • Shuo
    Shuo over 10 years
    I had the same experience, I thought shouldRasterize should improve the animation but instead it lags it. I did the instrument and the whole layer (the one with shouldRasterize) was offscreen rendering which caused the problem. PS. I know this is an old thread.