自定義轉(zhuǎn)場動(dòng)畫

寫在前面

本文中提到的 presented 和 presenting ,分別指被展示者和展示者,譬如 [viewControllerA presentViewController:viewControllerB animated:NO] 中,viewControllerA 是 presenting,viewControllerB 是 presented。

從熟悉的地方開始

蘋果提供的幾種轉(zhuǎn)場動(dòng)畫,可以用 UIModalPresentationStyleUIModalTransitionStyle 來設(shè)置,從 presentation 和 transition 兩個(gè)關(guān)鍵詞的語意來理解轉(zhuǎn)場動(dòng)畫,presentation 重靜態(tài)的展示,可通過 UIModalPresentationStyle 來定義 presented view 的展示形式(全屏、formsheet、popover 等);transition 重動(dòng)態(tài)的轉(zhuǎn)場,可通過 UIModalTransitionStyle 來定義從 presenting view 上呈現(xiàn) presented view 的動(dòng)畫。

UIModalPresentationStyle Discussion
UIModalPresentationFullScreen 全屏展示presented view,presenting view 在轉(zhuǎn)場動(dòng)畫完成后被移除
UIModalPresentationPageSheet horizontally compact 場景下同 UIModalPresentationFullScreen;horizontally regular 場景下 presented view 的 width 和 height 等于 presenting view 在 portrait 模式下的 width 與 height;未遮擋部分虛化禁止用戶交互
UIModalPresentationFormSheet horizontally compact 場景下同 UIModalPresentationFullScreen; horizontally regular 場景下,presented view 的長寬小于 screen 、居中顯示,landscape mode 下presented view 會(huì)隨著鍵盤的彈出而上移;未遮擋部分虛華禁止用戶交互
UIModalPresentationCurrentContext 轉(zhuǎn)場動(dòng)畫開始前,UIKit 開始從 presenting view controller 向上尋找,presented view controller 的內(nèi)容將覆蓋第一個(gè)找到的 definesPresentationContext = YES 的 view controller 的內(nèi)容;轉(zhuǎn)場動(dòng)畫結(jié)束時(shí),被覆蓋的內(nèi)容將被移除
UIModalPresentationCustom 將轉(zhuǎn)場動(dòng)畫交由 view controller 的 transitioningDelegate 對象來管理
UIModalPresentationOverFullScreen presented view 覆蓋的 content 不會(huì)從 view hierarchy 中移除,未被遮擋的部分對用戶可見
UIModalPresentationOverCurrentContext CurrentContext 和 OverFullScreen 的結(jié)合
UIModalPresentationPopover horizontally regular 場景下, popover 展示;horizontally compact 場景下,同 FullScreen
UIModalTransitionStyle Discussion
UIModalTransitionStyleCoverVertical 默認(rèn)值,從屏幕底部上推
UIModalTransitionStyleFlipHorizontal 旋轉(zhuǎn)門 自己體會(huì)
UIModalTransitionStyleCrossDissolve 漸入漸出
UIModalTransitionStylePartialCurl 浮夸的翻頁

(transition stylepresentation stylepresented view controller 來設(shè)置,這也符合 presented one 不應(yīng)該依賴 presenting one 的思想。)

如果上述預(yù)定義轉(zhuǎn)場動(dòng)畫不能滿足你,那么需要實(shí)現(xiàn)自定義動(dòng)畫;自定義動(dòng)畫主要通過實(shí)現(xiàn)或集成了 UIViewControllerAnimatedTransitioning 協(xié)議和 UIPresentationController 類的動(dòng)畫控制器 animator 來實(shí)現(xiàn),稍作了解后,你會(huì)發(fā)現(xiàn) UIViewControllerAnimatedTransitioningUIModalTransitionStyle 相似, UIPresentationControllerUIModalPresentationStyle 相似。

預(yù)定義轉(zhuǎn)場動(dòng)畫通過給 UIViewControllermodalPresentationStylemodalTransitionStyle 屬性賦值來實(shí)現(xiàn),而自定義轉(zhuǎn)場動(dòng)畫則須是令 modalPresentationStyle = UIModalPresentationStyleCustom ,并且給 transitioningDelegate 賦值一個(gè)繼承了 UIViewControllerTransitioningDelegate 的對象 (初次使用時(shí)容易把 UIViewControllerAnimatedTransitioningUIViewControllerTransitioningDelegate 弄混,其實(shí)二者是截然不同的概念),繼而在此對象的協(xié)議方法中分配動(dòng)畫控制器。通常我們使用當(dāng)前的 view controller 來實(shí)現(xiàn) UIViewControllerTransitioningDelegate 協(xié)議,具體代碼如下:

