【完美解决篇】iOS11下底部手势延迟引起的问题

情景再现

如下图所示,仿微信聊天界面的录音按钮,放在屏幕的最下方

示例图.png

问题一:每次按钮按下时都有1秒左右的延迟,体验很差。于是按照网上所说的,将屏幕底部的系统手势屏蔽掉;
问题二:即便按照上述方法屏蔽掉了系统手势,按钮的左半部分依然存在延迟。于是继续按照网上所说的,将导航的右滑返回屏蔽掉。

如此的解决方案,大概没有人会采用。
难道真如网上所说的,二者不可兼得吗?
但是为何微信却做到了?
难道是我不如马化腾吗?
作为一个站在食物链顶端的男人,我不服!
带着这种不屈不挠的精神,我开始了探索...
(探索过程略)
终于,我成功了!

下面是完整的处理代码,下文会结合代码一一讲解

#import "LLChatViewController.h"

//底部view的高
#define TOOL_HEIGHT 49
//屏幕高
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
@interface LLChatViewController ()

//记录当前导航的手势代理
@property (nonatomic, weak) id recognizerDelegate;
@property (nonatomic, assign, getter=isDeferredSystemGestures) BOOL deferredSystemGestures;

@end

@implementation LLChatViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //首先解决屏幕底部的系统手势引起的延迟
    //屏蔽系统底部手势
    self.deferredSystemGestures = YES;
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self updateRecognizerDelegate:YES];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self updateRecognizerDelegate:NO];
}

#pragma mark - 录音按钮手势冲突处理
//设置手势代理
- (void)updateRecognizerDelegate:(BOOL)appear {
    if (appear) {
        if (self.recognizerDelegate == nil) {
            self.recognizerDelegate = self.navigationController.interactivePopGestureRecognizer.delegate;
        }
        self.navigationController.interactivePopGestureRecognizer.delegate = self;
    }
    else {
        self.navigationController.interactivePopGestureRecognizer.delegate = self.recognizerDelegate;
    }
}

//是否响应触摸事件
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if (self.navigationController.viewControllers.count <= 1) return NO;
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        CGPoint point = [touch locationInView:gestureRecognizer.view];
        //判断是否触摸在底部view上, 是则不响应导航右滑返回事件
        if (point.y > SCREEN_HEIGHT-TOOL_HEIGHT) {
            return NO;
        }
        if (point.x <= 100) {//设置手势触发区
            return YES;
        }
    }
    return NO;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        CGFloat tx = [(UIPanGestureRecognizer *)gestureRecognizer translationInView:gestureRecognizer.view].x;
        if (tx < 0) {
            return NO;
        }
    }
    return YES;
}

//是否与其他手势共存,一般使用默认值(默认返回NO:不与任何手势共存)
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    
    //UIScrollView的滑动冲突
    if ([otherGestureRecognizer.view isKindOfClass:[UIScrollView class]]) {
        
        UIScrollView *scrollow = (UIScrollView *)otherGestureRecognizer.view;
        if (scrollow.bounds.size.width >= scrollow.contentSize.width) {
            return NO;
        }
        if (scrollow.contentOffset.x == 0) {
            return YES;
        }
    }
    return NO;
}

//屏蔽屏幕底部的系统手势
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
    if (self.isDeferredSystemGestures) {
        return  UIRectEdgeBottom;
    }
    return UIRectEdgeNone;
}

@end

细心地同学可能会发现, deferredSystemGestures这个属性我就在viewDidLoad中赋值了一次,自始至终都是YES。
那么你们肯定有疑问,既然如此,下面这段代码:

//屏蔽屏幕底部的系统手势
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
    if (self.isDeferredSystemGestures) {
        return  UIRectEdgeBottom;
    }
    return UIRectEdgeNone;
}

是不是可以写成这样:

//屏蔽屏幕底部的系统手势
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
    return  UIRectEdgeBottom;
}

答案是不可以,并且deferredSystemGestures赋值这段代码,只能放在viewDidLoad被调用之后。假如将deferredSystemGestures赋值写在viewDidLoad被调用之前,比如说放在初始化init里面,这两段代码就会等价。

下面我们来详细讲解preferredScreenEdgesDeferringSystemGestures方法的调用时机:
1、当viewDidLoad被调用之前,系统会自动调用preferredScreenEdgesDeferringSystemGestures方法,以确定是否延迟处理屏幕边缘的系统手势。此时, self.isDeferredSystemGestures的值为false,方法返回结果为UIRectEdgeNone,不延迟任何边缘手势,才使得屏幕底部上滑事件成为可能;
2、当视图加载完成后,我们点击录音按钮的时候,由于按钮位置靠近屏幕边缘,造成手势冲突,系统会调用preferredScreenEdgesDeferringSystemGestures方法来确定优先处理哪个手势。此时, self.isDeferredSystemGestures的值为true,方法返回结果为UIRectEdgeBottom,延迟处理屏幕底部手势,才使得录音按钮能第一时间响应成为可能;
3、当我们直接从手机边缘上滑时,无手势冲突,因此,不会调用preferredScreenEdgesDeferringSystemGestures,并且视图加载前系统已确认不延迟任何边缘手势,因此可直接处理上滑事件。

至此,由屏幕底部的系统手势引起的延迟已完美解决。

接下来就是问题二,导航右滑返回引起的按钮左半部分的延迟。
解决方法也不难,哪里有冲突就从哪里解决。既然是右滑返回引起的,那我们首先拿到右滑手势。

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self updateRecognizerDelegate:YES];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self updateRecognizerDelegate:NO];
}

//设置手势代理
- (void)updateRecognizerDelegate:(BOOL)appear {
    if (appear) {
        if (self.recognizerDelegate == nil) {
            self.recognizerDelegate = self.navigationController.interactivePopGestureRecognizer.delegate;
        }
        self.navigationController.interactivePopGestureRecognizer.delegate = self;
    }
    else {
        self.navigationController.interactivePopGestureRecognizer.delegate = self.recognizerDelegate;
    }
}

这段代码的大概意思就是,当视图出现时,将手势代理设置为self,当时图消失时,再还原成原来的值。
接下来就是手势的处理

//是否响应触摸事件
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    if (self.navigationController.viewControllers.count <= 1) return NO;
    if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
        CGPoint point = [touch locationInView:gestureRecognizer.view];
        //判断是否触摸在底部view上, 是则不响应导航右滑返回事件
        if (point.y > SCREEN_HEIGHT-TOOL_HEIGHT) {
            return NO;
        }
        if (point.x <= 100) {//设置手势触发区
            return YES;
        }
    }
    return NO;
}

当我们触摸到这个位置point.y > SCREEN_HEIGHT-TOOL_HEIGHT,也就是底部整个录音视图的位置时,返回false,不响应滑动返回,使得录音按钮能第一响应成为可能。

见证奇迹的时刻

需要看效果的同学可以去下载我写的这个聊天框架,试一试私聊界面的录音按钮。
一个完整的聊天UI框架 - 点我下载

你可能感兴趣的:(【完美解决篇】iOS11下底部手势延迟引起的问题)