Unexpected CALayer Vertical Flipping on 3D Rotation 'Bounce'

332 views Asked by At

I am trying to create a flip transition that contains a grid of CALayer cells that flip over and 'bounce' after they have done so. However, I am encountering a weird vertical flipping issue shown in this video. Without the 'bounce' animation at the end, the animation works as expected, so it's nothing to do with the image itself, or the splicing of the image into the individual cell layers before animating.

Animation Code (for each individual CALayer cell)

@interface transitionCell : CALayer

@property (nonatomic) UIImage* startImage;
@property (nonatomic) UIImage* endImage;

@property (nonatomic, readonly) BOOL animationDidFinish;
@property (nonatomic, readonly) BOOL isAnimating;

@property (nonatomic) CGFloat zRotateFactor;

@end

@implementation transitionCell

-(void) setStartImage:(UIImage *)startImage {
    self.contents = (__bridge id)startImage.CGImage;
}

-(void) startAnimationWithDuration:(CGFloat)duration { // First half of rotation

    _isAnimating = YES;

    CABasicAnimation* yRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
    yRotation.fromValue = @(0);
    yRotation.toValue = @(M_PI*0.5);

    CABasicAnimation* zRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    zRotation.fromValue = @(0);
    zRotation.toValue = @(zRotateMax*_zRotateFactor); // zRotateMax is M_PI*0.2 & _zRotateFactor is a value between -1 and 1.

    CAAnimationGroup* rotations = [CAAnimationGroup animation];
    rotations.duration = duration*0.5;
    rotations.delegate = self;
    rotations.removedOnCompletion = NO;
    rotations.fillMode = kCAFillModeForwards;
    rotations.animations = @[yRotation, zRotation];

    [self addAnimation:rotations forKey:@"startRotation"];
}

-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { // Second half of rotation

    if (anim == [self animationForKey:@"startRotation"] && flag) {

        self.contents = (__bridge id)_endImage.CGImage;

        CABasicAnimation* yRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
        yRotation.toValue = @(M_PI);
        yRotation.fromValue = @(M_PI*0.5);

        CABasicAnimation* zRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        zRotation.toValue = @(0);
        zRotation.fromValue = @(zRotateMax*_zRotateFactor);

        CAAnimationGroup* rotations = [CAAnimationGroup animation];
        rotations.duration = anim.duration;
        rotations.removedOnCompletion = NO;
        rotations.fillMode = kCAFillModeForwards;
        rotations.delegate = self;
        rotations.animations = @[yRotation, zRotation];
        [self addAnimation:rotations forKey:@"endRotate"];

    } else if (anim == [self animationForKey:@"endRotate"] && flag) { // The 'Bounce' animation (the one causing the issues)

        CABasicAnimation* yRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
        yRotation.toValue = @(M_PI+(M_PI*0.2));
        yRotation.fromValue = @(M_PI);

        CABasicAnimation* zRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        zRotation.fromValue = @(0);
        zRotation.toValue = @(-zRotateMax*_zRotateFactor*0.2);

        CAAnimationGroup* rotations = [CAAnimationGroup animation];
        rotations.duration = 0.2;
        rotations.removedOnCompletion = NO;
        rotations.fillMode = kCAFillModeForwards;
        rotations.delegate = self;
        rotations.autoreverses = YES;
        rotations.animations = @[yRotation, zRotation];
        [self addAnimation:rotations forKey:@"endRotate2"];

    } else if (anim  == [self animationForKey:@"endRotate2"] && flag) {
        _animationDidFinish = YES;
    }
}

@end

Initialisation Code (In the parent UIViewController)

-(instancetype) initWithStartImage:(UIImage*)startImage endImage:(UIImage*)endImage {
    if (self = [super init]) {

        CGFloat baseNodeHeight = screenWidth()/baseNumberNodesWidth;

        numNodesHeight = roundf(screenHeight()/baseNodeHeight);
        numNodesWidth = roundf(screenWidth()/baseNodeHeight);

        moveUpdateFreq = moveBaseUpdateFreq/(numNodesWidth*numNodesHeight);
        cellMoveUpdateFreq = cellMoveBaseUpdateFreq/(numNodesWidth*numNodesHeight);

        CGFloat const nodeWidth = screenWidth()/numNodesWidth;
        CGFloat const nodeHeight = screenHeight()/numNodesHeight;

        transition = (transitionType)arc4random_uniform(transitionTypeCount);

        cellArray = [NSMutableArray array];

        for (int x = 0; x < numNodesWidth; x++) {

            [cellArray addObject:[NSMutableArray array]];

            for (int y = 0; y < numNodesHeight; y++) {

                transitionCell* c = [transitionCell layer];
                c.frame = (CGRect){{x*nodeWidth, y*nodeHeight}, {nodeWidth, nodeHeight}};

                CGRect startSubRect = {{c.frame.origin.x, screenHeight()-c.frame.origin.y-c.frame.size.height}, c.frame.size};
                CGRect endSubRect = {{c.frame.origin.x, c.frame.origin.y}, c.frame.size};

                c.startImage = [startImage imageFromSubRect:startSubRect];
                c.endImage = [[endImage flippedVerticalImage] imageFromSubRect:endSubRect];

                c.zRotateFactor = -(((CGFloat)y-((CGFloat)numNodesHeight*0.5))/(CGFloat)numNodesHeight);

                [self.view.layer addSublayer:c];
                [cellArray[x] addObject:c];

            }
        }

    }
    return self;
}

Any ideas what I'm doing wrong?

1

There are 1 answers

0
Hamish On BEST ANSWER

Well, I can only assume this is a bug, but nonetheless I have found a quick fix. By adding a xRotation animation set to go from pi to pi on the 'bounce' animation, the problem is fixed. For example, in the bounce animation code:

// Bounce animation

CABasicAnimation* yRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.y"];
yRotation.toValue = @(M_PI+(M_PI*bounceFactor));
yRotation.fromValue = @(M_PI);

CABasicAnimation* zRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
zRotation.fromValue = @(0);
zRotation.toValue = @(-zRotateMax*_zRotateFactor*bounceFactor);

// New animation, set to do nothing on the x-axis
CABasicAnimation* xRotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
xRotation.toValue = @(M_PI);
xRotation.fromValue = @(M_PI);

CAAnimationGroup* rotations = [CAAnimationGroup animation];
rotations.duration = 0.2;
rotations.removedOnCompletion = NO;
rotations.fillMode = kCAFillModeForwards;
rotations.delegate = self;
rotations.autoreverses = YES;
rotations.animations = @[yRotation, zRotation, xRotation];
[self addAnimation:rotations forKey:@"endRotate2"];