在iOS开发中,页面跳转一般有两种方式:
- navigation:push & pop,动画是从右到左
- modal(模态):presnet & dismiss,动画是从下到上
在SDK开发中,由于需要减小侵入性,通常会使用modal方式弹出SDK的页面,那么默认唤起的动画就是从下到上。现在有一个需求,需要从右到左唤起SDK的页面,所以我们需要用到转场动画,将present的动画改成从右到左,也就是以Push方式实现Present跳转。
开始前
实现模态的转场动画的步骤,大概分以下几步:
自定义一个遵循的<UIViewControllerAnimatedTransitioning>
协议的动画转场管理对象,并实现两个必须实现的方法:
1 2 3 4
| - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
|
成为相应的代理,实现相应的代理方法,返回上面的自定义协议对象
1 2 3 4
| //返回一个管理prenent动画转场的对象 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; //返回一个管理dismiss动画转场的对象 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
|
vc调用presentViewController、dismissViewControllerAnimated时开启 animated
下面按照这个步骤来实现功能
ZDPushAnimatedTransition
创建一个类 ZDPushAnimatedTransition
来封装转场动画
1 2 3 4 5 6 7 8 9 10 11
| typedef NS_ENUM(NSUInteger, ZDPushAnimatedTransitionType) { ZDPushAnimatedTransitionTypePresent, // present动画 ZDPushAnimatedTransitionTypeDismiss // dismiss动画 };
/// push动画 @interface ZDPushAnimatedTransition : NSObject<UIViewControllerAnimatedTransitioning>
- (instancetype)initWithTransitionType:(ZDPushAnimatedTransitionType)type;
@end
|
实现系统的 UIViewControllerAnimatedTransitioning
协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext { return 0.25; }
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext { switch (_type) { case ZDPushAnimatedTransitionTypePresent: [self presentAnimation:transitionContext]; break; case ZDPushAnimatedTransitionTypeDismiss: [self dismissAnimation:transitionContext]; break; } }
|
实现present动画,实际就是使用UIView动画来改变 fromVC 和 toVC 的 view 的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /** A present to B. fromVC = A, toVC = B */ - (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *containerView = [transitionContext containerView]; [containerView addSubview:fromVC.view]; [containerView addSubview:toVC.view]; toVC.view.frame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height); [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ fromVC.view.transform = CGAffineTransformMakeTranslation(-containerView.bounds.size.width / 3, 0); toVC.view.transform = CGAffineTransformMakeTranslation(-containerView.bounds.size.width, 0); } completion:^(BOOL finished) { [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }
|
实现dismiss动画,和上面的present动画相反,将fromVC 和 toVC 的 view 的位置还原
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| /** B dismiss to A. fromVC = B, toVC = A */ - (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containerView = [transitionContext containerView]; [containerView addSubview:toVC.view]; [containerView addSubview:fromVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ fromVC.view.transform = CGAffineTransformIdentity; toVC.view.transform = CGAffineTransformIdentity; } completion:^(BOOL finished) { [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; }
|
这样,整个转场动画的封装就完成了,下面来实现代理
实现代理
在要present出来的vc中实现系统代理 UIViewControllerTransitioningDelegate
1 2 3 4 5 6 7 8 9 10 11 12
| // 设置代理 self.transitioningDelegate = self;
#pragma mark - UIViewControllerTransitioningDelegate - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { return [[ZDPushAnimatedTransition alloc] initWithTransitionType:ZDPushAnimatedTransitionTypePresent]; }
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { return [[ZDPushAnimatedTransition alloc] initWithTransitionType:ZDPushAnimatedTransitionTypeDismiss]; }
|
这样的话,整个功能就基本实现了
但是,体验上还缺少一点:没有侧滑返回。所以,接下来,我们实现侧滑返回的交互转场
ZDPopInteractiveTransition
创建一个继承自 UIPercentDrivenInteractiveTransition
的类 ZDPopInteractiveTransition
来封装交互转场
1 2 3 4 5 6 7
| @interface ZDPopInteractiveTransition : UIPercentDrivenInteractiveTransition
@property (nonatomic, assign) BOOL isTransitioning;
- (instancetype)initWithViewController:(UIViewController *)vc;
@end
|
为传入的vc添加拖动手势
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| - (instancetype)initWithViewController:(UIViewController *)vc { self = [super init]; if (self) { _viewControllerDismissing = vc; [self addPanGestureForViewController:viewController]; } return self; }
- (void)addPanGestureForViewController:(UIViewController *)vc { UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)]; pan.delegate = self; [vc.view addGestureRecognizer:pan]; }
|
拖动手势是加在全屏上的,我们需要将它限制到屏幕左侧,这样才是侧滑返回,否则是全屏返回
1 2 3 4 5 6
| #pragma mark - UIGestureRecognizerDelegate /// 侧滑返回 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view]; return location.x < 50; }
|
核心是处理手势交互转场的过程,我们需要更新转场百分比 updateInteractiveTransition
,标记 完成转场 finishInteractiveTransition
和取消转场 cancelInteractiveTransition
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| - (void)handleGesture:(UIPanGestureRecognizer *)panGesture { CGFloat transitionX = [panGesture translationInView:panGesture.view].x; CGFloat percent = transitionX / panGesture.view.frame.size.width; CGFloat velocityX = [panGesture velocityInView:panGesture.view].x;
switch (panGesture.state) { case UIGestureRecognizerStateBegan: self.isTransitioning = YES; [self startGesture]; break; case UIGestureRecognizerStateChanged: [self updateInteractiveTransition:percent]; break; case UIGestureRecognizerStateEnded: self.isTransitioning = NO; if (percent > 0.3 || velocityX > 300) { [self finishInteractiveTransition]; } else { [self cancelInteractiveTransition]; } break; default: break; } }
- (void)startGesture { [_viewControllerDismissing dismissViewControllerAnimated:YES completion:nil]; }
|
这样,整个交互转场的封装就完成了,下面来实现代理
实现交互代理
在要dismiss消失的vc中实现系统代理 UIViewControllerTransitioningDelegate
1 2 3 4 5 6 7 8
| // 创建交互转场对象 self.interactiveDismissing = [[ZDPopInteractiveTransition alloc] initWithViewController:self.visibleViewController];
#pragma mark - UIViewControllerTransitioningDelegate - (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator { return _interactiveDismiss.isTransitioning ? _interactiveDismissing : nil; }
|
这样的话,整个侧滑返回的功能就实现了
至此,我们基本上就实现了以Push方式实现Present跳转,同时可以像原生导航一样进行侧滑返回