Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Core animation progress callback

Is there an easy way to be called back when a Core Animation reaches certain points as it's running (for example, at 50% and 66% of completion ?

I'm currently thinking about setting up an NSTimer, but that's not really as accurate as I'd like.

like image 214
tarmes Avatar asked Sep 16 '13 12:09

tarmes


2 Answers

I've finally developed a solution for this problem.

Essentially I wish to be called back for every frame and do what I need to do.

There's no obvious way to observe the progress of an animation, however it is actually possible:

  • Firstly we need to create a new subclass of CALayer that has an animatable property called 'progress'.

  • We add the layer into our tree, and then create an animation that will drive the progress value from 0 to 1 over the duration of the animation.

  • Since our progress property can be animated, drawInContext is called on our sublass for every frame of an animation. This function doesn't need to redraw anything, however it can be used to call a delegate function :)

Here's the class interface:

@protocol TAProgressLayerProtocol <NSObject>

- (void)progressUpdatedTo:(CGFloat)progress;

@end

@interface TAProgressLayer : CALayer

@property CGFloat progress;
@property (weak) id<TAProgressLayerProtocol> delegate;

@end

And the implementation:

@implementation TAProgressLayer

// We must copy across our custom properties since Core Animation makes a copy
// of the layer that it's animating.

- (id)initWithLayer:(id)layer
{
    self = [super initWithLayer:layer];
    if (self) {
        TAProgressLayer *otherLayer = (TAProgressLayer *)layer;
        self.progress = otherLayer.progress;
        self.delegate = otherLayer.delegate;
    }
    return self;
}

// Override needsDisplayForKey so that we can define progress as being animatable.

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

// Call our callback

- (void)drawInContext:(CGContextRef)ctx
{
    if (self.delegate)
    {
        [self.delegate progressUpdatedTo:self.progress];
    }
}

@end

We can then add the layer to our main layer:

TAProgressLayer *progressLayer = [TAProgressLayer layer];
progressLayer.frame = CGRectMake(0, -1, 1, 1);
progressLayer.delegate = self;
[_sceneView.layer addSublayer:progressLayer];

And animate it along with the other animations:

CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"progress"];
anim.duration = 4.0;
anim.beginTime = 0;
anim.fromValue = @0;
anim.toValue = @1;
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;

[progressLayer addAnimation:anim forKey:@"progress"];

Finally, the delegate will be called back as the animation progresses:

- (void)progressUpdatedTo:(CGFloat)progress
{
    // Do whatever you need to do...
}
like image 163
tarmes Avatar answered Oct 20 '22 16:10

tarmes


If you don't want to hack a CALayer to report progress to you, there's another approach. Conceptually, you can use a CADisplayLink to guarantee a callback on each frame, and then simply measure the time that has passed since the start of the animation divided by the duration to figure out the percent complete.

The open source library INTUAnimationEngine packages this functionality up very cleanly into an API that looks almost exactly like the UIView block-based animation one:

// INTUAnimationEngine.h

// ...

+ (NSInteger)animateWithDuration:(NSTimeInterval)duration
                           delay:(NSTimeInterval)delay
                      animations:(void (^)(CGFloat percentage))animations
                      completion:(void (^)(BOOL finished))completion;

// ...

All you need to do is call this method at the same time you start other animation(s), passing the same values for duration and delay, and then for each frame of the animation the animations block will be executed with the current percent complete. And if you want peace of mind that your timings are perfectly synchronized, you can drive your animations exclusively from INTUAnimationEngine.

like image 28
smileyborg Avatar answered Oct 20 '22 16:10

smileyborg