Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I make an expand/contract transition between views on iOS?

I'm trying to make a transition animation in iOS where a view or view controller appears to expand to fill the whole screen, then contract back to its former position when done. I'm not sure what this type of transition is officially called, but you can see an example in the YouTube app for iPad. When you tap one of the search result thumbnails on the grid, it expands from the thumbnail, then contracts back into the thumbnail when you return to the search.

I'm interested in two aspects of this:

  1. How would you make this effect when transitioning between one view and another? In other words, if view A takes up some area of the screen, how would you transition it to view B which takes up the whole screen, and vice versa?

  2. How would you transition to a modal view this way? In other words, if UIViewController C is currently showing and contains view D which takes up part of the screen, how do you make it look like view D is turning into UIViewController E which is presented modally on top of C?

Edit: I'm adding a bounty to see if that gets this question more love.

Edit: I've got some source code that does this, and Anomie's idea works like a charm, with a few refinements. I had first tried animating the modal controller's view (E), but it didn't produce the effect of feeling like you're zooming into the screen, because it wasn't expanding all the stuff around the thumbnail view in (C). So then I tried animating the original controller's view (C), but the redrawing of it made for a jerky animation, and things like background textures did not zoom properly. So what I wound up doing is taking an image of the the original view controller (C) and zooming that inside the modal view (E). This method is substantially more complex than my original one, but it does look nice! I think it's how iOS must do its internal transitions as well. Anyway, here's the code, which I've written as a category on UIViewController.

UIViewController+Transitions.h:

#import <Foundation/Foundation.h>  @interface UIViewController (Transitions)  // make a transition that looks like a modal view  //  is expanding from a subview - (void)expandView:(UIView *)sourceView          toModalViewController:(UIViewController *)modalViewController;  // make a transition that looks like the current modal view  //  is shrinking into a subview - (void)dismissModalViewControllerToView:(UIView *)view;  @end 

UIViewController+Transitions.m:

#import "UIViewController+Transitions.h"  @implementation UIViewController (Transitions)  // capture a screen-sized image of the receiver - (UIImageView *)imageViewFromScreen {   // make a bitmap copy of the screen   UIGraphicsBeginImageContextWithOptions(     [UIScreen mainScreen].bounds.size, YES,      [UIScreen mainScreen].scale);   // get the root layer   CALayer *layer = self.view.layer;   while(layer.superlayer) {     layer = layer.superlayer;   }   // render it into the bitmap   [layer renderInContext:UIGraphicsGetCurrentContext()];   // get the image   UIImage *image = UIGraphicsGetImageFromCurrentImageContext();   // close the context   UIGraphicsEndImageContext();   // make a view for the image   UIImageView *imageView =      [[[UIImageView alloc] initWithImage:image]       autorelease];    return(imageView); }  // make a transform that causes the given subview to fill the screen //  (when applied to an image of the screen) - (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView {   // get the root view   UIView *rootView = sourceView;   while (rootView.superview) rootView = rootView.superview;   // convert the source view's center and size into the coordinate   //  system of the root view   CGRect sourceRect = [sourceView convertRect:sourceView.bounds toView:rootView];   CGPoint sourceCenter = CGPointMake(     CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));   CGSize sourceSize = sourceRect.size;   // get the size and position we're expanding it to   CGRect screenBounds = [UIScreen mainScreen].bounds;   CGPoint targetCenter = CGPointMake(     CGRectGetMidX(screenBounds),     CGRectGetMidY(screenBounds));   CGSize targetSize = screenBounds.size;   // scale so that the view fills the screen   CATransform3D t = CATransform3DIdentity;   CGFloat sourceAspect = sourceSize.width / sourceSize.height;   CGFloat targetAspect = targetSize.width / targetSize.height;   CGFloat scale = 1.0;   if (sourceAspect > targetAspect)     scale = targetSize.width / sourceSize.width;   else     scale = targetSize.height / sourceSize.height;   t = CATransform3DScale(t, scale, scale, 1.0);   // compensate for the status bar in the screen image   CGFloat statusBarAdjustment =     (([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)        / scale);   // transform to center the view   t = CATransform3DTranslate(t,      (targetCenter.x - sourceCenter.x),      (targetCenter.y - sourceCenter.y) + statusBarAdjustment,      0.0);    return(t); }  - (void)expandView:(UIView *)sourceView          toModalViewController:(UIViewController *)modalViewController {    // get an image of the screen   UIImageView *imageView = [self imageViewFromScreen];    // insert it into the modal view's hierarchy   [self presentModalViewController:modalViewController animated:NO];   UIView *rootView = modalViewController.view;   while (rootView.superview) rootView = rootView.superview;   [rootView addSubview:imageView];    // make a transform that makes the source view fill the screen   CATransform3D t = [self transformToFillScreenWithSubview:sourceView];    // animate the transform   [UIView animateWithDuration:0.4     animations:^(void) {       imageView.layer.transform = t;     } completion:^(BOOL finished) {       [imageView removeFromSuperview];     }]; }  - (void)dismissModalViewControllerToView:(UIView *)view {    // take a snapshot of the current screen   UIImageView *imageView = [self imageViewFromScreen];    // insert it into the root view   UIView *rootView = self.view;   while (rootView.superview) rootView = rootView.superview;   [rootView addSubview:imageView];    // make the subview initially fill the screen   imageView.layer.transform = [self transformToFillScreenWithSubview:view];   // remove the modal view   [self dismissModalViewControllerAnimated:NO];    // animate the screen shrinking back to normal   [UIView animateWithDuration:0.4      animations:^(void) {       imageView.layer.transform = CATransform3DIdentity;     }     completion:^(BOOL finished) {       [imageView removeFromSuperview];     }]; }  @end 

