17

I am wondering how one might animate a CALayer's bounds so, on each bounds change, the layer calls drawInContext:. I've tried the 2 following methods on my CALayer subclass:

  • Setting needsDisplayOnBoundsChange to YES
  • Returning YES for the + (BOOL)needsDisplayForKey:(NSString*)key for the bounds key

Neither work. CALayer seems determined to use the layer's original contents and simply scale them according to contentsGravity (which, I assume, is for performance.) Is their a workaround for this or am I missing something obvious?

EDIT: And, incidentally, I noticed that my custom CALayer subclass is not calling initWithLayer: to create a presentationLayer - weird.

Thanks in advance, Sam

Sam
  • 2,707
  • 20
  • 25
  • Another thing which does not work: subclassing and overriding `setFrame`, `setBounds` and `setPosition`. They are not called during the animation. – Alexander Ljungberg Oct 29 '11 at 11:31
  • I don't quite understand you. What are you trying to animate? Just CALayer's bounds or something else? Bounds animating is quite simple task, frame animating - more complex. – beryllium Oct 31 '11 at 09:39
  • Imagine your layer contains something like a button with a complex but size independent border graphic. If you animate it to say double the width, it will animate using bitmap scaling, becoming stretched and pixelated throughout the animation, even that you have `needsDisplayOnBoundsChange` YES. Only the final frame will be rendered properly with `drawInContext:`. – Alexander Ljungberg Nov 01 '11 at 23:33

5 Answers5

7

You can use the technique outlined here: override CALayer's +needsDisplayForKey: method and it will redraw its content at every step of the animation.

Community
  • 1
  • 1
titaniumdecoy
  • 18,900
  • 17
  • 96
  • 133
  • This is a great answer. Give some more information about the link so we know before we click. – Alexsander Akers Nov 01 '11 at 23:26
  • This method indeed makes the redraw method called for every frame as far as I can tell, although oddly the CALayer doesn't display the results of those redraws but continues with the stretched content it generated ahead of time. Still, it's a significant step forward. – Alexander Ljungberg Nov 04 '11 at 10:33
  • @AlexanderLjungberg: This method should redraw the content at every step of the animation. I created a test project to confirm that it works. I'm not sure what might be causing your layer to not be redisplayed. – titaniumdecoy Nov 04 '11 at 18:05
  • 3
    @titaniumdecoy: I also tried... It works for custom properties, but not for `bounds` (as well as other `CALayer` properties). The `actionForKey:` method IS called for custom properties, but not for `CALayer` properties. Are you animating custom property or `CALayer` property? – debleek63 Nov 14 '11 at 19:52
  • I was able to animate a custom property (the radius of a ball inside a square CALayer) that animated while the view's position changed. – titaniumdecoy Nov 14 '11 at 20:04
  • If you upvote this answer, please upvote the linked answer as well. Thanks. – titaniumdecoy Nov 30 '11 at 00:34
  • 2
    So if `+needsDisplayForKey:` doesn't work with `bounds`, then what does? – zakdances Apr 14 '13 at 11:04
1

I'm not sure that setting the viewFlags would be effective. The second solution definitely won't work:

The default implementation returns NO. Subclasses should * call super for properties defined by the superclass. (For example, * do not try to return YES for properties implemented by CALayer, * doing will have undefined results.)

You need to set the view's content mode to UIViewContentModeRedraw:

UIViewContentModeRedraw,    //redraw on bounds change (calls -setNeedsDisplay)

Check out Apple's documentation on providing content with CALayer's. They recommend using the CALayer's delegate property instead of subclass, which might be a lot easier than what you're trying now.

Ash Furrow
  • 12,391
  • 3
  • 57
  • 92
0

I don't know if this entirely qualifies as a solution to your question, but was able to get this to work.

I first pre-drew my contents image into a CGImageRef.

I then overrode the -display method of my layer INSTEAD OF -drawInContext:. In it I set contents to the pre-rendered CGImage, and it worked.

Finally, your layer also needs to change the default contentsGravity to something like @"left" to avoid the contents image being drawn scaled.

The problem I was having was that the context getting passed to -drawInContext: was of the starting size of the layer, not the final post-animation size. (You can check this with the CGBitmapContextGetWidth and CGBitmapContextGetHeight methods.)

My methods are still only called once for the entire animation, but setting the layer's contents directly with the -display method allows you to pass an image larger than the visible bounds. The drawInContext: method does not allow this, as you cannot draw outside the bounds of the CGContext context.

For more about the difference between the different layer drawing methods, see http://www.apeth.com/iOSBook/ch16.html

bcattle
  • 12,115
  • 6
  • 62
  • 82
0

I'm running into the same problem recently. This is what I found out:

There is a trick you can do it, that is animating a shadow copy of bounds like:

var shadowBounds: CGRect {
  get { return bounds }
  set { bounds = newValue}
}

then override CALayer's +needsDisplayForKey:.

However, this MAY NOT be what you want to do if your drawing depends on bounds. As you have already noticed, core animation simply scales the contents of layer to animate bounds. This is true even if you do the above trick, that is, the contents are scaled even if the bounds changed during animation. The result is your animation of drawings looks inconsistent.

How to resolve it? Since the content is scaled, you can calculate the values of custom variables determining your drawing by reverse-scaling them so that your drawing on the final but scaled content looks the same as the original and unscaled one, then set the fromValues to these values, the toValues to their old values, animate them at the same time with bounds. If the final values are to be changed, set the toValues to these final values. You must animate at least one custom variable so as to causing the redraw.

-2

This is your custom class:

@implementation MyLayer

-(id)init
{
    self = [super init];
    if (self != nil)
        self.actions = [NSDictionary dictionaryWithObjectsAndKeys:
                        [NSNull null], @"bounds",
                        nil];
    return self;
}

-(void)drawInContext:(CGContextRef)context
{
    CGContextSetRGBFillColor(context,
                             drand48(),
                             drand48(),
                             drand48(),
                             1);
    CGContextFillRect(context,
                      CGContextGetClipBoundingBox(context));
}

+(BOOL)needsDisplayForKey:(NSString*)key
{
    if ([key isEqualToString:@"bounds"])
        return YES;
    return [super needsDisplayForKey:key];
}

@end

These are additions to xcode 4.2 default template:

-(BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
        // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

        // create and add layer
    MyLayer *layer = [MyLayer layer];
    [self.window.layer addSublayer:layer];
    [self performSelector:@selector(changeBounds:)
               withObject:layer];

    return YES;
}

-(void)changeBounds:(MyLayer*)layer
{
        // change bounds
    layer.bounds = CGRectMake(0, 0,
                              drand48() * CGRectGetWidth(self.window.bounds),
                              drand48() * CGRectGetHeight(self.window.bounds));

        // call "when idle"
    [self performSelector:@selector(changeBounds:)
               withObject:layer
               afterDelay:0];
}

----------------- edited:

Ok... this is not what you asked for :) Sorry :|

----------------- edited(2):

And why would you need something like that? (void)display may be used, but documentation says it is there for setting self.contents...

debleek63
  • 1,181
  • 8
  • 17