转载

iOS 手势识别的工作原理及简单应用

在项目中遇到个问题就是使用AVPlayer播放视频时,如果用系统的手势返回,会出现视频播放卡顿的问题.
为了实现自定义手势返回, 我参考了<<精通iOS框架>>里手势章节介绍的内容. 给大家介绍一下关于手势的一些总结.下面的第一部分主要引用自这本书.

手势识别对象是如何工作的?

第一个需要理解的概念是手势识别操作是在常规视图响应链之外的. 首先 UIWindow 会将触碰事件发送到手势识别对象, 他们必须指明无法处理该事件, 之后该事件才会默认向前传到视图的响应链.

当应用试图确定是否有手势动作被识别时, 事件发生的顺序非常重要.

  1. 窗口会将触碰事件发送到手势识别对象.

  2. 手势识别对象将进入 UIGestureRecognizerStatePossible 状态.

  3. 对于离散型手势, 手势识别对象会判断他是 UIGestureRecognizerStateRecognized 还是 UIGestureRecognizerStateFailed 类型.
    3.1 如果是 UIGestureRecognizerStateRecognized, 手势识别接收触碰时间并调用指定的委托方法.
    3.2 如果是 UIGestureRecognizerStateFailed, 手势识别对象将触碰事件返回到响应链.

  4. 对于连续型手势, 手势识别对象会判断他是 UIGestureRecognizerStateBegan 还是UIGestureRecognizerStateFailed.
    4.1 如果是 UIGestureRecognizerStateBegan, 手势识别对象会接收触控事件并调用指定的委托方法. 之后每当手势发生变化时就将状态更新为 UIGestureRecognizerStateChanged, 并始终调用委托方法, 直到最后的触碰事件结束, 此时状态为 UIGestureRecognizerStateEnded. 如果触碰模式不再和期望的手势相匹配, 可以将状态改为 UIGestureRecognizerStateCancelled.
    4.2 如果是 UIGestureRecognizerStateFailed, 手势识别对象将触碰事件返回到响应链.

注意, UIGestureRecognizerStatePossible 和 UIGestureRecognizerStateFailed 这两个状态之间消耗的时间很久. 如果用户界面中的手势识别对象由于触碰动作导致运行缓慢而无法理解, 问题可能就出在这里. 最好的办法是在具体的处理方法中添加记录--每次方法被调用时就记录状态. 之后就可以根据记录的时间戳信息很清楚地了解状态变化的过程, 也就可以很好地判断延迟问题出在哪里(这部分内容不太理解...汗, 不过不影响写全屏手势).

在一个视图中识别多个手势

理解了上述步骤, 我们来处理两个或多个手势识别对象. 这需要实现 UIGestureRecognizerDelegate 协议, 对触碰动作如何发送到手势识别对象进行更加复杂的控制.
主要看下面三个协议方法:

  • - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
    使用这个方法来表示手势识别对象根据应用的状态, 是否应该从 UIGestureRecognizerStatePossible 转换为 UIGestureRecognizerStateBegan 状态. 如果返回 YES, 进行手势识别对象的处理; 否则状态变为 UIGestureRecognizerStateFailed.

  • - (BOOL)gestureRecognizer:(UIGestureRecognizer )gestureRecognizer shouldReceiveTouch:(UITouch )touch;
    使用这个方法来表示手势识别对象是否应该接受一个触碰动作. 这个方法可以让手势识别对象根据用户设置的一些约束条件拒绝识别手势动作.

  • - (BOOL)gestureRecognizer:(UIGestureRecognizer )gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer )otherGestureRecognizer;
    当有超过一个手势识别对象同步接收触碰事件时, 使用这个方法. 返回YES则同步 处理所有请求, 或者对传入的手势识别对象进行测试, 判断这些动作是否满足同步处理的要求.

实际应用

项目 SJVideoPlayerBackGR (全屏手势)

在处理手势识别过程中, 首先实现的有:

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
    if ( self.childViewControllers.count <= 1 ) return NO;
    CGPoint translate = [gestureRecognizer translationInView:self.view];
    BOOL possible = translate.x > 0 && translate.y == 0;
    if ( possible ) return YES;
    else return NO;
}

此方法是上面提到的第一个协议方法, 表示手势识别对象根据应用的状态, 是否应该从 Possible 转换为 Began 状态.
在这里我判断了子控制器的数量, 以及用户操作的方向, 如果是横向向右操作, 就返回YES, 启用全屏返回手势.

同时考虑到存在多个手势识别对象的情况, 我还实现了下面的方法:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    if ( [otherGestureRecognizer isMemberOfClass:NSClassFromString(@"UIPanGestureRecognizer")] ) {
        return NO;
    }
    if ([otherGestureRecognizer isMemberOfClass:NSClassFromString(@"UIScrollViewPanGestureRecognizer")] ||
        [otherGestureRecognizer isMemberOfClass:NSClassFromString(@"UIScrollViewPagingSwipeGestureRecognizer")]) {
        if ( [otherGestureRecognizer.view isKindOfClass:[UIScrollView class]] ) {
            return [self SJVideoPlayer_considerScrollView:(UIScrollView *)otherGestureRecognizer.view otherGestureRecognizer:otherGestureRecognizer];
        }
        return NO;
    }
    return YES;
}

