前言
这篇博文主要是记录一些实现思路,顺便将平时实现转场动画时遇到的一些问题和细节整理下来,也方便巩固下知识。在这里作为示例的转场动画也是目前项目中有使用到的,如果后续还有新的转场方式也会放在这里。
使用
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { MERSecondViewController *controller = [[MERSecondViewController alloc] init]; CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height; [tableView deselectRowAtIndexPath:indexPath animated:YES]; // Action Sheet if (indexPath.row == MERPresentationAnimationTypeActionSheet) { controller.mer_viewSize = CGSizeMake(screenWidth / 6.0 * 5, screenHeight / 5.0 * 2); [self presentActionSheetViewController:controller animated:YES completion:nil]; // 侧边滑入 } else if (indexPath.row == MERPresentationAnimationTypeSlider) { controller.mer_viewSize = CGSizeMake(screenWidth / 3.0 * 2, screenHeight); [self presentSliderViewController:controller direction:MERSlidePresentationDirectionLeft animated:YES completion:nil]; // 淡入淡出 } else if (indexPath.row == MERPresentationAnimationTypeFade) { [self presentFadePatternViewController:controller animated:YES completion:nil]; // 点扩散 } else if (indexPath.row == MERPresentationAnimationTypeDiffuse) { [self presentDiffuseViewController:controller startPoint:_clickView.lastClickPoint animated:YES completion:nil]; }}复制代码
转场的实现步骤
关于 iOS 转场动画的详细解读,可以参考这两篇文章:王巍写的 和唐巧写的 。篇幅较长,写的非常详细。
1.实现转场代理
UIViewController
的转场代理为transitioningDelegate
属性,遵循<UIViewControllerTransitioningDelegate>
协议;UINavigationController
的转场代理为delegate
属性,遵循<UINavigationControllerDelegate>
协议;UITabBarController
的转场代理为delegate
属性,遵循<UITabBarControllerDelegate>
协议;
因此我们只需要新建一个代理类,遵循并实现相应的代理,然后赋值给对应的代理属性即可。
其中 Present 转场需要给被 present 出来的
Tabbar 转场同理。UIViewController
设置代理,实现 present 和 dismiss 的自定义动画。 Push 转场则需要给导航控制器UINavigationController
设置代理,需要注意设置代理后如果只为限定的 VC 采用自定义动画,需要在代理的实现方法中做区分才行。
本篇主要以 Present 转场举例,下面是 <UIViewControllerTransitioningDelegate>
需要实现的方法
- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source { // 关于 UIPresentationController 类的功能描述可以参照上面贴出的唐巧的博客,主要作用可以总结为自定义 presentedView 的尺寸以及添加动画。例如需要 Present 出来的 VC 并非满屏大小时(参照系统的 ActionSheet 控件),只需要在这里面做简单的设置即可;}- (id)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { // Present 动画执行时需要提供的动画控制器}- (id )animationControllerForDismissedController:(UIViewController *)dismissed { // Dismiss 动画执行时需要提供的动画控制器 // ps: 方便的做法是共用一个动画控制器,通过 Bool 值区别是 Present 或者 Dismiss,来区别动画实现细节}- (id )interactionControllerForDismissal:(id )animator { // 为转场增加手势控制}复制代码
2.转场的动画控制器
无论是上面哪种控制器的转场代理,均需要提供一个实现了 <UIViewControllerAnimatedTransitioning>
协议的动画控制器,一般新建一个继承自NSObject
的类遵循并实现这个协议即可。
// 转场动画的时间-(NSTimeInterval)transitionDuration:(id)transitionContext;// 转场动画的具体实现-(void)animateTransition:(id )transitionContext;复制代码
核心点在于 -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
方法的实现。
3.转场动画实现的相关细节
上下文参数 transitionContext
遵循了 <UIViewControllerContextTransitioning>
协议,开发者们可以根据上下文拿到实现动画所需要的重要的信息:
// 动画发生的容器 ViewtransitionContext.containerView;// 转场前后两个 ViewController[transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];// 转场前后两个 View (即为 ViewController.view)[transitionContext viewForKey:UITransitionContextFromViewKey];[transitionContext viewForKey:UITransitionContextToViewKey];// 控制器在转场前后的 frame[transitionContext initialFrameForViewController:(UIViewController *)vc];[transitionContext finalFrameForViewController:(UIViewController *)vc];// 动画执行结束一定要调用这个方法-(void)completeTransition:(BOOL)didComplete;复制代码
总的来说就是系统提供给了开发者一个 containerView
,以及将要进行动画的前后两个视图控制器的 View
,由开发者来自行实现动画,并在动画结束时调用 -(void)completeTransition:(BOOL)didComplete
方法告知动画结束。因此对于开发者来说,问题简化为了容器 View 上的两个子 View 如何展现动画的简单问题。
这里有几点细节需要注意:
- Present / Push 动画需要手动将
UITransitionContextToViewKey
对应的View
addSubview 到containerView
中。 - 动画的实现可以采用 CALayer 动画或者 UIView 动画,但是实测在 iOS 11 下,CALayer 动画无法通过手势控制器实时控制动画进度,不清楚是不是 bug。
- 动画结束请一定要调用
-(void)completeTransition:(BOOL)didComplete
。
这里贴一个简单的栗子
- (NSTimeInterval)transitionDuration:(id)transitionContext { return 0.4; // 动画执行时间}- (void)animateTransition:(id )transitionContext { // isPresentation 属性为初始化时传入,区别是 Present 还是 Dismiss NSString *key = self.isPresentation ? UITransitionContextToViewControllerKey : UITransitionContextFromViewControllerKey; UIViewController *controller = [transitionContext viewControllerForKey:key]; if (self.isPresentation) { [transitionContext.containerView addSubview:controller.view]; } CGRect presentedFrame = [transitionContext finalFrameForViewController:controller]; CGRect dismissedFrame.origin.y = transitionContext.containerView.bounds.size.height; CGRect initialFrame = self.isPresentation ? dismissedFrame : presentedFrame; CGRect finalFrame = self.isPresentation ? presentedFrame : dismissedFrame; NSTimeInterval duration = [self transitionDuration:transitionContext]; controller.view.frame = initialFrame; [UIView animateWithDuration:duration delay:0.f usingSpringWithDamping:1.f initialSpringVelocity:5.f options:UIViewAnimationOptionCurveEaseInOut animations:^{ controller.view.frame = finalFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:finished]; }];}复制代码
4.手势驱动改变动画进度
系统提供了一个实现 UIViewControllerInteractiveTransitioning
协议的UIPercentDrivenInteractiveTransition
类,所以我们只要继承这个类,添加手势并在手势实现的方法中告知当前视图的百分比,通过此逻辑来驱动视图,在调用类中定义的一些方法就很容易实现视图的交互。
核心方法有三个:
// 更新动画百分比-(void)updateInteractiveTransition:(CGFloat)percentComplete;// 取消视图交互,返回动画执行前的状态-(void)cancelInteractiveTransition;// 继续完成动画,更新到完成后的状态-(void)finishInteractiveTransition;复制代码
实现方式通常如下:
@interface MERPresentationInteractive ()@property (nonatomic, weak) UIViewController *dismissedVC;@property (nonatomic, strong) UIScreenEdgePanGestureRecognizer *panGesture;@end@implementation MERPresentationInteractive- (instancetype)init { self = [super init]; if (self) { _isInteracting = NO; } return self;}- (void)setDismissGestureRecognizerToViewController:(UIViewController *)viewController { // 为被 Present 出来的 VC 添加滑动手势 _dismissedVC = viewController; UIViewController *vc = viewController; if ([viewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navi = (UINavigationController *)viewController; vc = navi.topViewController; } if (!_panGesture) { _panGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]; _panGesture.edges = UIRectEdgeLeft; } if (![[vc.view gestureRecognizers] containsObject:_panGesture]) { [vc.view addGestureRecognizer:_panGesture]; }}- (void)handlePan:(UIScreenEdgePanGestureRecognizer*)recognizer { if (recognizer.state == UIGestureRecognizerStateBegan) { _isInteracting = YES; [_dismissedVC dismissViewControllerAnimated:YES completion:nil]; // 开始执行动画 } else if (recognizer.state == UIGestureRecognizerStateChanged) { if (!_isInteracting) { return; } CGFloat progress = [recognizer translationInView:[UIApplication sharedApplication].keyWindow].x / ([UIApplication sharedApplication].keyWindow.bounds.size.width * 1.0); progress = MIN(1.0, MAX(0.0, progress)); [self updateInteractiveTransition:progress]; // 根据手势实时更新动画进度 } else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) { if (!_isInteracting) { return; } CGFloat progress = [recognizer translationInView:[UIApplication sharedApplication].keyWindow].x / (_dismissedVC.view.bounds.size.width * 1.0); progress = MIN(1.0, MAX(0.0, progress)); if (@available(iOS 11.0,*)) { // 此处由于在实现点扩散转场动画中,iOS 11下执行取消仍然会完成动画,因此对iOS 11区别处理了 self.completionSpeed = 1 - progress; [self finishInteractiveTransition]; } else { CGPoint velocity = [recognizer velocityInView:[UIApplication sharedApplication].keyWindow]; // 根据进度和速度方向来确定完成和取消的阈值,因人而异,可随意调整 if ((progress > 0.25 && velocity.x > 0) || progress > 0.5) { NSLog(@"Pop完成"); self.completionSpeed = 1; [self finishInteractiveTransition]; } else { NSLog(@"Pop取消"); [self updateInteractiveTransition:0.f]; [self cancelInteractiveTransition]; } } _isInteracting = NO; }}@end复制代码
5.在分类中使用
新建 UIViewController
的分类,新增自定义的 Present 方法,在实现中为被 Present 的 ViewController
添加转场代理,并设置 UIModalPresentationStyle
为 UIModalPresentationCustom
。
例如这样:
- (void)presentFadePatternViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion { MERGraduallyFadePresentationManager *graduallyFadePresentationManager = [[MERGraduallyFadePresentationManager alloc] init]; viewControllerToPresent.modalPresentationStyle = UIModalPresentationCustom; viewControllerToPresent.transitioningDelegate = graduallyFadePresentationManager; [self presentViewController:viewControllerToPresent animated:flag completion:completion];}复制代码
后续的拓展,只需要根据需求,新增动画代理控制器、转场代理控制器,然后像这样修改 ViewController
的 transitioningDelegate
即可。
目前发现的坑
问题主要集中在 iOS 11 及 iOS 11系统以下,动画的展示细节可能会有不同,也不清楚苹果又重构了他们什么代码实现……因此做转场请一定要在不同的系统环境下都跑一次看看。