iOS 轉(zhuǎn)場動(dòng)畫

轉(zhuǎn)場動(dòng)畫,就是Vc切換過程中的過渡動(dòng)畫。
官方支持以下幾種方式的自定義轉(zhuǎn)場:
1、我們最常見的在 UINavigationController 中 push 和 pop;
2、也是比較常見的在 UITabBarController 中切換 Tab;
3、Modal 轉(zhuǎn)場:presentation 和 dismissal;
4、UICollectionViewController 的布局轉(zhuǎn)場:UICollectionViewController 與 UINavigationController 結(jié)合的轉(zhuǎn)場方式;

如果需要更定制化的動(dòng)畫就需要自定義設(shè)計(jì)轉(zhuǎn)場了。本文集中于presentation-dismissal的自定義轉(zhuǎn)場動(dòng)畫。

首先這里介紹一下系統(tǒng)自帶的動(dòng)畫效果

@property(nonatomic,assign) UIModalTransitionStyle modalTransitionStyle NS_AVAILABLE_IOS(3_0);
@property(nonatomic,assign) UIModalPresentationStyle modalPresentationStyle NS_AVAILABLE_IOS(3_2);

UIViewController有modalTransitionStyle和modalPresentationStyle這兩個(gè)屬性,

  • modalTransitionStyle 系統(tǒng)自帶的幾種過渡動(dòng)畫。
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
        UIModalTransitionStyleCoverVertical = 0,      //自下而上覆蓋
        UIModalTransitionStyleFlipHorizontal __TVOS_PROHIBITED,    //翻轉(zhuǎn)
        UIModalTransitionStyleCrossDissolve,        //漸顯
        UIModalTransitionStylePartialCurl NS_ENUM_AVAILABLE_IOS(3_2)   __TVOS_PROHIBITED,    //翻頁效果
};
  • modalPresentationStyle 當(dāng)你用present的方式呈現(xiàn)一個(gè)viewController的時(shí)候,可以設(shè)置將要彈出的viewcontroller的展示樣式。
typedefNS_ENUM(NSInteger, UIModalPresentationStyle) {
      UIModalPresentationFullScreen =0,    //全屏覆蓋
      UIModalPresentationPageSheet,//在portrait時(shí)是FullScreen,在landscape時(shí)和FormSheet模式一樣。
      UIModalPresentationFormSheet,// 會(huì)將窗口縮小,使之居于屏幕中間。在portrait和landscape下都一樣,但要注意landscape下如果軟鍵盤出現(xiàn),窗口位置會(huì)調(diào)整。
      UIModalPresentationCurrentContext,//這種模式下,presented VC的彈出方式和presenting VC的父VC的方式相同。
      UIModalPresentationCustom,//自定義視圖展示風(fēng)格,由一個(gè)自定義演示控制器和一個(gè)或多個(gè)自定義動(dòng)畫對(duì)象組成。符合UIViewControllerTransitioningDelegate協(xié)議。使用視圖控制器的transitioningDelegate設(shè)定您的自定義轉(zhuǎn)換。presentingVc的視圖不會(huì)被移除
      UIModalPresentationOverFullScreen,//presentingVc的視圖不會(huì)被移除,如果視圖沒有被填滿,presentingVc的視圖可以透過
      UIModalPresentationOverCurrentContext,//視圖全部被透過
      UIModalPresentationPopover,
      UIModalPresentationNone ,
};

關(guān)于系統(tǒng)提供的轉(zhuǎn)場動(dòng)畫可點(diǎn)擊傳送至 http://www.itdecent.cn/p/2a1f3c424cfe

  • presentingViewController和presentedViewController,fromView和toView


    The from and to objects

    Presented和Presenting是一組相對(duì)的概念,它不受present或dismiss的影響,如果是從A視圖控制器present到B,那么A總是B的presentingViewController,B總是A的presentedViewController。
    這張圖示說明,我們還可以理解一下fromView和toView這個(gè)兩個(gè)概念:
    fromView表示當(dāng)前視圖toView表示要跳轉(zhuǎn)到的視圖。如果是從A視圖控制器present到B,則A是fromView,B是toView。從B視圖控制器dismiss到A時(shí),B變成了fromView,A是toView。

