全屏侧滑实现POP手势动画

2016-12-28  本文已影响233人  雷鸣1010

方案一代码下载地址
方案二代码下载地址

前言

苹果在IOS7以后给导航控制器增加了一个Pop的手势,只要手指在屏幕边缘滑动,当前的控制器的视图就会跟随你的手指移动,当用户松手后,系统会判断手指拖动出来的大小来决定是否要执行控制器的Pop操作。
这个操作的想法非常好,但是系统给我们规定的范围必须是屏幕左侧边缘才可以触发,这样实际使用过程中对于有些产品会产生不便,于是有些app就采取整个屏幕都响应这个手势并且pop动画还是用系统原生的,这样操作起来确实方便好多。
开始大家一定会有疑问,给控制器的View加个手势然后拖动控制器的View时改变它的frame不就可以了吗?没错,加手势这个想法是正确的。但是,由我们自己来改变控制器视图的位置是比较麻烦的,细心的朋友一定发现了,我们自定义pop手势上面的导航栏也是在随着你的手势拖拽而变动的,所以这样做还需要负责导航栏的动画,而且有一个重点问题,如果单独拖动view,这个view下面会是黑黑的一片,因为控制器的push和pop层级是由系统管理的
所以走这条路虽然可以,但实现起来会比较艰辛。那么,如何实现这个效果呢?今天就给大家提供两套实现方案。

方案一:自定义UIViewControllerInteractiveTransitioning对象,实现导航控制器代理方法。

这套方案虽然实现比较麻烦,但是动画相对灵活,你可以实现这样的效果

苹果自带侧滑功能.gif 全屏侧滑pop功能.gif

其实这个拖动过程属于导航控制器的动画,所以我们需要重写UINavigationController的两个代理方法
navigationController:animationControllerForOperation:fromViewController:toViewController:(名字很长下面就称为方法1)和
navigationController:interactionControllerForAnimationController:(方法2)。
解释一下他们的作用
方法1是苹果提供给我们用来重写控制器之间转场动画的(pop或者push)。
方法2你可以这样理解,苹果让我们返回一个交互的对象,用来实时管理控制器之间转场动画的完成度,通过它我们可以让控制器的转场动画与用户交互(注意一点,如果方法1返回是nil,方法2是不会调用的,也就是说,只有我们自定义的动画才可以与控制器交互)。

下面我们来看一下实现过程。为了便于大家理解,我会尽量在Demo中的注释写的最清晰明了。
同时,我们先用最简单的代码实现,在这篇文章的最后我会对本例中的Demo提供一个相对合理的写法。

首先在方法1中,我们返回一个遵守了UIViewControllerAnimatedTransitioning协议的对象,它就是自定义的动画对象,我们给它起名PopAnimation,在这个类中实现两个方法来自定义转场动画。
方法1:

// 方法1是苹果公司提供给我们重写push 和 pop 动画的方法
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC {
    /**
     *  方法1中判断如果当前执行的是Pop操作,就返回我们自定义的Pop动画对象。
     */
    if (operation == UINavigationControllerOperationPop)
        return [[PopAnimation alloc] init];

    return nil;
}

自定义转场动画PopAnimation


@interface PopAnimation ()<UIViewControllerAnimatedTransitioning>

@property (nonatomic, strong) id <UIViewControllerContextTransitioning> transitionContext;
@end

@implementation PopAnimation

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
    //这个方法返回动画执行的时间
    return 0.25;
}

/**
 *  transitionContext你可以看作是一个工具,用来获取一系列动画执行相关的对象,并且通知系统动画是否完成等功能。
 */
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    /**
     *  获取动画来自的那个控制器
     */
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    /**
     *  获取转场到的那个控制器
     */
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    /**
     *  转场动画是两个控制器视图时间的动画,需要一个containerView来作为一个“舞台”,让动画执行。
     */
    UIView *containerView = [transitionContext containerView];
    [containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
    
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    /**
     *  执行动画,我们让fromVC的视图移动到屏幕最右侧
     */
    [UIView animateWithDuration:duration animations:^{
        fromViewController.view.transform = CGAffineTransformMakeTranslation([UIScreen mainScreen].bounds.size.width, 0);
    }completion:^(BOOL finished) {
        /**
         *  当你的动画执行完成,这个方法必须要调用,否则系统会认为你的其余任何操作都在动画执行过程中。
         */
        [transitionContext completeTransition:!transitionContext.transitionWasCancelled];
    }];
    
}

- (void)animationDidStop:(CATransition *)anim finished:(BOOL)flag {
    [_transitionContext completeTransition:!_transitionContext.transitionWasCancelled];
}
@end

