需求:UIScrollView+btns。點擊btn高亮(Highlighted),滑動UIScrollView時取消高亮;研究了一下,整理如下:
1.實現:點擊btn高亮(Highlighted),且滑動UIScrollView時保持高亮狀態,是不合理的,無法實現的。
2.實現:點擊btn高亮(Highlighted),滑動UIScrollView時取消高亮狀態,是可以做到的。
按照從不同的要素去解釋原理:
一、UIScrollView原理,以時間為軸線:
從你的手指touch屏幕開始,scrollView開始一個timer,如果:
1.150ms內如果你的手指沒有任何動作,消息就會傳給subView。
2.150ms內手指有明顯的滑動(一個swipe動作),scrollView就會滾動,消息不會傳給subView。
3. ?150ms內手指沒有滑動,scrollView將消息傳給subView,但是之后手指開始滑動,scrollView傳送touchesCancelled消息給subView,然后開始滾動。
二、UIScrollView原理,以tracking屬性為軸線:
UIScrollView有一個BOOL類型的tracking屬性,用來返回用戶是否已經觸及內容并打算開始滾動,我們從這個屬性開始探究UIScrollView的工作原理:
當手指觸摸到UIScrollView內容的一瞬間,會產生下面的動作:
攔截觸摸事件
tracking屬性變為YES
一個內置的計時器開始生效,用來監控在極短的事件間隔內是否發生了手指移動
case1:當檢測到時間間隔內手指發生了移動,UIScrollView自己觸發滾動,tracking屬性變為NO,手指觸摸下即使有(可以響應觸摸事件的)內部控件也不會再響應觸摸事件。
case2:當檢測到時間間隔內手指沒有移動,tracking屬性保持YES,手指觸摸下如果有(可以響應觸摸事件的)內部控件,則將觸摸事件傳遞給控件進行處理。
當你手指是緩慢劃過或根本就沒動,才會觸發UIButton的觸摸事件,這是case1的情況;
有很多新聞類的App頂部都有一個滑動菜單欄,主要模型可能是由一個UIScrollView包含多個UIButton控件組成;當你操作的時候,手指如果是很迅速的在上面劃過,會發現即使手指觸摸的地方有UIButton,但是并沒有觸發該UIButton的任何觸摸事件,這就是上面提到的case2。
上面的工作原理其實有一個屬性開關來控制:delaysContentTouches。
官方解釋:
A Boolean value that determines whether the scroll view delays the handling of touch-down gestures.
中文翻譯:
一個布爾值,該值決定了滾動視圖是否延遲了觸控手勢的處理。
默認值為YES;如果設置為NO,則無論手指移動的多么快,始終都會將觸摸事件傳遞給內部控件;設置為NO可能會影響到UIScrollView的滾動功能。
delaysContentTouches的作用:
這個標志默認是YES,使用上面的150ms的timer,如果設置為NO,touch事件立即傳遞給subView,不會有150ms的等待。
If the value of this property isYES, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value isNO, the scroll view immediately callstouchesShouldBegin:withEvent:inContentView:. The default value isYES.
默認YES;如果設置為NO,會馬上執行touchesShouldBegin:withEvent:inContentView:
- (BOOL)touchesShouldBegin:(NSSet<UITouch*> *)toucheswithEvent:(UIEvent*)event
inContentView:(UIView*)view
The default behavior ofUIScrollViewis to invoke theUIResponderevent-handling methods of the target subview that the touches occur in.
系統默認是允許UIScrollView,按照消息響應鏈向子視圖傳遞消息的。(即返回YES)
ReturnNOif you don’t want the scroll view to send event messages toview. If you wantviewto receive those messages, returnYES(the default).
如果你不想UIScrollView的子視圖接受消息,返回NO。
應用描述(作者注釋):這個方法是最先接收到滑動事件的(優先于button的
UIControlEventTouchDown,以及- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event),
如果返回YES,touche事件沿著消息響應鏈傳遞;
如果返回NO,表示UIScrollView接收這個滾動事件,不必沿著消息響應鏈傳遞了。
- (BOOL)touchesShouldCancelInContentView:(UIView*)view
應用描述(作者注釋):
如果返回YES:(系統默認)是允許UIScrollView,按照消息響應鏈向子視圖傳遞消息的
如果返回NO:UIScrollView,就接收不到滑動事件了。
再看另一個BOOL類型的屬性canCancelContentTouches,從字面上理解是“可以取消內容觸摸“,默認值為YES。文檔里的解釋是這樣的:
A Boolean value that controls whether touches in the content view always lead to tracking.
If the value of this property is YES and a view in the content has begun
tracking a finger touching it, and if the user drags the finger enough
to initiate a scroll, the view receives a touchesCancelled:withEvent:
message and the scroll view handles the touch
as a scroll. If the value of this property is NO, the scroll view does
not scroll regardless of finger movement once the content view starts
tracking.
翻譯為中文大致如下:
這個BOOL類型的值控制content view里的觸摸是否總能引發跟蹤(tracking)
cancelsTouches的作用:
應用描述(作者注釋):
默認設置為YES,
如果設置為NO,這消息一旦傳遞給subView,這scroll事件不會再發生。
如果屬性值為YES并且跟蹤到手指正觸摸到一個內容控件,這時如果用戶拖動手指的距離足夠產生滾動,那么內容控件將收到一個touchesCancelled:withEvent:消息,而scroll
view將這次觸摸作為滾動來處理。如果值為NO,一旦content
view開始跟蹤(tracking==YES),則無論手指是否移動,scrollView都不會滾動。
簡單通俗點說,如果為YES,就會等待用戶下一步動作,如果用戶移動手指到一定距離,就會把這個操作作為滾動來處理并開始滾動,同時發送一個touchesCancelled:withEvent:消息給內容控件,由控件自行處理。如果為NO,就不會等待用戶下一步動作,并始終不會觸發scrollView的滾動了。
可以用一段代碼來驗證并觀察一下,定義一個MyScrollView繼承自UIScrollView,一個MyButton繼承自UIButton,然后重寫部分方法:
MyScrollView.m
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
[super touchesShouldCancelInContentView:view];
NSLog(@"touchesShouldCancelInContentView");returnYES;
}- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
NSLog(@"touchesCancelled");
}
MyButton.m
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
NSLog(@"【Button's touch cancelled】");
}- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesBegan:touches withEvent:event];
NSLog(@"【Button's touch began】");
}- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesMoved:touches withEvent:event];
NSLog(@"【Button's touch moved】");
}- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
NSLog(@"【Button's touch ended】");
}
其實就是在各個方法執行時打印出一個標記,當canCencelContentTouches值為YES時,用戶觸摸并移動手指再放開:
【Button's touch began】
【Button's touch moved】
……
【Button's touch moved】
touchesShouldCancelInContentView
【Button's touch cancelled】
當canCencelContentTouches值為NO時,用戶觸摸并移動手指再放開:
【Button's touch began】
【Button's touch moved】
……
【Button's touch moved】
【Button's touch ended】