Как я могу сделать переход расширения / контракта между представлениями на iOS? - PullRequest
37 голосов
/ 18 августа 2011

Я пытаюсь создать анимацию перехода в iOS, где контроллер представления или представления расширяется, чтобы заполнить весь экран, а затем сжимается назад к своей прежней позиции. Я не уверен, как официально называется этот тип перехода, но вы можете увидеть пример в приложении YouTube для iPad. Когда вы нажимаете на одну из миниатюр результатов поиска в сетке, она расширяется от миниатюры, а затем возвращается к миниатюре, когда вы возвращаетесь к поиску.

Меня интересуют два аспекта этого:

  1. Как бы вы создали этот эффект при переходе от одного вида к другому? Другими словами, если представление A занимает некоторую область экрана, как бы вы перевели ее в представление B, которое занимает весь экран, и наоборот?

  2. Как бы вы перешли к модальному виду таким образом? Другими словами, если UIViewController C в настоящее время показывает и содержит представление D, которое занимает часть экрана, как вы делаете так, чтобы представление D превращалось в UIViewController E, который представлен модально поверх C?

Редактировать: Я добавляю награду, чтобы посмотреть, получит ли этот вопрос больше любви.

Редактировать: У меня есть некоторый исходный код, который делает это, и идея Аноми работает как шарм, с некоторыми уточнениями. Сначала я попытался анимировать вид модального контроллера (E), но он не давал ощущения, будто вы приближаетесь к экрану, потому что он не расширял все элементы, связанные с миниатюрами в (C). Затем я попытался анимировать исходный вид контроллера (C), но его перерисовка делала резкую анимацию, а такие вещи, как фоновые текстуры, не масштабировались должным образом. Итак, я сделал снимок оригинального контроллера вида (C) и увеличил его внутри модального вида (E). Этот метод значительно сложнее, чем мой оригинальный, но выглядит хорошо! Я думаю, что именно так iOS должна делать и свои внутренние переходы. В любом случае, вот код, который я написал как категорию на 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

Вы можете использовать это примерно так в подклассе 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];
  }
}

Редактировать: Ну, оказывается, что на самом деле не обрабатывает ориентации интерфейса, кроме книжной ориентации. Поэтому мне пришлось переключиться на анимацию перехода в окне UIWindow, используя контроллер представления для передачи вращения. Смотрите гораздо более сложную версию ниже:

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

}

Уф! Но теперь это выглядит примерно как версия Apple без использования каких-либо ограниченных API. Кроме того, это работает, даже если ориентация изменяется, когда модальное представление находится впереди.

Ответы [ 2 ]

13 голосов
/ 25 августа 2011
  1. Сделать эффект просто. Вы берете полноразмерный вид, инициализируете его transform и center, чтобы расположить его поверх миниатюры, добавляете его к соответствующему суперпредставлению, а затем в блоке анимации сбрасываете transform и center для позиционирования. это в финальной позиции. Чтобы закрыть вид, просто сделайте наоборот: в анимационном блоке установите transform и center, чтобы поместить его поверх эскиза, а затем полностью удалите его в блоке завершения.

    Обратите внимание, что попытка масштабирования от точки (то есть прямоугольника с 0 шириной и 0 высотой) испортит вещи. Если вы хотите сделать это, вместо этого увеличьте масштаб прямоугольника с шириной / высотой, например, 0,00001.

  2. Одним из способов было бы сделать то же самое, что и в # 1, а затем вызвать presentModalViewController:animated: с анимированным NO, чтобы представить фактический контроллер представления, когда анимация завершена (что, если все сделано правильно, приведет к видимая разница из-за вызова presentModalViewController:animated:). И dismissModalViewControllerAnimated: с NO, за которым следует то же самое, что и в # 1, чтобы закрыть.

    Или вы можете манипулировать представлением контроллера модального представления напрямую, как в # 1, и принять, что parentViewController, interfaceOrientation и некоторые другие вещи просто не будут работать прямо в контроллере модального представления, так как Apple не поддерживает мы создаем наши собственные контроллеры представления контейнера.

9 голосов
/ 25 августа 2011

После просмотра анимации Youtube для iPad я понял, что это всего лишь иллюзия. Допустим, есть SearchViewController для результатов поиска, DetailViewController для самого видео и дополнительная информация о видео.

DetailViewController имеет метод, подобный - (id)initWithFullscreen, который запускает контроллер представления, используя полноэкранное пространство с видео.

Итак, последовательность выглядит так:

  1. SearchViewController представляет свои результаты.
  2. Пользователь нажимает на видео.
  3. DetailViewController создается с initWithFullscreen, но не представлен
  4. Начинается анимация «Увеличение». (Обратите внимание, что мы все еще находимся на SearchViewController, и эта анимация является простой анимацией просмотра)
  5. Анимация "Увеличение масштаба" заканчивается, представляет DetailViewController с animated:NO (как упоминала Аномия).
  6. DetailViewController теперь представлен и использует полный пробел.

Не похоже, что приложение youtube делает что-то более причудливое, что было связано с тем, что анимация «Увеличение» масштабируется до черного квадрата перед представлением полного видео.

...