@interface ViewController () <UIViewControllerTransitioningDelegate>
@end

@implementation ViewController 

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
  if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) != nil) {
    [self setModalPresentationStyle:UIModalPresentationCustom];
    [self setTransitioningDelegate:self];
  }

  return self;
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
  return nil; // return a transition animator  
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
  return nil; // return a transition animator
}

- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source
{
  return nil; // return a presentation animator
}

上述代碼中,animationControllerForPresentedController 要求 programer 返回一個(gè) present 當(dāng)前 ViewController 的動(dòng)畫控制器,animationControllerForDismissController 要求 coder 返回一個(gè) dismiss 當(dāng)前 view controller 的動(dòng)畫控制器,如何編寫動(dòng)畫控制器就是我們要發(fā)揮想象力的地方了。

好在蘋果給了一個(gè)很好用的協(xié)議,就是上文中提到的 UIViewControllerAnimatedTransitioning ,它有以下方法:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;

  • calls when presenting or dismissing a view controller
  • configure your custom transition in it
  • use view-based animation or core animation as you like
  • all animations must take place in view specified by the containerView property of transitionContext

animateTransition: 前三段解釋都好理解,第四條提出了一個(gè) containerView 的屬性, containerView 是轉(zhuǎn)場動(dòng)畫中涉及的所有 view 的 superview,且 containerView 默認(rèn)添加了 presenting view controller 的 view ,你要做的事情是將 presented view controller 的 view 也添加到 containerView 上。
(不得不說 containerView 是 UIKit 作的很大的一個(gè)改變,在 iOS5 之前,view controller 被蘋果封裝得“緊密嚴(yán)合”,雖然知道不同 view controller 之前的轉(zhuǎn)場不過是 view 的疊加,但我們不能真的用[presentingViewController.view addSubview:presentedViewController.view]這樣的代碼來替代 presentViewController:方法,而在 animateTransition: 方法中,你可以真切地感受到 view 的 present 與 dismiss 是通過 addSubview:removeFromSuperView 來完成的,不同之處是 containerView 必須是所有 view 的 "container")

- (void)animationEnded:(BOOL)transitionCompleted;

  • transitionCompleted = YES if the transition completed successfully and the new view controller is displayed
  • transitionCompleted = NO if the transition is canceled and the original view is still visible
  • use this method to perform any final cleanup operations required by your transition animator

animationEnded: 類似 completion handlerfailure handler

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;

  • return the duration in seconds of your custom transition
  • the value you provide should be the same value that you use when configuring the animations in your animateTransition: method.
  • UIKit uses the value to synchronize the actions of other objects that might be involved in the transition

了解了必要的知識以后,開始做真正有意思的事情吧~
(如果你想了解實(shí)現(xiàn)思路,可以瀏覽后文中的 GIF 圖和偽代碼;如果想要看到具體的實(shí)現(xiàn),可以使用【真·代碼閱讀術(shù)】前往 JYCustomTransition 哦)

仿iPhone相冊-點(diǎn)擊圖片放大的轉(zhuǎn)場動(dòng)畫
photo-compressed.gif

深情凝視 iOS 系統(tǒng)自帶相冊,會(huì)發(fā)現(xiàn)在照片流里 "點(diǎn)擊一張照片放大查看" 的過程,看似是 UIImageViewframe 發(fā)生了變化,實(shí)際上是 UICollectionViewController 切換到了 UIPageViewController 。當(dāng)然這只是沒找到源代碼之后基于觀察的猜測,兩個(gè)不同 view controller 切換更符合 Cocoa Frameworks 中一貫的各司其職的原則。
那么,仿制一個(gè)蘋果系統(tǒng)相冊,coder 須要做的是將 presentViewController 的具體過程委托給自定義的第三方(也就是上文提到的動(dòng)畫控制器)來實(shí)現(xiàn),以達(dá)到欺騙用戶眼球的效果。重點(diǎn)在于我們看到的 “整齊排列的照片中一張照片被放大” 的效果,筆者的做法是創(chuàng)建一個(gè)獨(dú)立于 viewControlleranimateImageView ,設(shè)置 animateImageView 的隱式動(dòng)畫來欺騙眼球,動(dòng)畫完成后即 remove,偽代碼如下:

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
     UIImage *image = getImageFromPhotoGraphy();
     UIImageView *animateImageView = [[UIImageView alloc] initWithImage:image];
     [animateImageView setFrame:getInitialFrameFromPhotoGraphy()];
     [containerView addSubview:animateImageView ];

     [UIView animateWithDuration:0.5 animations:^{
         [animateImageView setFrame:getFinalFrameFromPhotoGraphy()];
     } completion: ^(BOOL finished) {
         [animateImageView removeFromSuperView]
     }]
}