再来看方法2,我们需要返回一个遵守了UIViewControllerInteractiveTransitioning协议的对象(提示一下,这两个协议容易混淆,要注意区分,一个是负责动画,一个是负责交互过程),苹果已经有一个类专门处理这个功能,它叫UIPercentDrivenInteractiveTransition,当然你也可以自定义一个这样的类。我们可以这样理解它的作用:
前面在方法1中返回的动画,会在执行的过程中被系统分解以用于用户交互,这个交互过程的动画完成度就由它来调控。下面我们来看一下如何使用它。(为了让控制器视图拖动,我们给控制器的视图加了一个拖动手势,在拖动方法里我们对这个对象进行操作)

/**
 *  我们把用户的每次Pan手势操作作为一次pop动画的执行
 */
- (void)handleControllerPop:(UIPanGestureRecognizer *)recognizer {
    /**
     *  interactivePopTransition就是我们说的方法2返回的对象,我们需要更新它的进度来控制Pop动画的流程,我们用手指在视图中的位置与视图宽度比例作为它的进度。
     */
    CGFloat progress = [recognizer translationInView:recognizer.view].x / recognizer.view.bounds.size.width;
    /**
     *  稳定进度区间,让它在0.0(未完成)~1.0(已完成)之间
     */
    progress = MIN(1.0, MAX(0.0, progress));
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        /**
         *  手势开始,新建一个监控对象
         */
        self.interactivePopTransition = [[UIPercentDrivenInteractiveTransition alloc] init];
        /**
         *  告诉控制器开始执行pop的动画
         */
        [self.vc popViewControllerAnimated:YES];
    }
    else if (recognizer.state == UIGestureRecognizerStateChanged) {
        
        /**
         *  更新手势的完成进度
         */
        [self.interactivePopTransition updateInteractiveTransition:progress];
    }
    else if (recognizer.state == UIGestureRecognizerStateEnded || recognizer.state == UIGestureRecognizerStateCancelled) {
        
        /**
         *  手势结束时如果进度大于一半,那么就完成pop操作,否则重新来过。
         */
        if (progress > 0.5) {
            [self.interactivePopTransition finishInteractiveTransition];
        }
        else {
            [self.interactivePopTransition cancelInteractiveTransition];
        }

        self.interactivePopTransition = nil;
    }
    
}

最后在视图控制器里重写导航栏的两个方法。

// 方法1是苹果公司提供给我们重写push 和 pop 动画的方法
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC {
    /**
     *  方法1中判断如果当前执行的是Pop操作,就返回我们自定义的Pop动画对象。
     */
    if (operation == UINavigationControllerOperationPop)
        return [[PopAnimation alloc] init];

    return nil;
}

// 方法2是你可以这样理解,苹果让我们返回一个交互的对象,用来实时管理控制器之间转场动画的完成度,通过它我们可以让控制器的转场动画与用户交互(注意一点,如果方法1返回是nil,方法2是不会调用的,也就是说,只有我们自定义的动画才可以与控制器交互)
- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {

    /**
     *  方法2会传给你当前的动画对象animationController,判断如果是我们自定义的Pop动画对象,那么就返回interactivePopTransition来监控动画完成度。
     */
    if ([animationController isKindOfClass:[PopAnimation class]])
        return self.interactivePopTransition;

    return nil;
}

有两点不要忘记:

设置导航控制器的代理为当前控制器。
给控制器加手势。
OK,这样我们就完成了这个过程。

方案二:Runtime+KVC

要了解这样的做法,需要有Runtime的一些知识,会涉及到私有变量、私有方法的获取,但是这样做比较简单也比较有趣,如果你感兴趣就继续看下去吧。关于Runtime的知识,今后我会分享到博客里,朋友们敬请期待。

为了方便大家阅读下面的代码,我们需要先了解系统的这个手势。

前面我们了解到,这个手势属于UINavigationController,我们就跳到它的头文件里看看能不能找到线索。这个思路是正确的,确实有一个手势叫做interactivePopGestureRecognizer。属性为readonly,就是说我们不能给他换成自定义的手势,但是可以设置enable=NO。ok,既然找到了它,就打印一下看看它到底是一个什么手势。

<UIScreenEdgePanGestureRecognizer: 0x7ff77d801a10; state = Possible; delaysTouchesBegan = YES; view = <UILayoutContainerView 0x7ff77b7014f0>; target= <(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7ff77d8018d0>)>>

通过log,我们看到他属于UIScreenEdgePanGestureRecognizer这个类(之前我是没有用到过),它继承自UIPanGestureRecognizer,出现在IOS7以后,是专门处理在屏幕边缘触发的手势类型,并且只有一个属性叫edges,用来设置它的触发边缘(上、下、左、右、全部)。看到这里一些朋友会想,直接改它的edges为全部可不可以?经过试验了解到,改这个属性是没用的,它只能用来触发边缘,设为全部的意思是四个方向的边缘会触发,而且用来做控制器POP手势的只有左边缘。

