Implement custom animation to present modal view f

2019-01-16 03:29发布

问题:

On the iPad we get much more room to work with, so presenting full screen modal views is not ideal.

I know how to present modal views in the new formSheet and a close approach can be found on this question: iPad iTunes Animation

The problem is that you cannot choose where the animation will come from, so it just defaults and appears from the center, I want to customize it so that it appears from a specific location.

The best example I can find for this animation can be seen on the first few seconds of this video

If anyone can point me on the right direction using code, tutorials or documentation I would greatly appreciate it!

Update:

After some investigation I have found that this can be done using layers and Core Animation for the first part; and then animate it a formSheet modal view but I still dont quite understand how to achieve it, hopefully you guys can help!

回答1:

What I did was creating a new category for UIViewController as follows

UIViewController+ShowModalFromView.h

#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view;

@end

UIViewController+ShowModalFromView.m

#import "UIViewController+ShowModalFromView.h"

@implementation UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view
{
    modalViewController.modalPresentationStyle = UIModalPresentationFormSheet;

    // Add the modal viewController but don't animate it. We will handle the animation manually
    [self presentModalViewController:modalViewController animated:NO];

    // Remove the shadow. It causes weird artifacts while animating the view.
    CGColorRef originalShadowColor = modalViewController.view.superview.layer.shadowColor;
    modalViewController.view.superview.layer.shadowColor = [[UIColor clearColor] CGColor];

    // Save the original size of the viewController's view    
    CGRect originalFrame = modalViewController.view.superview.frame;

    // Set the frame to the one of the view we want to animate from
    modalViewController.view.superview.frame = view.frame;

    // Begin animation
    [UIView animateWithDuration:1.0f
                     animations:^{
                         // Set the original frame back
                         modalViewController.view.superview.frame = originalFrame;
                     }
                     completion:^(BOOL finished) {
                         // Set the original shadow color back after the animation has finished
                         modalViewController.view.superview.layer.shadowColor = originalShadowColor;
                     }];
}

@end

It's pretty straight forward. Please let me know if this helps you.

UPDATE

I've updated the answer to use animation blocks instead of [UIView beginAnimations:nil context:nil]; / [UIView commitAnimations] pair.



回答2:

Seems like you're essentially after translating (moving) a CALayer while scaling it down and rotating it about the y-axis at the same time. Try this:

NSValue *initialTransformValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
CATransform3D translation = CATransform3DMakeTranslation(finalPoint.x, finalPoint.y, 0.0);
CATransform3D scalingAndTranslation = CATransform3DScale(translation, kMyScalingFactor, kMyScalingFactor, 1.0);
CATransform3D finalTransform = CATransform3DRotate(scalingAndTranslation, myRotationAngle, 0.0, 1.0, 0.0);
NSArray *keyFrameValues = [NSArray arrayWithObjects:initialTransformValue, [NSValue valueWithCATransform3D:finalTransform], nil];
CAKeyframeAnimation *myAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
myAnimation.values = keyFrameValues;
myAnimation.duration = kMyAnimationDuration;
myAnimation.delegate = self;
myAnimation.removedOnCompletion = NO;
myAnimation.fillMode = kCAFillModeForwards;
[myLayer addAnimation:myAnimation forKey:@"myAnimationKey"];
  • finalPoint should be a CGPoint in the coordinate space of myLayer.
  • kMyScalingFactor should be <1.0 for scaling down and >1.0 for scaling up.
  • myRotationAngle should be in radians. Use positive values for rotating clockwise and negative values for counter-clockwise.

You also need to implement an animation termination handler to make the animation "stick":

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
    myLayer.transform = finalTransform;
    myLayer removeAnimationForKey:@"myAnimationKey"];
}

Hope this helps.



回答3:

I've gotten this working before by just animating views.

1) Album artwork is in a grid.
2) Transition the view of the album artwork using the flip animation.
3) Animate the view moving across the screen.