在具體實(shí)現(xiàn)的過程中,筆者還考慮了不同圖片尺寸的修正來達(dá)到視覺上的流暢效果,感興趣的小伙伴可以前往 JYCustomTransition 查看~

為了欺騙而欺騙-開門/關(guān)門的轉(zhuǎn)場動(dòng)畫

首先直接上GIF圖吧

door_effect-iloveimg-compressed.gif

依然是 “看起來” 是一張圖片被切割、被旋轉(zhuǎn),實(shí)際上是 view controller 之間的轉(zhuǎn)場。
(筆者在敲代碼的時(shí)候想了想,非游戲類的 App 應(yīng)該不會(huì)特意做這么花哨并且毫無意義的轉(zhuǎn)場動(dòng)畫,不過最后還是決定做出來,一個(gè)是為了向小伙伴們展示 coder 們?yōu)榱似垓_用戶眼球可以做到什么地步,二是為了介紹轉(zhuǎn)場過程中用到的干貨 - CATransform 3D,有很多漂亮的轉(zhuǎn)場效果都有小小的用到 3D Animation 哦)
具體的實(shí)現(xiàn)過程是用 UIKit 提供的 UIGraphicsGetImageFromCurrentImageContext()presenting view controller(雪山背景那個(gè))截屏生成 snapshotImage,通過對snapshotImage切割生成的 UIImageView 設(shè)置 transform 屬性來實(shí)現(xiàn)旋轉(zhuǎn)開門/關(guān)門效果,偽代碼如下:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIImage *snapshotImage = getSnapshotImageFromPresentingViewController();

    UIImageView *leftImageView = cropImageViewFromLeftSnapshotImage();
    [[leftImageView layer] setAnchorPoint:CGPointMake(0.0, 0.5)];
    [leftImageView setFrame:leftRect];
    [containerView insertSubview:leftImageView aboveSubview:toView];

    UIImageView *rightImageView = cropImageViewFromRightSnapshotImage();
    [[rightImageView layer] setAnchorPoint:CGPointMake(1.0, 0.5)];
    [rightImageView setFrame:rightRect];
    [containerView insertSubview:rightImageView aboveSubview:toView];

    CATransform3D leftRotateTransform = CATransform3DIdentity;
    leftRotateTransform.m34 = 4.5 / -2000;
    leftRotateTransform = CATransform3DRotate(leftRotateTransform, 90.0 * M_PI / 180.0f, 0, 1.0, 0);

    CATransform3D rightRotateTransform = CATransform3DIdentity;
    rightRotateTransform.m34 = 4.5 / -2000;
    rightRotateTransform = CATransform3DRotate(rightRotateTransform, -90.0 * M_PI / 180.0f, 0, 1.0, 0);

    [UIView animateWithDuration:0.5 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
      [[leftImageView layer] setTransform:leftRotateTransform];
      [[rightImageView layer] setTransform:rightRotateTransform];
    } completion:^(BOOL finished) {
       [leftImageView removeFromSuperview];
      [rightImageView removeFromSuperview];
    }];

}

筆者在具體實(shí)現(xiàn)過程遇到了多個(gè) layer 相互遮擋的問題,后來發(fā)現(xiàn)是由于 [leftImageView layer][rightImageView layer]z 坐標(biāo)小于 presented view controllerlayerz 坐標(biāo)的緣故,舉一反三的小伙伴們可以停下來想想怎么解決這個(gè)問題,也可以空降到 JYCustomTransition 去看看具體的解決方法哦

終于用到了UIPresentationController-彈出便箋的轉(zhuǎn)場動(dòng)畫

memo-compressed.gif

最后這個(gè)看起來最樸素的動(dòng)畫包含了 UIViewControllerAnimatedTransitioning 、UIPresentationController 兩大主力, UIViewControllerAnimatedTransitioning 實(shí)現(xiàn)轉(zhuǎn)場中的 frame 變化,偽代碼如下:

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
  UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
  UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
  UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
  UIView *containerView = [transitionContext containerView];

  const CGRect endFrame = [transitionContext finalFrameForViewController:toViewController];
  const CGRect startFrame = CGRectOffset(endFrame, 0.0, CGRectGetHeight(endFrame));

  [toView setFrame:startFrame];
  [toView setAlpha:0.0f];
  [containerView addSubview:toView];

  [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseIn animations:^{
    [toView setFrame:endFrame];
    [toView setAlpha:1.0];
  } completion:^(BOOL finished) {
    if ([transitionContext transitionWasCancelled]) {
      [toView removeFromSuperview];
    }
    [transitionContext completeTransition:YES];
  }];
}

