在實現上一篇介紹的自定義滑動關聯菜單控件BFScrollMenu時,關于滑動方向判斷的邏輯其實一開始是準備用手勢操作來實現的,結果發現在ScrollView中處理手勢的邏輯比較困難,在寫的時候還沒有仔細研究過ScrollView的滑動原理,只是知道需要自定義一個ScrollView才能實現,但是我的本意是不希望用戶還要顯示指定一個自定義的ScrollView而是直接用category進行無縫的對接,所以就改用didScroll delegate 用offset來計算滑動邏輯了,效果可以接受。
回過頭來花了些時間仔細研究了下UIScrollView的滑動處理邏輯,這樣就可以采用Customer ScrollView來實現同樣的BFScrollMenu邏輯了。
-
UIScrollView的滑動處理原理
網上相關的文章有很多,但是我感覺沒有一個能清晰的解釋清楚。這里我用流程圖的方式,把我自己經過試驗后的結論和理解和大家分享一下,希望能幫助到你。如果有不正確的歡迎指正。
首先我們來看Apple的官方代碼文檔里的注釋:
Scrolling with no scroll bars is a bit complex. on touch down, we don't know if the user will want to scroll or track a subview like a control. on touch down, we start a timer and also look at any movement. if the time elapses without sufficient change in position, we start sending events to the hit view in the content subview. if the user then drags far enough, we switch back to dragging and cancel any tracking in the subview. the methods below are called by the scroll view and give subclasses override points to add in custom behaviour. you can remove the delay in delivery of touchesBegan:withEvent: to subviews by setting delaysContentTouches to NO.
然后再往下看2個可以set的property和2個可以重載的方法:
// default is YES. if NO, we immediately call -touchesShouldBegin:withEvent:inContentView:.
// this has no effect on presses
@property(nonatomic) BOOL delaysContentTouches;
// 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
@property(nonatomic) BOOL canCancelContentTouches;
// override points for subclasses to control delivery of touch events to subviews of the scroll view
// called before touches are delivered to a subview of the scroll view.
// if it returns NO the touches will not be delivered to the subview
// this has no effect on presses
// default returns YES
- (BOOL)touchesShouldBegin:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event inContentView:(UIView *)view;
// called before scrolling begins if touches have already been delivered to a subview of the scroll view.
// if it returns NO the touches will continue to be delivered to the subview and scrolling will not occur
// not called if canCancelContentTouches is NO. default returns YES if view isn't a UIControl.
// this has no effect on presses
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;
有了以上這些,已經基本能夠理解ScrollView對手勢操作的處理原理,這里來統一歸納一下。我們直接上圖最清晰:
解釋下上圖:
- Apple使用了一個延遲機制來判斷在一個ScrollView內是否產生有效的滑動手勢
,而這個延遲機制由開關delaysContentTouches
來控制,默認為YES - 如果延遲打開且檢測到有效Scroll,則將會直接發送滑動操作到ScrollView,并停止向SubView發送任何tracking;
- 如果延遲未打開或者打開但是沒有檢測到有效的動作,則會看
touchesShouldBegin:withEvent:inContentView:
的返回:NO則立即返回給ScrollView,否則會將touch事件發送給SubView。默認為YES。
當touch事件已經發送給SubView之后,如果用戶繼續產生touch事件(做出Scroll動作),則: - 檢查
canCancelContentTouches
開關(默認為YES)。如果為N,則將所有后續事件發送到SubView,否則: - 檢查
touchesShouldCancelInContentView
的返回值,如果為N,則將所有事件發送到SubView,否則返回到ScrollView。默認情況下,如果SubView不是UIControl的一種,則返回YES。
-
一個簡單的總結:
針對1,2: 默認情況下,只要用戶迅速做出滑動手勢,都將觸發ScrollView滑動;
針對3:默認情況下,點擊操作都可以傳入到SubView
針對4,5:默認情況下,SubView中的Button, UISlider, UISwitch等等都可以直接響應你的Touch,滑動事件;但是UIView之類則無法響應復雜事件(Multi-Touch)
-
想要讓SubView響應手勢操作 ?
最簡單的:
- 關閉
delaysContentTouches
- 關閉
canCancelContentTouches
如果想要再多一些自定義,比如有些地方響應,有些地方不響應,則可以:
在touchesShouldBegin:withEvent:inContentView:
中設定響應條件,或者:
打開canCancelContentTouches
,在touchesShouldCancelInContentView:
中設定響應條件。
-
Sample Demo
說這么多還是沒懂?再來一發Demo,直接看代碼最清晰:
這個Demo中,首先自定義一個ScrollView叫做“MyScollView”,在MyScollView上,有2個SubView:綠色的greenView和黃色的yellowView,兩個View都添加的左右滑動的手勢操作,但是在touchesShouldBegin:withEvent:inContentView:
方法中,當檢測到當前view為greenView時,返回NO。
另外,有2個Switch開關,分別操作delaysContentTouches
和 canCancelContentTouches
。
我們可以看一下效果:
1) 初始狀態下,所有開關打開,UIButton正常工作,yellowView不能響應手勢;
a) 觸動yellowView屏幕后馬上滑動,則UISilder不能正常工作,ScrollView滑動;
b) 觸動yellowView屏幕后等待一小會再滑動,則UISilder正常工作;
2) 關閉canCancelContentTouches
:
a) 觸動yellowView屏幕后馬上滑動,則ScrollView滑動;
b) 觸動yellowView屏幕后等待一小會再滑動,則UISilder, yellowView響應手勢;
3)關閉delaysContentTouches
,無論怎樣,UISilder,yellowView都會響應手勢;
4)以上任何情況,greenView始終不會響應手勢
希望你喜歡,歡迎大家討論。
2016.6.14 完稿于南京