iOS: Simultaneous scale and 3D rotation animation

2019-02-02 17:10发布

问题:

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.

回答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;


回答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.