I quickly threw this together. Assuming you have an empty view controller and 3 views.

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self performSelector:@selector(transition) withObject:nil afterDelay:1];
    albumArtworkSquare = [[UIView alloc] initWithFrame:CGRectMake(400, 500, 300, 300)];
    albumArtworkSquare.backgroundColor = [UIColor blackColor];
    [self.view addSubview:albumArtworkSquare];

    frontViewOfAlbumArtwork = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    frontViewOfAlbumArtwork.backgroundColor = [UIColor blueColor];
    [albumArtworkSquare addSubview:frontViewOfAlbumArtwork];

    backViewToTransitionTo = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    backViewToTransitionTo.backgroundColor = [UIColor grayColor];
}

- (void)transition
{

    [UIView animateWithDuration:2 animations:^{
        albumArtworkSquare.frame = CGRectMake(10, 500, 300, 300);
    }];


    [UIView transitionFromView:frontViewOfAlbumArtwork toView:backViewToTransitionTo duration:2 options:UIViewAnimationOptionTransitionFlipFromRight completion:^(BOOL finished)
     {

         [frontViewOfAlbumArtwork removeFromSuperview];
         [albumArtworkSquare addSubview:backViewToTransitionTo];

     }];

}


回答4:

Mihai's answer does not handle flipping as iPad itunes animation does. I changed it a little bit to flip the view. You don't need all kinds of crazy CAAnimation stuff, just a few built-in UIView animation functions.

#import <QuartzCore/QuartzCore.h>
@interface UIViewController (ShowModalFromView)

- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view;

@end
@implementation UIViewController (ShowModalFromView)
- (void)presentModalViewController:(UIViewController *)modalViewController fromView:(UIView *)view
{
    NSTimeInterval scaleSpeed = 0.3;
    NSTimeInterval flipSpeed = 0.4;

    UIView __weak *containerView = view.superview;

    view.autoresizesSubviews = YES;

    [self presentModalViewController:modalViewController animated:NO];

    UIView __weak *presentedView = modalViewController.view.superview;

    //intead of show the actual view of modalViewController, we are showing the snapshot of it to avoid layout problem
    UIGraphicsBeginImageContext(presentedView.bounds.size);
    [presentedView.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage* modalSnapshot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    UIView __weak *originalSuperView = presentedView.superview;   


    CGRect originalFrame = presentedView.frame;
    CGRect frameInContainer = [containerView convertRect:originalFrame fromView:originalSuperView];
    [presentedView removeFromSuperview];
    UIImageView* snapshotView = [[UIImageView alloc] initWithImage:modalSnapshot];
    snapshotView.autoresizingMask = UIViewAutoresizingNone;


    [containerView bringSubviewToFront:view];

    [UIView animateWithDuration:scaleSpeed delay:0 options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionBeginFromCurrentState animations: ^{
        [UIView animateWithDuration:scaleSpeed
                              delay:0
                            options:UIViewAnimationOptionBeginFromCurrentState
                         animations: ^{
                             view.frame = frameInContainer;

                         }

                         completion:nil
         ];


    } completion:^(BOOL finished) {

        [UIView setAnimationBeginsFromCurrentState:YES];
        [UIView transitionWithView:view duration:flipSpeed options:UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionTransitionFlipFromRight animations:
         ^{
             snapshotView.frame = view.bounds;
             [view addSubview:snapshotView];

         } completion:^(BOOL finished) {
             [originalSuperView addSubview:presentedView];
             [snapshotView removeFromSuperview];
         }
         ];
    }];
}


回答5:

You can use this..For Navigation controller based application..

YourViewController *obj = [[YourViewController alloc]initWithNibName:@"YourViewControllerXIBName" bundle:nil];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 1.0];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:YES];
[self.navigationController pushViewController:obj animated:NO];
[UIView commitAnimations];
[obj release];


回答6:

I am doing something quite similar in one of my projects having a grid of album arts. This is the approach I am taking. The key is to use CAAnimationGroup.

1) The whole animation would include scaling, rotating and moving along a path all at the same time - first for the album art layer and then for the modal view layer.

2)Animate the album art layer by flipping it by 90 degrees, scaling a little bit and moving to a predestined location from its current location. At this point it will disappear(vertical to the screen).

3) Add the modal view. Scale and transform the modal view to be in the exact location that the album art is positioned in step 1.