我们继续看它的log。控制台除了打印了它的类,还打印了它的触发target:_UINavigationInteractiveTransition(这是一个私有类,看来是专门用来做导航控制器交互动画的),和action:handleNavigationTransition(这是它的一个私有方法),我们要做的就是新建一个UIPanGestureRecognizer,让它的触发和系统的这个手势相同,这就需要利用runtime获取系统手势的target和action。

那么如何获取这个target呢?一开始我用kvc想直接获取这个手势的target,程序崩溃了,原来它根本没有这样一个属性。所以我能想到的是,先利用runtime遍历它的所有成员变量,看看系统是怎么存储这个属性的,

 unsigned int count = 0;
    // 获取UIGestureRecognizer的所有成员变量
    Ivar *ivar = class_copyIvarList([UIGestureRecognizer class], &count);
    for (int i = 0; i < count; i ++) {
        Ivar _var = *(ivar + i);
        NSLog(@"%s",ivar_getTypeEncoding(_var));
        NSLog(@"%s",ivar_getName(_var));
        
    }

通过log我们可以看到,UIGestureRecognizer有一个叫_targets的属性,它的类型为NSMutableArray。

2016-12-28 17:38:11.298 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.298 popaAnimation[3669:199104] _targets
2016-12-28 17:38:11.298 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.298 popaAnimation[3669:199104] _delayedTouches
2016-12-28 17:38:11.299 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.299 popaAnimation[3669:199104] _delayedPresses
2016-12-28 17:38:11.299 popaAnimation[3669:199104] @"UIView"
2016-12-28 17:38:11.299 popaAnimation[3669:199104] _view
2016-12-28 17:38:11.299 popaAnimation[3669:199104] d
2016-12-28 17:38:11.300 popaAnimation[3669:199104] _lastTouchTimestamp
2016-12-28 17:38:11.300 popaAnimation[3669:199104] q
2016-12-28 17:38:11.300 popaAnimation[3669:199104] _state
2016-12-28 17:38:11.300 popaAnimation[3669:199104] q
2016-12-28 17:38:11.301 popaAnimation[3669:199104] _allowedTouchTypes
2016-12-28 17:38:11.301 popaAnimation[3669:199104] q
2016-12-28 17:38:11.301 popaAnimation[3669:199104] _initialTouchType
2016-12-28 17:38:11.306 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _internalActiveTouches
2016-12-28 17:38:11.307 popaAnimation[3669:199104] @"_UIForceLevelClassifier"
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _forceClassifier
2016-12-28 17:38:11.307 popaAnimation[3669:199104] q
2016-12-28 17:38:11.307 popaAnimation[3669:199104] _requiredPreviewForceState
2016-12-28 17:38:11.307 popaAnimation[3669:199104] @"_UITouchForceObservable"
2016-12-28 17:38:11.308 popaAnimation[3669:199104] _touchForceObservable
2016-12-28 17:38:11.309 popaAnimation[3669:199104] @"NSObservation"
2016-12-28 17:38:11.309 popaAnimation[3669:199104] _touchForceObservableAndClassifierObservation
2016-12-28 17:38:11.309 popaAnimation[3669:199104] @"NSMutableArray"
2016-12-28 17:38:11.309 popaAnimation[3669:199104] _forceTargets
2016-12-28 17:38:11.310 popaAnimation[3669:199104] Q
2016-12-28 17:38:11.310 popaAnimation[3669:199104] _forcePressCount
2016-12-28 17:38:11.310 popaAnimation[3669:199104] @"NSObservationSource"
2016-12-28 17:38:11.310 popaAnimation[3669:199104] _beganObservable
2016-12-28 17:38:11.310 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _failureRequirements
2016-12-28 17:38:11.311 popaAnimation[3669:199104] @"NSMutableSet"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _failureDependents
2016-12-28 17:38:11.311 popaAnimation[3669:199104] @"<UIGestureRecognizerDelegate>"
2016-12-28 17:38:11.311 popaAnimation[3669:199104] _delegate
2016-12-28 17:38:11.312 popaAnimation[3669:199104] @"NSArray"
2016-12-28 17:38:11.312 popaAnimation[3669:199104] _allowedPressTypes
2016-12-28 17:38:11.312 popaAnimation[3669:199104] @"UIGestureEnvironment"
2016-12-28 17:38:11.312 popaAnimation[3669:199104] _gestureEnvironment

