Martin-Gilles Lavoie
Martin-Gilles Lavoie

Reputation: 584

CAKeyframeAnimation not animating until I rotate device

I seem to be missing the obvious when animating a key frame. I have looked at many code samples including Apple's MoveMe, referenced in the CAKeyframeAnimation documentation, yet, I cant find a discrepancy that would cause what I'm seeing.

I create a CGMutablePathRef, then a CAKeyframeAnimation and set it to animate an image view along the path. An animation group is created so I can remove the view when done.

Yet, my animation never shows up. UNTIL I rotate the device. It seems a relayout of the view causes the animation to kickstart. I tried the obvious like [theImageView setNeedsDisplay] or even setNeedsLayout, and on the container view as well. Yet, still cant get it to work when I need to. They only show up when I rotate the device.

In the following, -cgPathFromArray: takes an NSArray of internal opcodes which is converted into a CGPathRef. Its verified to be correct because when I rotate the device, the animation does show along the programmed path.

- (void) animateImage: (NSString*) imageName
               onPath: (NSArray*) path
         withDuration: (NSString*) duration
{
    if (self.sceneView)
    {
        CGMutablePathRef    animationPath   =   [self cgPathFromArray: path];

        if (animationPath)
        {
            UIImage*    image       =   [self findImage: imageName];

            if (image)
            {
                UIImageView*    imageView   =   [[UIImageView alloc] initWithFrame: CGRectMake(0, 0, image.size.width, image.size.height)];

                if (imageView)
                {
                    CAKeyframeAnimation*    keyFrameAnimation   =   [CAKeyframeAnimation animationWithKeyPath: @"position"];

                    imageView.image = image;

                    [self.sceneView addSubview: imageView];

                    keyFrameAnimation.removedOnCompletion   =   YES;
                    keyFrameAnimation.fillMode              =   kCAFillModeForwards;
                    keyFrameAnimation.duration              =   duration.floatValue;
                    keyFrameAnimation.timingFunction        =   [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionLinear];
                    keyFrameAnimation.repeatCount = 0;

                    keyFrameAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];

                    [keyFrameAnimation setPath: animationPath];

                    //group animation with termination block to cleanup
                    CAAnimationGroup* group     =   [CAAnimationGroup animation];

                    group.duration              =   keyFrameAnimation.duration;
                    group.removedOnCompletion   =   YES;
                    group.fillMode              =   kCAFillModeForwards;
                    group.animations            =   @[keyFrameAnimation];

                    CorpsAnimationCompletionBlock theBlock = ^void(void)
                    {
                        [imageView removeFromSuperview];
                    };

                    [group setValue: theBlock
                             forKey: kCorpsAnimationCompletionBlock];

                    group.delegate = self;

                    [imageView.layer addAnimation: group
                                           forKey:  nil];
                }
            }
        }
    }
}

Anyone can help with this?

Upvotes: 0

Views: 638

Answers (1)

rob mayoff
rob mayoff

Reputation: 385680

You are probably having trouble because you're adding an animation to a layer in the same transaction where the layer is added to the visible layer tree. Core Animation doesn't like to attach animations to layers that haven't been committed yet. You may be able to work around this by doing [CATransaction flush] after adding the layer.

Your code is rather hard to look at because of the excessive nesting. Consider using early returns to make it more readable.

Also, you're explicitly creating the same frame that the -[UIImage initWithImage:] initializer would create for you.

If you're using an animation group and setting a delegate simply so you can execute a block at the end of the animation, there is an easier way. You can begin a CATransaction, set the transaction's completion block, then add the animation, then commit the transaction.

Thus:

- (void) animateImage:(NSString *)imageName onPath: (NSArray *)path
    withDuration: (NSString *)duration
{
    if (!self.sceneView)
        return;

    CGMutablePathRef animationPath = [self cgPathFromArray:path];
    if (!animationPath)
        return;

    UIImage *image = [self findImage:imageName];
    if (!image)
        return;

    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [self.sceneView addSubview: imageView];

    // commit the implicit transaction so we can add an animation to imageView.
    [CATransaction flush];

    [CATransaction begin]; {
        [CATransaction setCompletionBlock:^{
            [imageView removeFromSuperview];
        }];

        CAKeyframeAnimation *animation = [CAKeyframeAnimation
            animationWithKeyPath:@"position"];
        animation.duration = duration.floatValue;
        animation.timingFunction = [CAMediaTimingFunction
            functionWithName:kCAMediaTimingFunctionLinear];
        animation.path = animationPath;
        [imageView.layer addAnimation:animation forKey:animation.keyPath];
    } [CATransaction commit];
}

Upvotes: 1

Related Questions