而展示的效果則是在 UIPresentationController 的子類實(shí)現(xiàn),設(shè)置了 presented view controller 中的 viewframeportrait 模式下固定高度 500,landscape 模式下占據(jù)全屏),并為 view 加了一個(gè)半透明的背景 backgroundView ,這樣在 presented view controller 的初始化方法中就可以放心大膽的使用 [[self view] addSubview:subview] 而不用擔(dān)心 subview 相對上邊界的偏移,偽代碼如下:

@implementation _JYMemoViewController_PresentationController

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController
{
  if ((self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController]) != nil) {
    _backgroundView = [[UIView alloc] initWithFrame:CGRectZero];
    [_backgroundView setUserInteractionEnabled:NO];
    [_backgroundView setTranslatesAutoresizingMaskIntoConstraints:NO];

    _tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTapGestureRecognizer:)];
    [_tapGestureRecognizer setDelegate:self];
  }

  return self;
}

- (CGRect)frameOfPresentedViewInContainerView
{
  CGFloat height = fmin(CGRectGetHeight([[self containerView] frame]), 500.0);
  return CGRectMake(0.0, CGRectGetMaxY([[self containerView] frame]) - height, CGRectGetWidth([[self containerView] frame]), height);
}

- (void)presentationTransitionWillBegin
{
  [super presentationTransitionWillBegin];

  [_backgroundView setBackgroundColor:[UIColor colorWithWhite:0.0 alpha:0.5]];
  [_backgroundView setAlpha:0.0];
  [[self containerView] addSubview:_backgroundView];

  [[[_backgroundView topAnchor] constraintEqualToAnchor:[[self containerView] topAnchor]] setActive:YES];
  [[[_backgroundView bottomAnchor] constraintEqualToAnchor:[[self containerView] bottomAnchor]] setActive:YES];
  [[[_backgroundView leadingAnchor] constraintEqualToAnchor:[[self containerView] leadingAnchor]] setActive:YES];
  [[[_backgroundView trailingAnchor] constraintEqualToAnchor:[[self containerView] trailingAnchor]] setActive:YES];

  [[[self presentingViewController] transitionCoordinator] animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext>  context) {
    [_backgroundView setAlpha:1.0];
  } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    if ([context isCancelled]) {
      [[[self presentedView] layer] setShadowOpacity:0.0f];
      [_backgroundView setAlpha:0.0];
    }
  }];
}

- (void)presentationTransitionDidEnd:(BOOL)completed
{
  if (completed) {
    [[self containerView] addGestureRecognizer:_tapGestureRecognizer];
  }

  [super presentationTransitionDidEnd:completed];
}

- (void)dismissalTransitionWillBegin
{
  [super dismissalTransitionWillBegin];

  [[self containerView] removeGestureRecognizer:_tapGestureRecognizer];

  [[[self presentingViewController] transitionCoordinator] animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    [[[self presentedView] layer] setShadowOpacity:0.0f];
    [_backgroundView setAlpha:0.0];
  } completion:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    if ([context isCancelled]) {
      [[[self presentedView] layer] setShadowOpacity:1.0f];
      [_backgroundView setAlpha:1.0];
    }
    else {
      [_backgroundView removeFromSuperview];
    }
  }];
}

- (void)dismissalTransitionDidEnd:(BOOL)completed
{
  if (!completed) {
    [[self containerView] addGestureRecognizer:_tapGestureRecognizer];
  }

  [super dismissalTransitionDidEnd:completed];
}

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

  [coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
    [[self presentedView] setFrame:CGRectMake(CGRectGetMinX([[self containerView] frame]),  CGRectGetMinY([[self containerView] frame]) + fmax(size.height - 500.0, 0.0), CGRectGetWidth([[self containerView] frame]), CGRectGetHeight([[self containerView] frame]) - fmax(size.height - 500.0, 0.0))];
  } completion:nil];
}

- (void)_handleTapGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
  [[self presentingViewController] dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
  if (gestureRecognizer == _tapGestureRecognizer) {
    if ([touch view] != nil) {
      return [touch view] != [self presentedView] && ![[touch view] isDescendantOfView:[self presentedView]];
    }
  }
  
  return YES;
}

@end

依然可以前往 JYCustomTransition 看到全部代碼哦

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容