此方法是上面提到的第三个协议方法, 表示当有一个以上手势识别对象存在时, 是否同步处理这些手势对象.

在实现中, 第一种情况是UIPanGestureRecognizer. 如果是别的pan 手势, 为了避免冲突, 在这里直接返回了 NO, 表示不触发返回手势.

第二个和第三个, 都是同scrollView的相关的滑动手势, 当存在scrollView时, 需要考虑多个 scrollView 嵌套的问题.

- (BOOL)SJVideoPlayer_considerScrollView:(UIScrollView *)subScrollView otherGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    if ( 0 != subScrollView.contentOffset.x ) return NO;
    // 当scrollview.contentOffset.x == 0, 判断手势移动方向, 向左还是向右
    CGPoint translate = [self.sj_pan translationInView:self.view];
    if ( translate.x <= 0 ) return NO; // 如果向左, 不触发全屏手势
    else {
        [otherGestureRecognizer setValue:@(UIGestureRecognizerStateCancelled) forKey:@"state"];
        return YES;
    }
}

首先, 在多个scrollView 嵌套的情况下需要一个何时触发全屏手势的约定, 我这里的约定是当子scrollView的contentOffset.x == 0的情况下, 触发全局手势. 在以上代码中, 我判断了手势移动的方向(向左还是向右), 同时考虑了contentOffset.x是否等于0的情况. 效果如下:

iOS 手势识别的工作原理及简单应用

全屏手势遇到的问题

问题1:

全屏手势在触发后, 移动的过程中会出现如下问题:

iOS 手势识别的工作原理及简单应用

为了避免这种情况, 我在为viewController添加了三个分类方法:

/// 全屏手势触发
@property (nonatomic, copy, readwrite) void(^sj_viewWillBeginDragging)(__kindof UIViewController *vc);
/// 全屏手势触发中
@property (nonatomic, copy, readwrite) void(^sj_viewDidDrag)(__kindof UIViewController *vc);
/// 全屏手势结束
@property (nonatomic, copy, readwrite) void(^sj_viewDidEndDragging)(__kindof UIViewController *vc);

同时, 在用到scrollView的地方, 做了如下处理:

   self.sj_viewWillBeginDragging = ^(ViewController *viewController) {
        viewController.collectionView.scrollEnabled = NO;
    };
    self.sj_viewDidEndDragging = ^(ViewController *viewController) {
        viewController.collectionView.scrollEnabled = YES;
    };

这样, 在全屏手势触发的情况下, 使得scrollView停止滚动, 结束全屏手势时, 恢复scrollView滚动, 效果如下:

iOS 手势识别的工作原理及简单应用

不过, 后来的处理是当全屏手势触发后, 找到其视图中的根scrollView, 将其禁止滚动, 以下代码是找到视图中的根根scrollView, 并在手势触发开始时禁止滚动, 手势结束后开启滚动.

- (UIScrollView *)_findingSubScrollView {
    __block UIScrollView *scrollView = nil;
    [self.topViewController.view.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if ( ![obj isKindOfClass:[UIScrollView class]] ) return;
        *stop = YES;
        scrollView = obj;
    }];
    return scrollView;
}
- (void)SJVideoPlayer_handlePanGR:(UIPanGestureRecognizer *)pan {
    CGFloat offset = [pan translationInView:self.view].x;
    switch (pan.state) {
        case UIGestureRecognizerStateBegan: {
            [self SJVideoPlayer_ViewWillBeginDragging];
        }
            break;
        case UIGestureRecognizerStateChanged: {
            if ( offset < 0 ) return;
            [self SJVideoPlayer_ViewDidDrag:offset];
        }
            break;
        case UIGestureRecognizerStatePossible:
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateFailed: {
            [self SJVideoPlayer_ViewDidEndDragging:offset];
        }
            break;
    }
}

问题2:

在使用UIPageViewController时, 由于其复用机制, 无法确定他内部的contentOffset,  所以当遇到UIPageViewController的控制器中, 我使用了系统的边缘返回手势, 当在边缘滑动时, 触发系统返回, 当在中间部分滑动时, 触发UIPageViewController滚动.
为此, 我为UINavigationController添加了分类方法, 用于来回切换两个手势:

// 用于来回切换两个手势
@property (nonatomic, assign, readwrite) BOOL useNativeGesture;- (void)viewWillAppear:
(BOOL)animated {
    [super viewWillAppear:animated];
    self.navigationController.useNativeGesture = YES;
}
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.navigationController.useNativeGesture = NO;
}

问题3:

在没有设置navigationBar的颜色时, 10以下系统导航栏头部莫名变成了黑色...

iOS 手势识别的工作原理及简单应用

于是我在基类的NavigationController中添加了下面代码

// NavigationController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationBar.barTintColor = [UIColor whiteColor];
    self.view.backgroundColor = [UIColor whiteColor];
    // Do any additional setup after loading the view.
}

Over..

  • 作者:changsanjiang

  • 链接:https://juejin.im/post/5a150c166fb9a04524057832

  • 来源:掘金

  • 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

正文到此结束
Loading...