4) Animate the modal view from this position by scaling, rotating and moving along a path to fill the screen.

5) Remove the modal view.

6) Present modal view with no animation.

7) The path chosen would normally add the center of the screen as a control point. But then that can be changed based on how you want the animation to appear.

Below is a function where you can see the use of animation group. Hope this helps you. I still haven't figured out how to avoid the clipping of the animation by the navigation bars and the tab bars though. :)

+ (void)animateWithCurrentView:(UIView *)currentView 
{
    #define kResizeKey @"bounds.size"
    #define kPathMovement @"position"
    #define kRotation @"transform"
    #define kGroupAnimation @"subviewBeingAnimated"
    #define kLayerAnimation @"animateLayer"

    //flip the view by 180 degrees in its place first.
    currentView.layer.transform = CATransform3DRotate(currentView.layer.transform,radians(180), 0, 1, 0);

    //set the anchor point so that the view rotates on one of its sides.
    currentView.layer.anchorPoint = CGPointMake(0.0, 0.5);



    /**
     * Set up scaling
     */
    CABasicAnimation *resizeAnimation = [CABasicAnimation animationWithKeyPath:kResizeKey];

    //we are going to fill the screen here. So 320,480
    [resizeAnimation setToValue:[NSValue valueWithCGSize:CGSizeMake(320, 480)]];
    resizeAnimation.fillMode            = kCAFillModeForwards;
    resizeAnimation.removedOnCompletion = NO;


    /**
     * Set up path movement
     */
    UIBezierPath *movePath = [UIBezierPath bezierPath];

    //the control point is now set to centre of the filled screen. Change this to make the path different.
    CGPoint ctlPoint       = CGPointMake(160.0, 240.0);

    //This is the starting point of the animation. This should ideally be a function of the frame of the view to be animated. Hardcoded here.
    [movePath moveToPoint:CGPointMake(320, 60)];

    //The anchor point is going to end up here at the end of the animation.
    [movePath addQuadCurveToPoint:CGPointMake(0, 240) controlPoint:ctlPoint];

    CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:kPathMovement];

    moveAnim.path                = movePath.CGPath;
    moveAnim.removedOnCompletion = YES;

    /**
     * Setup rotation animation
     */
    CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:kRotation];
    //start from 180 degrees (done in 1st line)
    CATransform3D fromTransform       = CATransform3DMakeRotation(radians(180), 0, 1, 0);
    //come back to 0 degrees
    CATransform3D toTransform         = CATransform3DMakeRotation(radians(0), 0, 1, 0);

    //This is done to get some perspective.
    CATransform3D persp1 = CATransform3DIdentity;
    persp1.m34 = 1.0 / -3000;

    fromTransform = CATransform3DConcat(fromTransform, persp1);
    toTransform = CATransform3DConcat(toTransform,persp1);

    rotateAnimation.toValue             = [NSValue valueWithCATransform3D:toTransform];
    rotateAnimation.fromValue           = [NSValue valueWithCATransform3D:fromTransform];
    //rotateAnimation.duration            = 2;
    rotateAnimation.fillMode            = kCAFillModeForwards;
    rotateAnimation.removedOnCompletion = NO;

    /**
     * Setup and add all animations to the group
     */
    CAAnimationGroup *group = [CAAnimationGroup animation]; 

    [group setAnimations:[NSArray arrayWithObjects:moveAnim,rotateAnimation, resizeAnimation, nil]];

    group.fillMode            = kCAFillModeForwards;
    group.removedOnCompletion = NO;
    group.duration            = 0.7f;
    group.delegate            = self;

    [group setValue:currentView forKey:kGroupAnimation];

    /**
     * ...and go
     */
    [currentView.layer addAnimation:group forKey:kLayerAnimation];

}


回答7:

If you can first get the modal view to show up where you want it w/o animation. Should be strait forward, and setting the bounds/frame of view.superview can be useful on sheet style.

If you get that resolved, then you can perform your "pure view" animation to get it there, then remove the view, and "present" the modal controller (which controls that view) to instantaneously swap the controller into logical control of the view hierarchy.