在實(shí)現(xiàn)上一篇介紹的自定義滑動(dòng)關(guān)聯(lián)菜單控件BFScrollMenu時(shí),關(guān)于滑動(dòng)方向判斷的邏輯其實(shí)一開始是準(zhǔn)備用手勢(shì)操作來實(shí)現(xiàn)的,結(jié)果發(fā)現(xiàn)在ScrollView中處理手勢(shì)的邏輯比較困難,在寫的時(shí)候還沒有仔細(xì)研究過ScrollView的滑動(dòng)原理,只是知道需要自定義一個(gè)ScrollView才能實(shí)現(xiàn),但是我的本意是不希望用戶還要顯示指定一個(gè)自定義的ScrollView而是直接用category進(jìn)行無縫的對(duì)接,所以就改用didScroll delegate 用offset來計(jì)算滑動(dòng)邏輯了,效果可以接受。
回過頭來花了些時(shí)間仔細(xì)研究了下UIScrollView的滑動(dòng)處理邏輯,這樣就可以采用Customer ScrollView來實(shí)現(xiàn)同樣的BFScrollMenu邏輯了。
-
UIScrollView的滑動(dòng)處理原理
網(wǎng)上相關(guān)的文章有很多,但是我感覺沒有一個(gè)能清晰的解釋清楚。這里我用流程圖的方式,把我自己經(jīng)過試驗(yàn)后的結(jié)論和理解和大家分享一下,希望能幫助到你。如果有不正確的歡迎指正。
首先我們來看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個(gè)可以set的property和2個(gè)可以重載的方法:
// 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;
有了以上這些,已經(jīng)基本能夠理解ScrollView對(duì)手勢(shì)操作的處理原理,這里來統(tǒng)一歸納一下。我們直接上圖最清晰:

解釋下上圖:
- Apple使用了一個(gè)延遲機(jī)制來判斷在一個(gè)ScrollView內(nèi)是否產(chǎn)生有效的滑動(dòng)手勢(shì)
,而這個(gè)延遲機(jī)制由開關(guān)delaysContentTouches來控制,默認(rèn)為YES - 如果延遲打開且檢測(cè)到有效Scroll,則將會(huì)直接發(fā)送滑動(dòng)操作到ScrollView,并停止向SubView發(fā)送任何tracking;
- 如果延遲未打開或者打開但是沒有檢測(cè)到有效的動(dòng)作,則會(huì)看
touchesShouldBegin:withEvent:inContentView:的返回:NO則立即返回給ScrollView,否則會(huì)將touch事件發(fā)送給SubView。默認(rèn)為YES。
當(dāng)touch事件已經(jīng)發(fā)送給SubView之后,如果用戶繼續(xù)產(chǎn)生touch事件(做出Scroll動(dòng)作),則: - 檢查
canCancelContentTouches開關(guān)(默認(rèn)為YES)。如果為N,則將所有后續(xù)事件發(fā)送到SubView,否則: - 檢查
touchesShouldCancelInContentView的返回值,如果為N,則將所有事件發(fā)送到SubView,否則返回到ScrollView。默認(rèn)情況下,如果SubView不是UIControl的一種,則返回YES。
-
一個(gè)簡單的總結(jié):
針對(duì)1,2: 默認(rèn)情況下,只要用戶迅速做出滑動(dòng)手勢(shì),都將觸發(fā)ScrollView滑動(dòng);
針對(duì)3:默認(rèn)情況下,點(diǎn)擊操作都可以傳入到SubView
針對(duì)4,5:默認(rèn)情況下,SubView中的Button, UISlider, UISwitch等等都可以直接響應(yīng)你的Touch,滑動(dòng)事件;但是UIView之類則無法響應(yīng)復(fù)雜事件(Multi-Touch)
-
想要讓SubView響應(yīng)手勢(shì)操作 ?
最簡單的:
- 關(guān)閉
delaysContentTouches - 關(guān)閉
canCancelContentTouches
如果想要再多一些自定義,比如有些地方響應(yīng),有些地方不響應(yīng),則可以:
在touchesShouldBegin:withEvent:inContentView:中設(shè)定響應(yīng)條件,或者:
打開canCancelContentTouches,在touchesShouldCancelInContentView:中設(shè)定響應(yīng)條件。
-
Sample Demo
說這么多還是沒懂?再來一發(fā)Demo,直接看代碼最清晰:
這個(gè)Demo中,首先自定義一個(gè)ScrollView叫做“MyScollView”,在MyScollView上,有2個(gè)SubView:綠色的greenView和黃色的yellowView,兩個(gè)View都添加的左右滑動(dòng)的手勢(shì)操作,但是在touchesShouldBegin:withEvent:inContentView:方法中,當(dāng)檢測(cè)到當(dāng)前view為greenView時(shí),返回NO。
另外,有2個(gè)Switch開關(guān),分別操作delaysContentTouches 和 canCancelContentTouches。
我們可以看一下效果:
1) 初始狀態(tài)下,所有開關(guān)打開,UIButton正常工作,yellowView不能響應(yīng)手勢(shì);
a) 觸動(dòng)yellowView屏幕后馬上滑動(dòng),則UISilder不能正常工作,ScrollView滑動(dòng);
b) 觸動(dòng)yellowView屏幕后等待一小會(huì)再滑動(dòng),則UISilder正常工作;

2) 關(guān)閉canCancelContentTouches:
a) 觸動(dòng)yellowView屏幕后馬上滑動(dòng),則ScrollView滑動(dòng);
b) 觸動(dòng)yellowView屏幕后等待一小會(huì)再滑動(dòng),則UISilder, yellowView響應(yīng)手勢(shì);

3)關(guān)閉delaysContentTouches,無論怎樣,UISilder,yellowView都會(huì)響應(yīng)手勢(shì);

4)以上任何情況,greenView始終不會(huì)響應(yīng)手勢(shì)
希望你喜歡,歡迎大家討論。
2016.6.14 完稿于南京