UIScrollView的滾動和觸摸

一、UIScrollView原理

從你的手指touch屏幕開始,scrollView開始一個timer,如果:

    1. 150ms內(nèi)如果你的手指沒有任何動作,消息就會傳給subView。
    1. 150ms內(nèi)手指有明顯的滑動(一個swipe動作),scrollView就會滾動,消息不會傳給subView。
    1. 150ms內(nèi)手指沒有滑動,scrollView將消息傳給subView,但是之后手指開始滑動,scrollView傳送touchesCancelled消息給subView,然后開始滾動。

- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;
系統(tǒng)默認是允許UIScrollView,按照消息響應(yīng)鏈向子視圖傳遞消息的。(即返回YES)。如果你不想UIScrollView的子視圖接受消息,返回NO。當(dāng)返回NO,表示UIScrollView接收這個滾動事件,不必沿著消息響應(yīng)鏈傳遞了。如果返回YES,touches事件沿著消息響應(yīng)鏈傳遞; 例如:scrollView上添加一個按鈕,返回NO的話,scrollView將不會將觸摸事件傳遞給按鈕,按鈕的event事件就不響應(yīng)。
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
返回YES 在這個view上取消進一步的touched消息(不在這個view上處理,事件傳到下一個view)。如果這個參數(shù)view不是一個UIControl對象,默認返回YES。如果是一個UIControl 對象返回NO。 返回NO它會停止拖動,將觸摸事件傳遞給子實圖例如scrollView上添加一個按鈕,按鈕是UIControl,返回NO,所以拖拽按鈕將不會滾動,為了能繼續(xù)滾動需要重寫這個方法返回YES;另外當(dāng)設(shè)置canCancelContentTouches = NO時,這個方法將不會被調(diào)用。

- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
     if ([view isKindOfClass:UIButton.class]) {
         return YES;
     }
     return [super touchesShouldCancelInContentView:view];
 }
例子
 MyScrollView *scrollView = [[MyScrollView alloc]init];
    [self.view addSubview:scrollView];
    scrollView.frame = CGRectMake(0, 0, self.view.bounds.size.width, 400);
    scrollView.backgroundColor = [UIColor redColor];
    scrollView.contentSize = CGSizeMake(0, self.view.frame.size.height);
    UIView *yellowview = [[GreenView alloc]init];
    yellowview.backgroundColor = [UIColor yellowColor];
    yellowview.frame = CGRectMake(100, 200, 200, 400);
    [scrollView addSubview:yellowview];

@interface MyScrollView : UIScrollView
@end
@implementation MyScrollView
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view{
    BOOL inContinue = [super touchesShouldBegin:touches withEvent:event inContentView:view];
    NSLog(@"是否將觸摸事件傳遞給子控件:%s",__func__);
    return inContinue;
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view{
    BOOL cancel = [super touchesShouldCancelInContentView:view];
    NSLog(@"是否取消進一步的touched消息:%s",__func__);
    return cancel;
}

測試一:如果手指快速滑動yellowview ,很明顯的滑動操作,控制臺不會打印任何東西。touchesShouldBegin和touchesShouldCancelInContentView都不會執(zhí)行。

根據(jù)上面UIScrollView原理可知,150ms內(nèi)手指有明顯的滑動,scrollView就會滾動,消息不會傳給subView。touchesShouldBegin系統(tǒng)默認是返回yes,也意味著只有消息傳給subView時才會被觸發(fā)。

測試二:如果先觸摸拖拽滑動yellowview,不明顯的滑動操作。控制臺會打印


截屏2020-10-29 下午2.59.57.png

根據(jù)上面UIScrollView原理可知,150ms內(nèi)手指沒有滑動之后手指開始滑動,scrollView傳送touchesCancelled消息給subView,然后開始滾動。

- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view{
    NSLog(@"不否將觸摸事件傳遞給子控件:%s",__func__);
    return NO;
}
  • 如果你想直接攔截touch事件的傳遞,你直接返回NO就可以了。
小結(jié):1、一個scrollView只有是明顯的滑動時,才不會將觸摸事件傳遞給子控件,其他情況下都會將觸摸事件傳給子控件。
2、直接重寫touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event inContentView返回NO時也不會將觸摸事件傳給子控件。
3、當(dāng)發(fā)生不明顯的滑動,首先會將觸摸事件傳給子控件。但是當(dāng)scrollView開始滑動時,scrollView傳送touchesCancelled消息給subView又會取消觸摸事件。

二、delaysContentTouches和canCancelContentTouches

@property(nonatomic) BOOL delaysContentTouches;   
default is YES. if NO, we immediately call -touchesShouldBegin:withEvent:inContentView:. this has no effect on presses

@property(nonatomic) BOOL canCancelContentTouches;    
 default is YES. if NO, then once we start tracking, we don't try to drag if the touch moves. this has no effect on presses

delaysContentTouches的作用:
這個標志默認是YES,使用上面的150ms的timer,如果設(shè)置為NO,touch事件立即傳遞給subView,不會有150ms的等待。默認YES;如果設(shè)置為NO,會馬上執(zhí)行touchesShouldBegin:withEvent:inContentView:(不管你滑得有多快,都能將事件立即傳遞給subView)

canCencelContentTouches從字面上理解是“可以取消內(nèi)容觸摸“,默認值為YES。文檔里的解釋是這樣的:翻譯為中文大致如下:
這個BOOL類型的值控制content view里的觸摸是否總能引發(fā)跟蹤(tracking)
如果屬性值為YES并且跟蹤到手指正觸摸到一個內(nèi)容控件,這時如果用戶拖動手指的距離足夠產(chǎn)生滾動,那么內(nèi)容控件將收到一個touchesCancelled:withEvent:消息,而scroll view將這次觸摸作為滾動來處理。如果值為NO,一旦content view開始跟蹤(tracking==YES),則無論手指是否移動,scrollView都不會滾動。

簡單通俗點說,如果為YES,就會等待用戶下一步動作,如果用戶移動手指到一定距離,就會把這個操作作為滾動來處理并開始滾動,同時發(fā)送一個touchesCancelled:withEvent:消息給內(nèi)容控件,由控件自行處理。如果為NO,就不會等待用戶下一步動作,并始終不會觸發(fā)scrollView的滾動了。

三、UIScrollView和hitTested-view

   MyScrollView*scrollView =[[MyScrollView alloc]init];
    scrollView.MyDelegate = self;
    [self.view addSubview:scrollView];
    scrollView.backgroundColor =[UIColor yellowColor];
    scrollView.frame = self.view.bounds;

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"點擊了控制器");
}

