情景再现
如下图所示,仿微信聊天界面的录音按钮,放在屏幕的最下方
问题一:每次按钮按下时都有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框架 - 点我下载