You might use it something like this in a UIViewController subclass:

#import "UIViewController+Transitions.h"  ...  - (void)userDidTapThumbnail {    DetailViewController *detail =      [[DetailViewController alloc]       initWithNibName:nil bundle:nil];    [self expandView:thumbnailView toModalViewController:detail];    [detail release]; }  - (void)dismissModalViewControllerAnimated:(BOOL)animated {   if (([self.modalViewController isKindOfClass:[DetailViewController class]]) &&       (animated)) {      [self dismissModalViewControllerToView:thumbnailView];    }   else {     [super dismissModalViewControllerAnimated:animated];   } } 

Edit: Well, it turns out that doesn't really handle interface orientations other than portrait. So I had to switch to animating the transition in a UIWindow using a view controller to pass along the rotation. See the much more complicated version below:

UIViewController+Transitions.m:

@interface ContainerViewController : UIViewController { } @end  @implementation ContainerViewController   - (BOOL)shouldAutorotateToInterfaceOrientation:           (UIInterfaceOrientation)toInterfaceOrientation {     return(YES);   } @end  ...  // get the screen size, compensating for orientation - (CGSize)screenSize {   // get the size of the screen (swapping dimensions for other orientations)   CGSize size = [UIScreen mainScreen].bounds.size;   if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {     CGFloat width = size.width;     size.width = size.height;     size.height = width;   }   return(size); }  // capture a screen-sized image of the receiver - (UIImageView *)imageViewFromScreen {    // get the root layer   CALayer *layer = self.view.layer;   while(layer.superlayer) {     layer = layer.superlayer;   }   // get the size of the bitmap   CGSize size = [self screenSize];   // make a bitmap to copy the screen into   UIGraphicsBeginImageContextWithOptions(     size, YES,      [UIScreen mainScreen].scale);   CGContextRef context = UIGraphicsGetCurrentContext();   // compensate for orientation   if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {     CGContextTranslateCTM(context, size.width, 0);     CGContextRotateCTM(context, M_PI_2);   }   else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) {     CGContextTranslateCTM(context, 0, size.height);     CGContextRotateCTM(context, - M_PI_2);   }   else if (self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {     CGContextTranslateCTM(context, size.width, size.height);     CGContextRotateCTM(context, M_PI);   }   // render the layer into the bitmap   [layer renderInContext:context];   // get the image   UIImage *image = UIGraphicsGetImageFromCurrentImageContext();   // close the context   UIGraphicsEndImageContext();   // make a view for the image   UIImageView *imageView =      [[[UIImageView alloc] initWithImage:image]       autorelease];   // done   return(imageView); }  // make a transform that causes the given subview to fill the screen //  (when applied to an image of the screen) - (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView                  includeStatusBar:(BOOL)includeStatusBar {   // get the root view   UIView *rootView = sourceView;   while (rootView.superview) rootView = rootView.superview;   // by default, zoom from the view's bounds   CGRect sourceRect = sourceView.bounds;   // convert the source view's center and size into the coordinate   //  system of the root view   sourceRect = [sourceView convertRect:sourceRect toView:rootView];   CGPoint sourceCenter = CGPointMake(     CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));   CGSize sourceSize = sourceRect.size;   // get the size and position we're expanding it to   CGSize targetSize = [self screenSize];   CGPoint targetCenter = CGPointMake(     targetSize.width / 2.0,     targetSize.height / 2.0);    // scale so that the view fills the screen   CATransform3D t = CATransform3DIdentity;   CGFloat sourceAspect = sourceSize.width / sourceSize.height;   CGFloat targetAspect = targetSize.width / targetSize.height;   CGFloat scale = 1.0;   if (sourceAspect > targetAspect)     scale = targetSize.width / sourceSize.width;   else     scale = targetSize.height / sourceSize.height;   t = CATransform3DScale(t, scale, scale, 1.0);   // compensate for the status bar in the screen image   CGFloat statusBarAdjustment = includeStatusBar ?     (([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)        / scale) : 0.0;   // transform to center the view   t = CATransform3DTranslate(t,      (targetCenter.x - sourceCenter.x),      (targetCenter.y - sourceCenter.y) + statusBarAdjustment,      0.0);    return(t); }  - (void)expandView:(UIView *)sourceView          toModalViewController:(UIViewController *)modalViewController {    // get an image of the screen   UIImageView *imageView = [self imageViewFromScreen];   // show the modal view   [self presentModalViewController:modalViewController animated:NO];   // make a window to display the transition on top of everything else   UIWindow *window =      [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];   window.hidden = NO;   window.backgroundColor = [UIColor blackColor];   // make a view controller to display the image in   ContainerViewController *vc = [[ContainerViewController alloc] init];   vc.wantsFullScreenLayout = YES;   // show the window   [window setRootViewController:vc];   [window makeKeyAndVisible];   // add the image to the window   [vc.view addSubview:imageView];    // make a transform that makes the source view fill the screen   CATransform3D t = [self      transformToFillScreenWithSubview:sourceView     includeStatusBar:(! modalViewController.wantsFullScreenLayout)];    // animate the transform   [UIView animateWithDuration:0.4     animations:^(void) {       imageView.layer.transform = t;     } completion:^(BOOL finished) {       // we're going to crossfade, so change the background to clear       window.backgroundColor = [UIColor clearColor];       // do a little crossfade       [UIView animateWithDuration:0.25          animations:^(void) {           imageView.alpha = 0.0;         }         completion:^(BOOL finished) {           window.hidden = YES;           [window release];           [vc release];         }];     }]; }  - (void)dismissModalViewControllerToView:(UIView *)view {    // temporarily remove the modal dialog so we can get an accurate screenshot    //  with orientation applied   UIViewController *modalViewController = [self.modalViewController retain];   [self dismissModalViewControllerAnimated:NO];    // capture the screen   UIImageView *imageView = [self imageViewFromScreen];   // put the modal view controller back   [self presentModalViewController:modalViewController animated:NO];   [modalViewController release];    // make a window to display the transition on top of everything else   UIWindow *window =      [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];   window.hidden = NO;   window.backgroundColor = [UIColor clearColor];   // make a view controller to display the image in   ContainerViewController *vc = [[ContainerViewController alloc] init];   vc.wantsFullScreenLayout = YES;   // show the window   [window setRootViewController:vc];   [window makeKeyAndVisible];   // add the image to the window   [vc.view addSubview:imageView];    // make the subview initially fill the screen   imageView.layer.transform = [self      transformToFillScreenWithSubview:view     includeStatusBar:(! self.modalViewController.wantsFullScreenLayout)];    // animate a little crossfade   imageView.alpha = 0.0;   [UIView animateWithDuration:0.15      animations:^(void) {       imageView.alpha = 1.0;     }     completion:^(BOOL finished) {       // remove the modal view       [self dismissModalViewControllerAnimated:NO];       // set the background so the real screen won't show through       window.backgroundColor = [UIColor blackColor];       // animate the screen shrinking back to normal       [UIView animateWithDuration:0.4          animations:^(void) {           imageView.layer.transform = CATransform3DIdentity;         }         completion:^(BOOL finished) {           // hide the transition stuff           window.hidden = YES;           [window release];           [vc release];         }];     }];  } 