自定義轉(zhuǎn)場動(dòng)畫中幾個(gè)關(guān)鍵協(xié)議

  • 轉(zhuǎn)場協(xié)議 UIViewControllerTransitioningDelegate
    UIViewController的transitioningDelegate屬性為遵守UIViewControllerTransitioningDelegate協(xié)議的對(duì)象
 // Present過程中返回一個(gè)遵守 <UIViewControllerAnimatedTransitioning> 協(xié)議的對(duì)象,也就是實(shí)現(xiàn)動(dòng)畫過程的對(duì)象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
 // dismiss過程中返回一個(gè)遵守 <UIViewControllerAnimatedTransitioning> 協(xié)議的對(duì)象,也就是實(shí)現(xiàn)動(dòng)畫過程的對(duì)象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
//如果delegate實(shí)現(xiàn)了此方法,在轉(zhuǎn)場過程中會(huì)調(diào)用,可根據(jù)是否有手勢來判斷是否返回交互控制對(duì)象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//如果delegate實(shí)現(xiàn)了此方法,在轉(zhuǎn)場過程中會(huì)調(diào)用,可根據(jù)是否有手勢來判斷是否返回交互控制對(duì)象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
/* 返回一個(gè)UIPresentationController的子類對(duì)象,UIPresentationController,提供了四個(gè)函數(shù)來定義present和dismiss動(dòng)畫開始前后的操作:
       1、presentationTransitionWillBegin: present將要執(zhí)行時(shí)
       2、presentationTransitionDidEnd:    present執(zhí)行結(jié)束后
       3、dismissalTransitionWillBegin:    dismiss將要執(zhí)行時(shí)
       4、dismissalTransitionDidEnd:         dismiss執(zhí)行結(jié)束后
*/
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

  • 動(dòng)畫協(xié)議 UIViewControllerAnimatedTransitioning
    轉(zhuǎn)場動(dòng)畫的執(zhí)行由此協(xié)議控制
//返回動(dòng)畫執(zhí)行的時(shí)長
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
// 轉(zhuǎn)場動(dòng)畫就是在這個(gè)方法里面添加
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  • 轉(zhuǎn)場環(huán)境協(xié)議 UIViewControllerContextTransitioning
    動(dòng)畫協(xié)議UIViewControllerAnimatedTransitioning 的兩個(gè)方法都有個(gè)遵守UIViewControllerContextTransitioning協(xié)議的參數(shù),表示的是當(dāng)前轉(zhuǎn)場的上下文,可以獲取到 fromViewController、或者是toViewController以及fromView、toView、contentView等。

簡單示例

Demo地址 https://github.com/YanLYM/YMTransitionDemo

  • 漸顯動(dòng)畫
//FromViewController中
- (void)event_present {
    YMFadeToViewController *vc = [YMFadeToViewController new];
    vc.transitioningDelegate = self;
    vc.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:vc animated:YES completion:nil];
}
//實(shí)現(xiàn)代理方法
#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    return [YMFadeInAnimate new];
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    return [YMFadeInAnimate new];
}
//此轉(zhuǎn)場不涉及手勢交互,所以不需要實(shí)現(xiàn)其它協(xié)議方法
//YMFadeInAnimate遵守了UIViewControllerAnimatedTransitioning協(xié)議
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 0.5;
}
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    //獲取當(dāng)前上下文的容器
    UIView *contentView = [transitionContext containerView];
    //fromViewController
    UIViewController *fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    //toViewController
    UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
     
    fromView.frame = [transitionContext initialFrameForViewController:fromVc];
    toView.frame = [transitionContext finalFrameForViewController:toVc];
    [contentView addSubview:toView];
    NSTimeInterval time = [self transitionDuration:transitionContext];
    fromView.alpha = 1;
    toView.alpha = 0;
    [UIView animateWithDuration:time animations:^{
        fromView.alpha = 0;
        toView.alpha = 1;
    } completion:^(BOOL finished) {
        //transitionWasCancelled 這個(gè)方法判斷轉(zhuǎn)場是否已經(jīng)取消了,下面的completeTransition設(shè)置轉(zhuǎn)場完成
        //動(dòng)畫結(jié)束后一定要調(diào)用completeTransition方法 來完成或取消轉(zhuǎn)場
        BOOL cancelTransition = [transitionContext transitionWasCancelled];
        [transitionContext completeTransition:!cancelTransition];
    }];
}

