在iOS开发中,页面跳转一般有两种方式:
- navigation:push & pop,动画是从右到左
- modal(模态):presnet & dismiss,动画是从下到上
在SDK开发中,由于需要减小侵入性,通常会使用modal方式弹出SDK的页面,那么默认唤起的动画就是从下到上。现在有一个需求,需要从右到左唤起SDK的页面,所以我们需要用到转场动画,将present的动画改成从右到左,也就是以Push方式实现Present跳转。
开始前
实现模态的转场动画的步骤,大概分以下几步:
-
自定义一个遵循的
<UIViewControllerAnimatedTransitioning>
协议的动画转场管理对象,并实现两个必须实现的方法://返回动画时间 - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext; //所有的转场动画事务都在这个方法里面完成 - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
-
成为相应的代理,实现相应的代理方法,返回上面的自定义协议对象
//返回一个管理prenent动画转场的对象 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source; //返回一个管理dismiss动画转场的对象 - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
-
vc调用presentViewController、dismissViewControllerAnimated时开启 animated
下面按照这个步骤来实现功能
WBHCPushAnimatedTransition
创建一个类 WBHCPushAnimatedTransition
来封装转场动画
typedef NS_ENUM(NSUInteger, WBHCPushAnimatedTransitionType) {
WBHCPushAnimatedTransitionTypePresent, // 管理present动画
WBHCPushAnimatedTransitionTypeDismiss // 管理dismiss动画
};
/// 动画转场,以push动画实现present dismiss
@interface WBHCPushAnimatedTransition : NSObject<UIViewControllerAnimatedTransitioning>
- (instancetype)initWithTransitionType:(WBHCPushAnimatedTransitionType)type;
@end
实现系统的 UIViewControllerAnimatedTransitioning
协议
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
switch (_type) {
case WBHCPushAnimatedTransitionTypePresent:
[self presentAnimation:transitionContext];
break;
case WBHCPushAnimatedTransitionTypeDismiss:
[self dismissAnimation:transitionContext];
break;
}
}
实现present动画,实际就是使用UIView动画来改变 fromVC 和 toVC 的 view 的位置
- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext {
/**
A present to B. fromVC = A, toVC = B
*/
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 的位置还原
- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext {
/**
B dismiss to A. fromVC = B, toVC = A
*/
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
// 设置代理
self.transitioningDelegate = self;
#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [[WBHCPushAnimatedTransition alloc] initWithTransitionType:WBHCPushAnimatedTransitionTypePresent];
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [[WBHCPushAnimatedTransition alloc] initWithTransitionType:WBHCPushAnimatedTransitionTypeDismiss];
}
这样的话,整个功能就基本实现了
但是,体验上还缺少一点:没有侧滑返回。所以,接下来,我们实现侧滑返回的交互转场
WBHCPopInteractiveTransition
创建一个继承自 UIPercentDrivenInteractiveTransition
的类 WBHCPopInteractiveTransition
来封装交互转场
/// 交互转场,以pop侧滑返回方式实现dismiss
@interface WBHCPopInteractiveTransition : UIPercentDrivenInteractiveTransition
/// 记录是否开始手势,判断dismiss操作是手势触发还是返回键触发
@property (nonatomic, assign) BOOL isTransitioning;
- (instancetype)initWithViewController:(UIViewController *)viewController;
@end
为传入的vc添加拖动手势
- (instancetype)initWithViewController:(UIViewController *)viewController {
self = [super init];
if (self) {
_viewControllerDismissing = viewController;
[self addPanGestureForViewController:viewController];
}
return self;
}
- (void)addPanGestureForViewController:(UIViewController *)viewController {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
pan.delegate = self;
[viewController.view addGestureRecognizer:pan];
}
拖动手势是加在全屏上的,我们需要将它限制到屏幕左侧,这样才是侧滑返回,否则是全屏返回
#pragma mark - UIGestureRecognizerDelegate
/// 侧滑返回
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
return location.x < 50;
}
核心是处理手势交互转场的过程,我们需要更新转场百分比 updateInteractiveTransition
,标记 完成转场 finishInteractiveTransition
和取消转场 cancelInteractiveTransition
/// 手势过渡的过程
- (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:
// 手势过程中,通过updateInteractiveTransition设置转场的百分比
[self updateInteractiveTransition:percent];
break;
case UIGestureRecognizerStateEnded:
// 手势完成后结束标记并且判断移动距离是否过半,过则finishInteractiveTransition完成转场操作,否者取消转场操作
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
// 创建交互转场对象
self.interactiveDismiss = [[WBHCPopInteractiveTransition alloc] initWithViewController:self.visibleViewController];
#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return _interactiveDismiss.isTransitioning ? _interactiveDismiss : nil;
}
这样的话,整个侧滑返回的功能就实现了
至此,我们基本上就实现了以Push方式实现Present跳转,同时可以像原生导航一样进行侧滑返回
网友评论