iOS: Simultaneous scale and 3D rotation animation

11,922

Solution 1

You can't animate the same property without the first animation being canceled. You should drop down to using Core Animation and either doing two animations or one keyframe animation.

Two transformation animations

By creating one animation for scaling (which you could do as z-translation if you want) and one for rotating giving them very specific key paths they will not cancel each other.

CABasicAnimation *scale = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
scale.fromValue = @0.75; // Your from value (not obvious from the question)
scale.toValue = @1.0;
scale.duration = 0.4;
scale.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

CAKeyframeAnimation *rotate = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.x"];
rotate.values = @[@0.0, @(- 20 * M_PI / 180.0f), @0.0];
rotate.duration = 0.4;
rotate.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];

[view.layer addAnimation:scale forKey:@"move forward by scaling"];
[view.layer addAnimation:rotate forKey:@"rotate back and forth"];
view.transform = CGAffineTransformIdentity; // Set end value (animation won't apply the value to the model)

A single keyframe animation

Since you have three very good keyframes the code to animate between the three keyframes would be easy to read and understand. You would possibly lose some of the control if you wanted to change the timing of the scaling separate from that of the rotating.

CATransform3D firstFrame  = CATransform3DMakeScale(0.75, 0.75, 1.0);
CATransform3D secondFrame = CATransform3DMakeScale(0.875, 0.875, 1.0); // halfway to 1.0 from 0.75
secondFrame = CATransform3DRotate(secondFrame, -20.0*M_PI/180.0, 1.0, 0.0, 0.0);
CATransform3D lastFrame   = CATransform3DIdentity;

CAKeyframeAnimation *scaleAndRotate = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
scaleAndRotate.values = @[[NSValue valueWithCATransform3D:firstFrame],
                          [NSValue valueWithCATransform3D:secondFrame],
                          [NSValue valueWithCATransform3D:lastFrame] ];
scaleAndRotate.duration = 1.0;
scaleAndRotate.timingFunctions = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];

[view.layer addAnimation:scaleAndRotate forKey:@"entire animation"];
view.transform = CGAffineTransformIdentity; // Set end value (animation won't apply the value to the model)

Perspective

In both cases I did the perspective by setting the sublayerTransform on the superlayer of the animating view (assuming there is only one subview with a transform there)

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = 1.0 / - 1800.0;    
view.superview.layer.sublayerTransform = perspective;

Solution 2

I found a way but its more like a hack. Do the following -

1)create a view with transparent background - (this view will translate) 2)create a subview of 1) - (this view will rotate).

Now use create two transforms, a scaling transform for view 1) and a rottaion transform for view 2) and apply them simultaneously.

Share:
11,922
radutzan
Author by

radutzan

A coding UX designer.

Updated on July 27, 2022

Comments

  • radutzan
    radutzan almost 2 years

    I made a video demonstrating the animation i'm trying to accomplish: Here it is.

    Notice the video illustrates the action as it would be seen from the side. The camera icon represents the user's POV. It's basically a simulation of a translation among the Z-axis accomplished though a scale transform, with a simultaneous independent two-step 3D rotation happening along the X axis.

    Looks and sounds simple enough, right? Wrong. Here's the ideal code, which doesn't work:

    [UIView animateWithDuration:0.4 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        view.transform = CGAffineTransformMakeScale(1.0, 1.0);
    } completion:^(BOOL finished) {}];
    
    [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
        CATransform3D rotation = CATransform3DIdentity;
        rotation.m34 = 1.0 / - 1800;
        rotation = CATransform3DRotate(rotation, - 20 * M_PI / 180.0f, 1, 0, 0);
    
        view.layer.transform = rotation;
    
    } completion:^(BOOL finished) {
        [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
            view.layer.transform = CATransform3DIdentity;
    
        } completion:^(BOOL finished) {}];
    }];
    

    Turns out, if you do this, the 3D rotation gets ignored completely. I've tried a number of other approaches, and they all have failed.

    Some requirements:

    • Scale should preferably be a CGAffineTransform
    • Easings should be as illustrated, especially on the rotation

    Of course, if a solution comes up that requires some of these to change, I could adapt.

    Thanks in advance for any help. If you need clarification, please do ask.

  • radutzan
    radutzan over 11 years
    This looks great. I'll try it tomorrow and let you know how it goes. Thanks a lot and happy holidays!
  • radutzan
    radutzan over 11 years
    I couldn't wait, and it's already technically tomorrow here, so I tried it, and it works great! I went with the first one, because it keeps the timing functions separate between the two transforms. Thank you so much!