漸顯.gif
  • 交互控制器協(xié)議 UIViewControllerInteractiveTransitioning
    官方提供了一個(gè)已實(shí)現(xiàn)UIViewControllerInteractiveTransitioning協(xié)議的類UIPercentDrivenInteractiveTransition
    為我們預(yù)先實(shí)現(xiàn)和提供了一系列便利的方法,可以用一個(gè)百分比來控制交互式切換的過程,利用手勢來完成這個(gè)轉(zhuǎn)場。
//暫停交互 
- (void)pauseInteractiveTransition NS_AVAILABLE_IOS(10_0);
//更新方法,一般交互時(shí)候的進(jìn)度更新就在這個(gè)方法里面
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//第三個(gè)是取消交互
- (void)cancelInteractiveTransition;
//第四個(gè)的話就是設(shè)置交互完成
- (void)finishInteractiveTransition;
  • 下一個(gè)栗子:手勢切換Vc


    手勢切換Vc.gif
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator {
    //有手勢 返回處理交互協(xié)議對(duì)象
    if (self.gestureRecognizer) {
        //YMSwipeInteractiveTransition 繼承自UIPercentDrivenInteractiveTransition
        return [[YMSwipeInteractiveTransition alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge];
    }
    return nil;
}

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
    if (self.gestureRecognizer) {
        return [[YMSwipeInteractiveTransition alloc] initWithGestureRecognizer:self.gestureRecognizer edgeForDragging:self.targetEdge];
    }
    return nil;
}
  //YMSwipeInteractiveTransition  中
-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
    [super startInteractiveTransition:transitionContext];
    //保存我們的交互上下文,方便做進(jìn)度更新等操作
    self.transitionContext = transitionContext;
}
//初始化時(shí)添加手勢action
-(void)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer{
    switch (gestureRecognizer.state){
        case UIGestureRecognizerStateBegan:
            break;
        case UIGestureRecognizerStateChanged:
            // 調(diào)用updateInteractiveTransition來更新動(dòng)畫進(jìn)度
            // 里面嵌套定義 percentForGesture 方法計(jì)算動(dòng)畫進(jìn)度
            [self updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
            break;
        case UIGestureRecognizerStateEnded:
            //判斷手勢位置,要大于一般,就完成這個(gè)轉(zhuǎn)場,要小于一半就取消
            if ([self percentForGesture:gestureRecognizer] >= 0.5f)
                // 完成交互轉(zhuǎn)場
                [self finishInteractiveTransition];
            else
                // 取消交互轉(zhuǎn)場
                [self cancelInteractiveTransition];
            break;
        default:
            [self cancelInteractiveTransition];
            break;
    }
} 
// 計(jì)算動(dòng)畫進(jìn)度
-(CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture{
    UIView * transitionContainerView = self.transitionContext.containerView;
    // 手勢滑動(dòng) 在transitionContainerView中 的位置
    // 這個(gè)位置判斷的方法可以具體根據(jù)你的需求確定
    CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];
    CGFloat width  = CGRectGetWidth(transitionContainerView.bounds);
    CGFloat height = CGRectGetHeight(transitionContainerView.bounds);
    if (self.edge == UIRectEdgeRight)
        return (width - locationInSourceView.x) / width;
    else if (self.edge == UIRectEdgeLeft)
        return locationInSourceView.x / width;
    else if (self.edge == UIRectEdgeBottom)
        return (height - locationInSourceView.y) / height;
    else if (self.edge == UIRectEdgeTop)
        return locationInSourceView.y / height;
    else
        return 0.f;
}
這里值得注意的是,UIViewControllerTransitioningDelegate代理中返回 動(dòng)畫協(xié)議對(duì)象 的方法,有些文章說當(dāng)執(zhí)行交互式轉(zhuǎn)場式不會(huì)調(diào)用,經(jīng)測試,在調(diào)用返回交互控制協(xié)議對(duì)象之前會(huì)先調(diào)用 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;因?yàn)閯?dòng)畫還是由其控制,只是進(jìn)度百分比由交互控制協(xié)議對(duì)象控制。
  • 轉(zhuǎn)場協(xié)調(diào)器協(xié)議 UIViewControllerTransitionCoordinator