Whew! But now it looks just about like Apple's version without using any restricted APIs. Also, it works even if the orientation changes while the modal view is in front.

like image 733
Jesse Crossen Avatar asked Aug 18 '11 15:08

Jesse Crossen


People also ask

What is IOS transition?

Transition animations provide visual feedback about changes to your app's interface. UIKit provides a set of standard transition styles to use when presenting view controllers, and you can supplement the standard transitions with custom transitions of your own.


1 Answers

  1. Making the effect is simple. You take the full-sized view, initialize its transform and center to position it on top of the thumbnail, add it to the appropriate superview, and then in an animation block reset the transform and center to position it in the final position. To dismiss the view, just do the opposite: in an animation block set transform and center to position it on top of the thumbnail, and then remove it completely in the completion block.

    Note that trying to zoom from a point (i.e. a rectangle with 0 width and 0 height) will screw things up. If you're wanting to do that, zoom from a rectangle with width/height something like 0.00001 instead.

  2. One way would be to do the same as in #1, and then call presentModalViewController:animated: with animated NO to present the actual view controller when the animation is complete (which, if done right, would result in no visible difference due to the presentModalViewController:animated: call). And dismissModalViewControllerAnimated: with NO followed by the same as in #1 to dismiss.

    Or you could manipulate the modal view controller's view directly as in #1, and accept that parentViewController, interfaceOrientation, and some other stuff just won't work right in the modal view controller since Apple doesn't support us creating our own container view controllers.

like image 151
Anomie Avatar answered Oct 14 '22 18:10

Anomie