iphoneanimationcalayerperspectivecatransform3d

Origami transition using CATransform3D perspective


I'm trying to achieve a kind of origami transition on two UIView using only layer capabilities. The idea is to fold two views with a perspective effect. Both views have a perspective, the transition is defined by a rotation on each view, as well as a translation on one view such that this view seems to be attached to the other one.

The issue is that the view overlaps one another in the middle of the transition. I don't want to use a zPosition to visually avoid this overlapping, I really want these two views to act as if they were bound together by their shared side. Here is the code for the transition.

Any idea, or any other solution?

Overlapping views during transition

- (void)animateWithPerspective
{
    CGFloat rotationAngle = 90;
    CATransform3D transform = CATransform3DIdentity;
    UIView *topView;
    UIView *bottomView;
    UIView *mainView;
    CGRect frame;
    CGFloat size = 200;

    mainView = [[UIView alloc] initWithFrame:CGRectMake(10,10, size, size*2)];
    [self.view addSubview:mainView];
    bottomView = [[UIView alloc] initWithFrame:CGRectZero];
    bottomView.layer.anchorPoint = CGPointMake(0.5, 1);
    bottomView.frame = CGRectMake(0, size, size, size);
    bottomView.backgroundColor = [UIColor blueColor];
    [mainView addSubview:bottomView];

    topView = [[UIView alloc] initWithFrame:CGRectZero];
    topView.layer.anchorPoint = CGPointMake(0.5, 0);
    topView.frame = CGRectMake(0, 0, size, size);
    topView.backgroundColor = [UIColor redColor];
    [mainView addSubview:topView];

    transform.m34 = 1.0/700.0;
    topView.layer.transform = transform;
    bottomView.layer.transform = transform;

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:2];
    [UIView setAnimationRepeatAutoreverses:YES];
    [UIView setAnimationRepeatCount:INFINITY];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    frame = bottomView.frame;
    frame.origin.y = bottomView.frame.origin.y - bottomView.frame.size.height - topView.frame.size.height;
    bottomView.frame = frame;
    topView.layer.transform = CATransform3DRotate(transform, rotationAngle * M_PI/180, 1, 0, 0);
    bottomView.layer.transform = CATransform3DRotate(transform, -rotationAngle * M_PI/180, 1, 0, 0);
    [UIView commitAnimations];
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    [self animate];
}

To simplify the problem, let's get rid of any perspective transform. Here is a simpler code with the same kind of issue:

- (void)animateWithoutPerspective
{
    CGFloat rotationAngle = 90;
    UIView *topView;
    UIView *bottomView;
    UIView *mainView;
    CGRect frame;
    CGFloat size = 200;

    mainView = [[UIView alloc] initWithFrame:CGRectMake(10,10, size, size*2)];
    [self.view addSubview:mainView];
    bottomView = [[UIView alloc] initWithFrame:CGRectMake(0, size, size, size)];
    bottomView.backgroundColor = [UIColor blueColor];
    [mainView addSubview:bottomView];

    topView = [[UIView alloc] initWithFrame:CGRectZero];
    topView.layer.anchorPoint = CGPointMake(0.5, 0);
    topView.frame = CGRectMake(10, 0, size-20, size);
    topView.backgroundColor = [UIColor redColor];
    [mainView addSubview:topView];

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:2];
    [UIView setAnimationRepeatAutoreverses:YES];
    [UIView setAnimationRepeatCount:INFINITY];
    [UIView setAnimationCurve:UIViewAnimationCurveLinear];
    frame = bottomView.frame;
    frame.origin.y = bottomView.frame.origin.y - bottomView.frame.size.height;
    bottomView.frame = frame;
    topView.layer.transform = CATransform3DMakeRotation(rotationAngle * M_PI/180, 1, 0, 0);
    [UIView commitAnimations];
}