根據(jù)開發(fā)經(jīng)驗我們可以清楚的知道,因為scrollView的存在,touchesBegan事件不會再被觸發(fā),很明顯可以猜測到事件從window->scrollView,scrollView自己處理了touchesBegan:事件,并沒有繼續(xù)沿著響應(yīng)鏈傳遞.

為了讓它繼續(xù)沿著響應(yīng)鏈傳遞,我們就可以這樣

@implementation MyScrollView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.nextResponder touchesBegan:touches withEvent:event];
}

第二個例子:和上面基本上差不多的代碼,只是添加了一個手勢識別器

- (void)viewDidLoad {
    [super viewDidLoad];
    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap:)];
    [self.view addGestureRecognizer:gesture];
 
    MyScrollView*scrollView =[[MyScrollView alloc]init];
    [self.view addSubview:scrollView];
    scrollView.backgroundColor = [UIColor yellowColor];
    scrollView.frame = CGRectMake(0,0 , 100, 100);
}
- (void)tap:(UIGestureRecognizer*)geture{
    NSLog(@"測試");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"點擊了控制器");
}

這時你會發(fā)現(xiàn),touchesBegan似乎又沒響應(yīng)了。很顯然可以知道肯定是添加手勢影響的。沒錯,你只需要再添上這一行就如之前一樣了。

  gesture.cancelsTouchesInView = NO;

下面我們來分析一下原因吧:

UIScrollView 中有一個UIScrollViewDelayedTouchesBeganGestureRecognizer識別器,這個手勢會截斷hit-tested view事件并延遲0.15s才發(fā)送給hit-tested view。我們這里當(dāng)點擊屏幕時,首先會被gesture識別,而UIScrollViewDelayedTouchesBeganGestureRecognizer又會截斷hit-tested view事件,當(dāng)gesture識別完成后,hit-tested view事件也繼續(xù)發(fā)送過去,就會被取消。當(dāng)我們gesture.cancelsTouchesInView = NO;就不會再被取消,這樣hit-tested view事件會繼續(xù)沿著響應(yīng)鏈進行傳遞和處理。

實際應(yīng)用:點擊鍵盤收回鍵盤

- (void)viewDidLoad {
    [super viewDidLoad];
    UITextField *textFiled =[[UITextField alloc]init];
    [self.view addSubview:textFiled];
   // textFiled.frame = CGRectMake(0, 0, 100, 40);
    textFiled.backgroundColor = [UIColor redColor];
    [textFiled  becomeFirstResponder];
    
    UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tap)];
    [self.view addGestureRecognizer:gesture];
    gesture.cancelsTouchesInView = NO;
    
    UITableView *tableView = [[UITableView alloc]init];
    tableView.frame = self.view.bounds;
    [self.view addSubview:tableView];
    tableView.dataSource = self;
    tableView.delegate = self;
    tableView.estimatedRowHeight = 0 ;
    tableView.estimatedSectionHeaderHeight = 0;
    tableView.estimatedSectionFooterHeight = 0;
    [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
   
    return cell;
        
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
    NSLog(@"%@",touch.view);
    return YES;
}

#pragma mark - 點擊鍵盤收回鍵盤
- (void)tap{
    [self.view endEditing:YES];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"點擊cell");
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 10;
}
@end
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容