前言
这篇博文主要是记录一些实现思路,顺便将平时实现转场动画时遇到的一些问题和细节整理下来,也方便巩固下知识。在这里作为示例的转场动画也是目前项目中有使用到的,如果后续还有新的转场方式也会放在这里。
使用
- (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 转场动画的详细解读,可以参考这两篇文章:王巍写的 ViewController切换 和唐巧写的 iOS 视图控制器转场详解 。篇幅较长,写的非常详细。
1.实现转场代理
UIViewController
的转场代理为transitioningDelegate
属性,遵循<UIViewControllerTransitioningDelegate>
协议;UINavigationController
的转场代理为delegate
属性,遵循<UINavigationControllerDelegate>
协议;UITabBarController
的转场代理为delegate
属性,遵循<UITabBarControllerDelegate>
协议;
因此我们只需要新建一个代理类,遵循并实现相应的代理,然后赋值给对应的代理属性即可。
其中 Present 转场需要给被 present 出来的
UIViewController
设置代理,实现 present 和 dismiss 的自定义动画。 Push 转场则需要给导航控制器UINavigationController
设置代理,需要注意设置代理后如果只为限定的 VC 采用自定义动画,需要在代理的实现方法中做区分才行。
Tabbar 转场同理。
本篇主要以 Present 转场举例,下面是 <UIViewControllerTransitioningDelegate>
需要实现的方法
- (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
// 关于 UIPresentationController 类的功能描述可以参照上面贴出的唐巧的博客,主要作用可以总结为自定义 presentedView 的尺寸以及添加动画。例如需要 Present 出来的 VC 并非满屏大小时(参照系统的 ActionSheet 控件),只需要在这里面做简单的设置即可;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
// Present 动画执行时需要提供的动画控制器
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
// Dismiss 动画执行时需要提供的动画控制器
// ps: 方便的做法是共用一个动画控制器,通过 Bool 值区别是 Present 或者 Dismiss,来区别动画实现细节
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
// 为转场增加手势控制
}
复制代码
2.转场的动画控制器
无论是上面哪种控制器的转场代理,均需要提供一个实现了 <UIViewControllerAnimatedTransitioning>
协议的动画控制器,一般新建一个继承自NSObject
的类遵循并实现这个协议即可。
该协议主要实现两个方法:
// 转场动画的时间
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext;
// 转场动画的具体实现
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
复制代码
核心点在于 -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext;
方法的实现。
3.转场动画实现的相关细节
上下文参数 transitionContext
遵循了 <UIViewControllerContextTransitioning>
协议,开发者们可以根据上下文拿到实现动画所需要的重要的信息:
// 动画发生的容器 View
transitionContext.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<UIViewControllerContextTransitioning>)transitionContext {
return 0.4; // 动画执行时间
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)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系统以下,动画的展示细节可能会有不同,也不清楚苹果又重构了他们什么代码实现……因此做转场请一定要在不同的系统环境下都跑一次看看。