前言
目前而言自定义继承与UIViewController的容器子类,并没有现成的可使用的API来进行动画(将一个视图控制器转场到另外一个)。本文中将介绍如何自定义视图控制器的容器类的转场动画(如从一个控制器视图推出另一个控制器视图)。
视图控制器的推出目前看来有三种方式:
在这里当然还是有方法可以进行容器视图的转场,transitionFromViewController:toViewController:duration:options:animations:completion:,这方法看似好用,但若是对每个容器转场动画实现相同的逻辑,那就得实现对应的若干个相同方法。这样便产生了严重的代码冗余。若我们通过指定一个转场的动画,让所有的转场都执行此动画就提高了其代码的复用性,也降低了其耦合性。因此在这里主要讲解如何来自定义转场动画。demo地址:转场动画-demo,各位可以通过demo与本文描述结合理解,这样更能加深印象!
相关API
在开始代码之前,先看看我们所需要用到的相关类别和接口
转场动画交互方式分两种,第一种是属于非交互式:必须要实现动画控制类(相当于我们平时的直接点击一个按钮然后present出另一个视图控制器),第二种交互式:必须要实现动画控制类和交互控制类(例如可以通过手势的滑动距离来控制转场动画的一个进度,一般应用中都可以通过手势的滑动来推出一个视图控制器)。当然不论哪种转场动画,都必须要设定一个转场代理。
代码部分
Present转场动画
非交互式转场动画
1.新建工程,给FirstVc视图控制器的视图配置背景色和一个点击按钮(按钮用于present下一个控制器),在推出第二控制器视图的时候要注意配置其转场动画代理
2.创建一个视图控制器(SecondVc)用于被推出,给其配置另外一种背景色和一个返回按钮(用于dismiss)
3.配置转场代理,以下为FirstVc的按钮点击事件中配置代码示例
SecondViewController *secondVc = [[SecondViewController alloc] init]; //UIModalPresentationFullScreen:由系统管理推出完成后的视图 //UIModalPresentationCustom:自己管理推出完成后的视图 secondVc.modalPresentationStyle = UIModalPresentationCustom; //配置转场代理 secondVc.transitioningDelegate = self; [self presentViewController:secondVc animated:YES completion:nil]; //此处需要注意不要将transitioningDelegate写为modalTransitionStyle。modalTransitionStyle是系统提供转场动画效果,效果较少。
4.配置转场代理协议方法
prsent推送时调用的方法,返回动画控制类
1)animationControllerForPresentedController: presentingController: sourceController:dismiss推送时调用方法,返回动画控制类
2)animationControllerForDismissedController:
交互式present推送调用方法,返回交互控制类
3)interactionControllerForPresentation:
交互式dismiss推送调用方法,返回交互控制类
4)interactionControllerForDismissal:
iOS8后引入的转场动画控制器,可以在转场动画的同时利用此控制器配置其他动画,只有在配置presentationStyle为Custom类型才有效果
5)presentationControllerForPresentedViewController: presentingViewController:sourceViewController:
在非交互式的情况下3),4)方法默认返回nil对象,不进行交互。若此时强行返回一个交互控制类会导致视图不能正常推出,造成程序假死情况!
5.新建一个类(BQTransitionAnimation)继承自NSObject对象,遵从UIViewControllerAnimatedTransitioning协议,协议方法有:
配置转场动画时间
1)transitionDuration:
配置转场动画具体逻辑方法
2)animateTransition:
方法中自带参数transitionContext,即前文中所提到的转场上下文,我们可以通过获取上下文中的元数据。如下所示:
UIViewController *toVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController *fromVc = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
从上下文中取出了fromVc和toVc,接着再给这些视图控制器的视图做相应的动画即是转场动画。其中需要注意的是fromVc和toVc的一个相对概念。fromVc代表的是从哪个视图控制器来,toVc代表的是要到哪个视图控制器去。如上述中prsent时fromVc即为ViewController,toVc为NextViewController。但若是dismiss时,fromVc就变为了NextViewController,toVc为ViewController。以下是一个完整的BQTransitionAnimation.m代码,其中成员变量_presenting 用于判断此时控制器是在进行present还是dismiss,其中的动画逻辑可以根据自己的实际需要来进行编写,笔者的动画逻辑可在demo中BQTransitionAnimation类中查看
6.在Viewcontroller中配置转场代理协议方法,实现其中的prsent和dismiss方法,代码如下:
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Alpha_Change]; } - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Rotate]; }
根据以上代码思路,一个简单的自定义非交互式转场动画便完成了。
交互式转场动画
交互式转场动画需要在非交互式的基础上再增加一个交互控制类,并以手势的方式来进行交互。
1.在FirstVc视图中添加一个边缘滑动手势(UIScreenEdgePanGestureRecognizer),并指定滑动方向,并添加手势响应方法,具体代码实例如下:
- (UIScreenEdgePanGestureRecognizer *)gesture { if (_gesture == nil) { _gesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(gestureSliderEvent:)]; _gesture.edges = UIRectEdgeRight; } return _gesture; } - (void)gestureSliderEvent:(UIScreenEdgePanGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateBegan) { //模态推送按钮 [self showNextVcBtnClickedEvent:nil]; } } - (void)showNextVcBtnClickedEvent:(UIButton *)sender { BQPresentInteractionSecondVc *nextVc = [[BQPresentInteractionSecondVc alloc] init]; nextVc.modalPresentationStyle = UIModalPresentationCustom; //将转场代理设置为单独的一个对象,这样方便控制其是否需要交互 nextVc.transitioningDelegate = self.transitionDelegate; nextVc.preVc = self; //通过判断是否通过按钮点击推出 if (sender != nil) { //按钮点击推出,则转场代理手势设置为nil self.transitionDelegate.gesture = nil; }else { //非按钮点击推出(手势交互),则将手势传入转场代理,转场代理根据此手势来进行交互比例判断 self.transitionDelegate.gesture = self.gesture; } [self presentViewController:nextVc animated:YES completion:nil]; }
2.接下来在FirstVc中配置转场代理,并新建一个转场代理(BQTransitioningDelegate)类,关键代码示例如下:
- (BQTransitioningDelegate *)transitionDelegate { if (_transitionDelegate == nil) { _transitionDelegate = [[BQTransitioningDelegate alloc] init]; } return _transitionDelegate; }
BQTransitioningDelegate.m文件中关键代码
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator { if (self.gesture != nil) { return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.gesture]; } return nil; } - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{ if (self.gesture != nil) { return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.gesture]; } return nil; }
3.创建一个类(BQpercentDrivenInteractive)继承自UIPercentDrivenInteractiveTransition。其内部的主要方法是
1)startInteractiveTransition:(开启交互)
2)updateInteractiveTransition:(更新交互动画)
3)finishInteractiveTransition(完成交互动画)
4)cancelInteractiveTransition(取消交互动画)
在类中根据传入的手势添加一个手势响应方法,通过其响应可以计算出手势完成比例,再根据此比例数值更新其交互动画,具体事例代码如下:
- (instancetype)initWithPanGesture:(UIScreenEdgePanGestureRecognizer *)gesture { self = [super init]; if (self) { _gesture = gesture; //添加手势触发事件 [_gesture addTarget:self action:@selector(updateViewContorllerTransition:)]; } return self; } - (void)dealloc { [_gesture removeTarget:self action:@selector(updateViewContorllerTransition:)]; } //根据手势状态来更新交互动画操作 - (void)updateViewContorllerTransition:(UIScreenEdgePanGestureRecognizer *)sender { switch (sender.state) { case UIGestureRecognizerStateBegan: break; case UIGestureRecognizerStateChanged: [self updateInteractiveTransition:[self completPercentFromGesture]]; break; case UIGestureRecognizerStateEnded: { if ([self completPercentFromGesture] >= 0.5) { [self finishInteractiveTransition]; }else { [self cancelInteractiveTransition]; } } break; default: [self cancelInteractiveTransition]; break; } } //开始交互动画时调用 - (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext { _transitionContext = transitionContext; [super startInteractiveTransition:transitionContext]; } //根据手势计算当前手势的完成度 - (CGFloat)completPercentFromGesture { UIView *sourceView = _transitionContext.containerView; CGPoint point = [_gesture locationInView:sourceView]; CGFloat percent = 0; if (_gesture.edges == UIRectEdgeRight) { percent = (Screen_Width - point.x) / Screen_Width; }else { percent = point.x / Screen_Width; } NSLog(@"%lf",percent); return percent; }
4.在SecondVc视图控制器中同样添加一个滑动手势,并同First中做出同样配置。当由手势交互引发视图控制器dismiss时,先将转场代理的手势配置为SecondVc中的滑动手势。这样就可以进行反向的交互转场动画了,关键示例代码如下:
- (void)backBtnClickedEvent:(UIButton *)sender { if (sender != nil) { ((BQTransitioningDelegate *)self.preVc.transitionDelegate).gesture = nil; }else{ __weak typeof(self) weakSelf = self; ((BQTransitioningDelegate *)self.preVc.transitionDelegate).gesture = weakSelf.gesture; } [self dismissViewControllerAnimated:YES completion:nil]; } - (void)gestureSliderChange:(UIScreenEdgePanGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateBegan) { [self backBtnClickedEvent:nil]; } }
至此Present交互式的转场动画就告一段落。
Navagation的转场动画
Navagation配置同Present方法基本相同。首先只需要配置Navigation代理,接下来实现其代理方法即可。其中思路Present相同,代码示例如下:
self.navigationController.delegate = self; #pragma mark - UINavigationControllerDelegate Method - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { return [[BQTransitionAnimation alloc] initWithAnimationType:AnimationType_Alpha_Change]; } - (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController { if (_isInteraction == YES) { return [[BQpercentDrivenInteractive alloc] initWithPanGesture:self.panGesture]; } return nil; }
TabbarController转场动画
关于tabbarController的转场动画配置同上面两种一样,此处并没有实现交互式转场动画。因为tabbarController在项目中一般是作为容器使用,其内部包含的视图一般包含有其他手势,所以对tabbarController做交互动画的极容易产生手势冲突。所以只需要配置其非交互是转场动画设置即可。自定义的转场动画就讲到这里,关于更多的细节部分(包括转场动画控制器、协调器的基本使用),请参照demo学习研究。如果上述有任何错误欢迎指正!