- (BOOL)animateAlongsideTransition:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
                        completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;

- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view
                               animation:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))animation
                              completion:(void (^ __nullable)(id <UIViewControllerTransitionCoordinatorContext>context))completion;

- (void)notifyWhenInteractionEndsUsingBlock: (void (^)(id <UIViewControllerTransitionCoordinatorContext>context))handler NS_DEPRECATED_IOS(7_0, 10_0,"Use notifyWhenInteractionChangesUsingBlock");

- (void)notifyWhenInteractionChangesUsingBlock: (void (^)(id <UIViewControllerTransitionCoordinatorContext>context))handler NS_AVAILABLE_IOS(10_0);

可在轉(zhuǎn)場動(dòng)畫發(fā)生的同時(shí)并行執(zhí)行其他的動(dòng)畫,其作用與其說協(xié)調(diào)不如說輔助,主要在 Modal 轉(zhuǎn)場和交互轉(zhuǎn)場取消時(shí)使用,其他時(shí)候很少用到;遵守<UIViewControllerTransitionCoordinator>協(xié)議;由 UIKit 在轉(zhuǎn)場時(shí)生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一個(gè)遵守該協(xié)議的對(duì)象,且該方法只在該控制器處于轉(zhuǎn)場過程中才返回一個(gè)此類對(duì)象,不參與轉(zhuǎn)場時(shí)返回 nil。轉(zhuǎn)場協(xié)調(diào)器的使用可結(jié)合UIPresentationController使用。

  • UIPresentationController
    UIPresentationController,它提供了四個(gè)函數(shù)來定義present和dismiss動(dòng)畫開始前后的操作:

     1、presentationTransitionWillBegin: present將要執(zhí)行時(shí)
    
     2、presentationTransitionDidEnd:    present執(zhí)行結(jié)束后
    
     3、dismissalTransitionWillBegin:    dismiss將要執(zhí)行時(shí)
    
     4、dismissalTransitionDidEnd:         dismiss執(zhí)行結(jié)束后
    

Example:


卡片式.gif

如果要實(shí)現(xiàn)圖中點(diǎn)擊空白區(qū)域?qū)崿F(xiàn)dismiss的效果就可以通過UIPresentationController來實(shí)現(xiàn)了,可在presentationTransitionWillBegin: 方法中替換presentedView

/**
 present將要執(zhí)行
 */