它是用数组来存储每一个target-action,所以可以动态的增加手势触发对象。那么又是怎么存储每一个target-action呢?为了了解这个我们拿到这个属性的名字"_targets"通过kvc获取它,接着打印出来。

 UIGestureRecognizer *gesture = self.interactivePopGestureRecognizer;
    NSMutableArray *_targets = [gesture valueForKey:@"_targets"];
    NSLog(@"%@",_targets);
    NSLog(@"&@",_targets[0]);

打印结果如下:

2016-12-28 17:56:00.073 popaAnimation[3757:211127] (
    "(action=handleNavigationTransition:, target=<_UINavigationInteractiveTransition 0x7faa4a60a4f0>)"
)
2016-12-28 17:56:04.677 popaAnimation[3757:211127] &@

可以看到,由于系统重写了它的description方法,所以我们没办法通过打印获取这个对象是什么类型。既然不能打印,那么我们就用断点调试,来看它的真实类型,

屏幕快照 2016-12-28 下午6.03.22.png

我们看到,原来每一个target-action是用UIGestureRecognizerTarget这样一个类来存储的,它也是一个私有类。
苹果把许多的类做私有化也是有原因所在,其实在平时我们拿到这个类也是没有用的,他们的目的之一是避免对开发者公开无用的类,影响了封装性。所以在类的设计上,还是要向苹果学习。

下面直接看代码。

我们在控制器的ViewDidLoad加上这段代码,并且它只需要执行一次。

 UIGestureRecognizer *gesture = self.interactivePopGestureRecognizer;    
    gesture.enabled = NO;
    UIView *gestureView = gesture.view;
    
    UIPanGestureRecognizer *popRecognizer = [[UIPanGestureRecognizer alloc] init];
    popRecognizer.delegate = self;
    popRecognizer.maximumNumberOfTouches = 1;
    [gestureView addGestureRecognizer:popRecognizer];
    
    /**
     *  获取系统手势的target数组
     */
    NSMutableArray *_targets = [gesture valueForKey:@"_targets"];
    /**
     *  获取它的唯一对象,我们知道它是一个叫UIGestureRecognizerTarget的私有类,它有一个属性叫_target
     */
    id gestureRecognizerTarget = [_targets firstObject];
    /**
     *  获取_target:_UINavigationInteractiveTransition,它有一个方法叫handleNavigationTransition:
     */
    id navigationInteractiveTransition = [gestureRecognizerTarget valueForKey:@"_target"];
    /**
     *  通过前面的打印,我们从控制台获取出来它的方法签名。
     */
    SEL handleTransition = NSSelectorFromString(@"handleNavigationTransition:");
    /**
     *  创建一个与系统一模一样的手势,我们只把它的类改为UIPanGestureRecognizer
     */
    [popRecognizer addTarget:navigationInteractiveTransition action:handleTransition];
    

优化

这个demo我会提供给大家,下面简单说下程序的优化思路。

优化点一:对于方案一,其实不应该把导航控制器的代理方法以及手势处理的方法交给视图控制器,因为这段代码不是属于某一个视图控制器,而是全局的导航控制器,所以我们应该参考苹果的设计思想:新建一个专门管理交互过程的对象,这个类我们叫做NavigationInteractiveTransition。

优化点二:再来看之前的ViewDidLoad中只执行一次的代码,其实写在这里也不够妥当,同样的,这段代码也不属于某一个Controller,优化方案是新建一个导航控制器,在这个导航控制器的viewDidLoad中写上这些代码,这样也并不需要dispatch once。

优化点三:由于我们自定义的手势是加在一个私有view上,这个view是一个全局的,所以当这个控制器为根控制器时,我们的手势还是在起作用,这就相当于对根控制器做了pop操作,这会出现一个错误nested pop animation can result in corrupted navigation bar。导致这个错误的原因还有一个,如果我们pop的动画正在执行,再去触发一次手势,会导致导航控制器和导航条的动画混乱。为了避免问题出现我们需要成为手势的代理,判断当前控制器是否为根控制器并且pop或者push动画是否在执行(这个变量是私有的,需要用kvc来获取)。

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    /**
     *  这里有两个条件不允许手势执行,1、当前控制器为根控制器;2、如果这个push、pop动画正在执行(私有属性)
     */
    return self.viewControllers.count != 1 && ![[self valueForKey:@"_isTransitioning"] boolValue];
}

经过最后的优化,视图控制器可以什么都不写,想使用这个效果,只要使用我们自定义的导航控制器就可以了,这样的好处是手势动画与控制器完全解耦,并且不用给每一个控制器都addGesture。

文/J_雨(简书作者)
原文链接:http://www.jianshu.com/p/d39f7d22db6c
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

上一篇下一篇

猜你喜欢

热点阅读