vida ciclo appstore apple app ios animation uiview transition modalviewcontroller

ciclo - ¿Cómo hago una transición de expansión/contrato entre las vistas en iOS?



connect appstore (2)

Estoy tratando de hacer una animación de transición en iOS donde un controlador de vista o vista parece expandirse para llenar toda la pantalla, y luego volver a contratar a su posición anterior cuando haya terminado. No estoy seguro de cómo se llama oficialmente este tipo de transición, pero puede ver un ejemplo en la aplicación de YouTube para iPad. Cuando toca una de las miniaturas de resultados de búsqueda en la cuadrícula, se expande desde la miniatura y luego vuelve a contraerse en la miniatura cuando vuelve a la búsqueda.

Me interesan dos aspectos de esto:

  1. ¿Cómo harías esto al hacer la transición entre una vista y otra? En otras palabras, si la vista A ocupa un área de la pantalla, ¿cómo la pasaría a la vista B, que ocupará toda la pantalla, y viceversa?

  2. ¿Cómo pasaría a una vista modal de esta manera? En otras palabras, si UIViewController C se muestra actualmente y contiene la vista D que ocupa parte de la pantalla, ¿cómo se ve como que la vista D se está convirtiendo en UIViewController E, que se presenta modalmente sobre C?

Editar: Estoy agregando una recompensa para ver si eso hace que esta pregunta sea más amorosa.

Editar: Tengo un código fuente que hace esto, y la idea de Anomie funciona como un amuleto, con algunos refinamientos. Primero traté de animar la vista del controlador modal (E), pero no produjo el efecto de sentir que estás enfocando en la pantalla, porque no estaba expandiendo todo el contenido de la vista de miniaturas en (C). Entonces traté de animar la vista del controlador original (C), pero el redibujado de la misma creó una animación espasmódica, y las texturas de fondo no se ampliaron correctamente. Entonces, lo que terminé haciendo es tomar una imagen del controlador de vista original (C) y acercarlo a la vista modal (E). Este método es sustancialmente más complejo que el original, ¡pero se ve bien! Creo que es así como iOS debe hacer sus transiciones internas también. De todos modos, aquí está el código, que he escrito como una categoría en 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

Puede usar algo como esto en una subclase UIViewController:

#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]; } }

Editar: Bueno, resulta que realmente no maneja orientaciones de interfaz que no sean retratos. Así que tuve que cambiar a la animación de la transición en una ventana UI usando un controlador de vista para pasar la rotación. Vea la versión mucho más complicada a continuación:

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]; }]; }]; }

¡Uf! Pero ahora se parece a la versión de Apple sin usar ninguna API restringida. Además, funciona incluso si la orientación cambia mientras que la vista modal está al frente.


  1. Hacer el efecto es simple. Usted toma la vista de tamaño completo, inicializa su transform y se center para colocarla en la parte superior de la miniatura, agregarla a la supervista apropiada y luego, en un bloque de animación, reiniciar la transform y center para colocarla en la posición final. Para descartar la vista, simplemente haga lo contrario: en un bloque de animación, configure transform y center para colocarlo en la parte superior de la miniatura, y luego elimínelo por completo en el bloque de finalización.

    Tenga en cuenta que intentar hacer zoom desde un punto (es decir, un rectángulo con 0 de ancho y 0 de altura) arruinará las cosas. Si quiere hacerlo, amplíe desde un rectángulo con ancho / alto algo así como 0.00001 en su lugar.

  2. Una forma sería hacer lo mismo que en # 1, y luego llamar a presentModalViewController:animated: con NO animado para presentar el controlador de vista real cuando la animación esté completa (lo cual, si se hace bien, no daría lugar a diferencias visibles debido a la presentModalViewController:animated: llamada). Y dismissModalViewControllerAnimated: con NO seguido de lo mismo que en el # 1 para descartar.

    O bien, podría manipular la vista del controlador de vista modal directamente como en # 1, y aceptar que parentViewController , interfaceOrientation y algunas otras cosas simplemente no funcionarán en el controlador de vista modal ya que Apple no nos permite crear nuestros propios controladores de vista de contenedor .


Después de ver la animación del iPad de Youtube, descubrí que es solo una ilusión. Digamos que hay un SearchViewController para los resultados de búsqueda, y un DetailViewController para el video en sí, y la información adicional del video.

DetailViewController tiene un método como - (id)initWithFullscreen que inicia el controlador de vista utilizando el espacio de pantalla completa con el video.

Entonces la secuencia es así:

  1. SearchViewController presenta sus resultados.
  2. El usuario hace clic en un video.
  3. DetailViewController se crea con initWithFullscreen , pero no se presenta
  4. Comienza la animación "Acercar". (Observe que todavía estamos en SearchViewController, y esta animación es solo una simple animación de Vista)
  5. La animación "Zoom in" finaliza, presenta DetailViewController con animated:NO (como Anomie mencionó).
  6. El DetailViewController ahora se presenta y usa espacio completo.

No parece que la aplicación de youtube esté haciendo algo más elegante, el regalo fue que la animación "Acercar" se acerca a un cuadrado negro, antes de presentar el video completo.