Solution

  • Finally, here is some solution for a three-sleeves animation with simple shadows added. The key to solve this kind of animation is to use several well organized sublayers and also some CATransformLayer.

    - (void)animate
    {
        CATransform3D transform = CATransform3DIdentity;
        CALayer *topSleeve;
        CALayer *middleSleeve;
        CALayer *bottomSleeve;
        CALayer *topShadow;
        CALayer *middleShadow;
        UIView *mainView;
        CGFloat width = 300;
        CGFloat height = 150;
        CALayer *firstJointLayer;
        CALayer *secondJointLayer;
        CALayer *perspectiveLayer;
    
        mainView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, width, height*3)];
        mainView.backgroundColor = [UIColor yellowColor];
        [self.view addSubview:mainView];
    
        perspectiveLayer = [CALayer layer];
        perspectiveLayer.frame = CGRectMake(0, 0, width, height*2);
        [mainView.layer addSublayer:perspectiveLayer];
    
        firstJointLayer = [CATransformLayer layer];
        firstJointLayer.frame = mainView.bounds;
        [perspectiveLayer addSublayer:firstJointLayer];
    
        topSleeve = [CALayer layer];
        topSleeve.frame = CGRectMake(0, 0, width, height);
        topSleeve.anchorPoint = CGPointMake(0.5, 0);
        topSleeve.backgroundColor = [UIColor redColor].CGColor;
        topSleeve.position = CGPointMake(width/2, 0);
        [firstJointLayer addSublayer:topSleeve];
        topSleeve.masksToBounds = YES;
    
        secondJointLayer = [CATransformLayer layer];
        secondJointLayer.frame = mainView.bounds;
        secondJointLayer.frame = CGRectMake(0, 0, width, height*2);
        secondJointLayer.anchorPoint = CGPointMake(0.5, 0);
        secondJointLayer.position = CGPointMake(width/2, height);
        [firstJointLayer addSublayer:secondJointLayer];
    
        middleSleeve = [CALayer layer];
        middleSleeve.frame = CGRectMake(0, 0, width, height);
        middleSleeve.anchorPoint = CGPointMake(0.5, 0);
        middleSleeve.backgroundColor = [UIColor blueColor].CGColor;
        middleSleeve.position = CGPointMake(width/2, 0);
        [secondJointLayer addSublayer:middleSleeve];
        middleSleeve.masksToBounds = YES;
    
        bottomSleeve = [CALayer layer];
        bottomSleeve.frame = CGRectMake(0, height, width, height);
        bottomSleeve.anchorPoint = CGPointMake(0.5, 0);
        bottomSleeve.backgroundColor = [UIColor grayColor].CGColor;
        bottomSleeve.position = CGPointMake(width/2, height);
        [secondJointLayer addSublayer:bottomSleeve];
    
        firstJointLayer.anchorPoint = CGPointMake(0.5, 0);
        firstJointLayer.position = CGPointMake(width/2, 0);
    
        topShadow = [CALayer layer];
        [topSleeve addSublayer:topShadow];
        topShadow.frame = topSleeve.bounds;
        topShadow.backgroundColor = [UIColor blackColor].CGColor;
        topShadow.opacity = 0;
    
        middleShadow = [CALayer layer];
        [middleSleeve addSublayer:middleShadow];
        middleShadow.frame = middleSleeve.bounds;
        middleShadow.backgroundColor = [UIColor blackColor].CGColor;
        middleShadow.opacity = 0;
    
        transform.m34 = -1.0/700.0;
        perspectiveLayer.sublayerTransform = transform;
    
        CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:0]];
        [animation setToValue:[NSNumber numberWithDouble:-90*M_PI/180]];
        [firstJointLayer addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:0]];
        [animation setToValue:[NSNumber numberWithDouble:180*M_PI/180]];
        [secondJointLayer addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.x"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:0]];
        [animation setToValue:[NSNumber numberWithDouble:-90*M_PI/180]];
        [bottomSleeve addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"bounds.size.height"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:perspectiveLayer.bounds.size.height]];
        [animation setToValue:[NSNumber numberWithDouble:0]];
        [perspectiveLayer addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"position.y"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:perspectiveLayer.position.y]];
        [animation setToValue:[NSNumber numberWithDouble:0]];
        [perspectiveLayer addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:0]];
        [animation setToValue:[NSNumber numberWithDouble:0.5]];
        [topShadow addAnimation:animation forKey:nil];
    
        animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
        [animation setDuration:2];
        [animation setAutoreverses:YES];
        [animation setRepeatCount:INFINITY];
        [animation setFromValue:[NSNumber numberWithDouble:0]];
        [animation setToValue:[NSNumber numberWithDouble:0.5]];
        [middleShadow addAnimation:animation forKey:nil];
    }