2011-08-18 9 views
35

Estoy tratando de hacer una animación de transición en iOS donde aparece una vista o controlador de vista para expandirse para llenar toda la pantalla, luego contraer de nuevo 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.¿Cómo realizo una transición de expansión/contrato entre las vistas en iOS?

Estoy interesado en dos aspectos de este:

  1. ¿Cómo le haría este efecto en la transición entre uno y otro punto de vista? 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 añadiendo una recompensa para ver si eso llega a esta pregunta más amor.

Edición: 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 rediseño de la misma hizo una animación desigual, y cosas como 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 

podría utilizar 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 doesn Realmente maneja orientaciones de interfaz que no sean verticales. 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. Ver la versión mucho más complicado 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.

Respuesta

12
  1. Haciendo el efecto es simple.Tome la vista de tamaño completo, inicialice su transform y center para colocarla en la parte superior de la miniatura, agréguelo a la supervista adecuada y luego, en un bloque de animación, reinicie transform y center para colocarlo en la posición final. Para descartar la vista, simplemente haga lo contrario: en un bloque de animación, establezca transform y center para colocarlo en la parte superior de la miniatura, y luego eliminarlo 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 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) a la llamada presentModalViewController:animated:). Y dismissModalViewControllerAnimated: con NO seguido de lo mismo que en el # 1 para descartar.

    O usted podría manipular la opinión del controlador de vista modal directamente como en el # 1, y aceptar que parentViewController, interfaceOrientation, y algunas otras cosas simplemente no va a funcionar justo en el controlador de vista modal ya que Apple no admite la creación de nuestra nosotros poseer controladores de vista de contenedor.

+0

Gracias. No estoy seguro de qué íbamos a animar en la parte 2. ¿Es posible animar la vista del controlador de vista modal antes de presentarla de forma modal? En este caso, debería agregarse como una subvista del controlador de vista que se muestra actualmente, ¿verdad? Estaría muy interesado en ver algún código fuente para esto. –

+0

@JesseCrossen: por primera vez en el n. ° 2, estaría animando una vista que se parece a la vista del controlador de vista modal. Podría ser solo una "captura de pantalla" creada con el 'renderInContext:' de CALayer. Para la segunda forma, estaría manipulando la vista del controlador de vista directamente. – Anomie

+0

¡Gracias por aclarar! Descubrí que la manipulación no estándar de los controladores de visualización puede ser espinosa en la práctica, por lo que pregunté sobre el código fuente.Veré cómo funciona y te lo haré saber. –

9

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.

Así que la secuencia es así:

  1. SearchViewController presenta sus resultados.
  2. El usuario hace clic en un video.
  3. DetailViewController se ha creado con initWithFullscreen, pero no se ha presentado
  4. Comienza la animación "Acercar". (Tenga en cuenta que todavía estamos en SearchViewController, y esta animación es simplemente una animación de Vista)
  5. Termina la animación "Acercar", presenta DetailViewController con animated:NO (como se menciona Anomie).
  6. Ahora se presenta DetailViewController y utiliza 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.

+0

¡Buen ojo! Se ve de esa manera en modo paisaje, pero pruébalo en retrato. El efecto es increíblemente suave, con los detalles textuales de la miniatura pasando a una descripción más expandida. Creo que pueden estar usando dos técnicas diferentes, donde en modo apaisado se expande un recuadro negro para cubrir la pantalla y se intercambia con una vista modal, y en modo retrato la miniatura se expande para llenar los límites de su vista principal, luego se visualiza el detalle empujado a la pila de navegación sin animación (o tal vez solo un pequeño fundido cruzado). –

+0

Por cierto, esta respuesta es realmente antigua, puedes lograr todas estas geniales animaciones de pantalla completa a través de instantáneas, transiciones modales/push personalizadas o incluso UIStoryboardSegues personalizados – Can

Cuestiones relacionadas