博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
如何一行代码完成页面自定义跳转
阅读量:6637 次
发布时间:2019-06-25

本文共 10008 字,大约阅读时间需要 33 分钟。

前言

这篇博文主要是记录一些实现思路,顺便将平时实现转场动画时遇到的一些问题和细节整理下来,也方便巩固下知识。在这里作为示例的转场动画也是目前项目中有使用到的,如果后续还有新的转场方式也会放在这里。

使用

- (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 出来的 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 
)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 如何展现动画的简单问题。

这里有几点细节需要注意:

  1. Present / Push 动画需要手动将 UITransitionContextToViewKey 对应的 View addSubview 到 containerView 中。
  2. 动画的实现可以采用 CALayer 动画或者 UIView 动画,但是实测在 iOS 11 下,CALayer 动画无法通过手势控制器实时控制动画进度,不清楚是不是 bug。
  3. 动画结束请一定要调用 -(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 添加转场代理,并设置 UIModalPresentationStyleUIModalPresentationCustom

例如这样:

- (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];}复制代码

后续的拓展,只需要根据需求,新增动画代理控制器、转场代理控制器,然后像这样修改 ViewControllertransitioningDelegate 即可。

目前发现的坑

问题主要集中在 iOS 11 及 iOS 11系统以下,动画的展示细节可能会有不同,也不清楚苹果又重构了他们什么代码实现……因此做转场请一定要在不同的系统环境下都跑一次看看。

文章参考

转载地址:http://sxsvo.baihongyu.com/

你可能感兴趣的文章
objective-C: nonatomic retain copy assgin 等属性详解
查看>>
取消word里面所有超链接
查看>>
Java工程师书单(初级、中级、高级)
查看>>
Head First Python 读书笔记
查看>>
华为实习日记——第四十五天
查看>>
WPF(x:Null 使用)
查看>>
Tcpdump命令的使用与示例——linux下的网络分析
查看>>
github desktop 下载
查看>>
(转)java垃圾回收机制
查看>>
css2图片边框
查看>>
DNS与DSN
查看>>
TCP和UDP的优缺点及区别
查看>>
oracle之 关闭透明大页
查看>>
RAC 11.2的新特性
查看>>
位运算 之(1) 按位与(AND)& 操作
查看>>
video-视频标签
查看>>
The Art of Books
查看>>
SortedMap和TreeMap有什么区别?
查看>>
程序员不应轻易当真的那些话
查看>>
学习SpirngMVC之如何获取请求参数
查看>>