- (void)presentationTransitionWillBegin {
    self.replacePresentView = [[UIView alloc] initWithFrame:[self frameOfPresentedViewInContainerView]];
    self.replacePresentView.layer.cornerRadius = 16;
    self.replacePresentView.layer.shadowOpacity = 0.44f;
    self.replacePresentView.layer.shadowRadius = 13.f;
    self.replacePresentView.layer.shadowOffset = CGSizeMake(0, -6.f);
    UIView *presentedView = [super presentedView];
    presentedView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.replacePresentView addSubview:presentedView];
    
    UIView *dismissView = [[UIView alloc] initWithFrame:self.containerView.bounds];
    [self.containerView addSubview:dismissView];
    self.dismissView = dismissView;
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gesture_dismiss)];
    [self.dismissView addGestureRecognizer:tap];
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
    
    self.dismissView.alpha = 0.f;
    self.dismissView.backgroundColor = [UIColor blackColor];
    //執(zhí)行轉(zhuǎn)場動(dòng)畫的同時(shí)執(zhí)行其他動(dòng)畫
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dismissView.alpha = 0.5f;
    } completion:NULL];
    
}
- (UIView *)presentedView {
    return self.replacePresentView;
}
  • 再看一個(gè)擴(kuò)散圓的例子


    擴(kuò)散圓.gif

    重要的部分就是使用UIBezierPath 和 CABasicAnimation來做動(dòng)畫,懂得了原理,加上會(huì)做動(dòng)畫,轉(zhuǎn)場動(dòng)畫實(shí)現(xiàn)就是如此。

- (void)presentViewControllerWithTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    UINavigationController *navVc = (UINavigationController *)[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    YMCircleFromViewController * fromVc = navVc.viewControllers.lastObject;
    UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVc.view];
    
    UIBezierPath *startCircle = [UIBezierPath bezierPathWithOvalInRect:fromVc.presentBtn.frame];
    // sqrtf 求平方根函數(shù)  pow求次方函數(shù),這里的意思是求X的2次方,要是pow(m,9)就是求m的9次方
    CGFloat radius = sqrtf(pow(containerView.frame.size.width, 2) + pow(containerView.frame.size.height, 2));
    UIBezierPath *endCircle = [UIBezierPath bezierPathWithArcCenter:containerView.center radius:radius startAngle:0 endAngle:M_PI * 2 clockwise:YES];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = endCircle.CGPath;
    toVc.view.layer.mask = maskLayer;
    //創(chuàng)建路徑動(dòng)畫
    CABasicAnimation * maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
    maskLayerAnimation.delegate = self;
    //動(dòng)畫是加到layer上的,所以必須為CGPath,再將CGPath橋接為OC對(duì)象
    maskLayerAnimation.fromValue = (__bridge id)(startCircle.CGPath);
    maskLayerAnimation.toValue   = (__bridge id)((endCircle.CGPath));
    maskLayerAnimation.duration  = [self transitionDuration:transitionContext];
    
    //速度控制函數(shù)
    maskLayerAnimation.timingFunction = [CAMediaTimingFunction  functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [maskLayerAnimation setValue:transitionContext forKey:@"transitionContext"];
    // 添加動(dòng)畫
    [maskLayer addAnimation:maskLayerAnimation forKey:@"path"];
    
}

再看一個(gè)簡單的Pop/Push的開關(guān)門動(dòng)畫

//開門動(dòng)畫
- (void) {
        YMOpenViewController *vc = [YMOpenViewController new];
        self.navigationController.delegate = vc;
        [self.navigationController pushViewController:vc animated:YES];
}

//YMOpenViewController 遵守UINavigationControllerDelegate協(xié)議,并實(shí)現(xiàn)此代理方法
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC {
    //通過operation判斷是出棧還是入棧
    if (operation == UINavigationControllerOperationPush) {
        self.animation.isPop = NO;
    } else if (operation == UINavigationControllerOperationPop) {
        self.animation.isPop = YES;
    }
    return self.animation;
}
Pop開門.gif

Demo地址再發(fā)一遍 https://github.com/YanLYM/YMTransitionDemo
至此,presentation-dismissal的自定義轉(zhuǎn)場動(dòng)畫基本做法已講述完畢,下篇一起學(xué)習(xí)UINavigationController 中 push 和 pop 以及 UITabBarController 中切換 Tab的轉(zhuǎn)場。

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

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

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