值得注意的事,當一個view上面有多個手勢時,touch是無序的
1事件是怎么樣產生與傳遞的?(由上至下的過程)
當發生一個觸摸事件后,系統會將該事件加入到一個由UIApplication管理的事件隊列中.
UIApplication會從事件隊列中取出最前面的事件,并將事件分發下去以便處理.
主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件
觸摸事件的傳遞是從父控件傳遞到子控件的.
如果一個父控件不能接收事件,那么它里面的了子控件也不能夠接收事件.
如何尋找最合適的View?
主窗口會在視圖層次結構中找到一個最合適的視圖來處理觸摸事件.
那如何找到最合適的View呢?
1.先判斷自己是否能夠接收觸摸事件,如果能再繼續往下判斷,
2.再判斷觸摸的當前點在不在自己的身上.
3.如果在自己身上,它會從后往前遍歷子控件,遍歷出每一個子控件后,重復前面的兩個步驟.
4.如果沒有符合條件的子控件,那么它自己就是最適合的View.
2-事件響應(由下至上)
用戶點擊屏幕后產生的一個觸摸事件,經過一系列的傳遞過程后,會找到最合適的視圖控件來處理這個事件,
找到最合適的視圖控件后,就會調用控件的touches方法來作具體的事件處理
那這些touches方法的默認做法是將事件順著響應者鏈條向上傳遞,將事件交給上一個響應者進行處理
什么是響應者鏈條?
是由多個響應者對象連接起來的鏈條.
什么是響應者對象?
繼承了UIResponder對象我們稱之為響應者對象,也就是能處理事件的對象
事件傳遞的完整過程?
在產生一個事件時,系統會將該事件加入到一個由UIApplication管理的事件隊列中,
UIApplication會從事件隊列中取出最前面的事件,將它傳遞給先發送事件給應用程序的主窗口.
主窗口會調用hitTest方法尋找最適合的視圖控件,找到后就會調用視圖控件的touches方法來做具體的事情.
當調用touches方法,它的默認做法, 就會將事件順著響應者鏈條往上傳遞,
傳遞給上一個響應者,接著就會調用上一個響應者的touches方法
如何去尋找上一個響應者?
1.如果當前的View是控制器的View,那么控制器就是上一個響應者.
2.如果當前的View不是控制器的View,那么它的父控件就是上一個響應者.
3.在視圖層次結構的最頂級視圖,如果也不能處理收到的事件或消息,則其將事件或消息傳遞給window對象進行處理
4.如果window對象也不處理,則其將事件或消息傳遞給UIApplication對象
5.如果UIApplication也不能處理該事件或消息,則將其丟棄
3事件的產生與傳遞(由上至下)? ? 和? ? 事件響應(由下至上)
(通俗講: 方便大家理解,并無其他意思,勿噴.)
第一步? 事件的傳遞與傳遞 ? (找到哪里發生地點)
一個地方發生了大型搶劫時間.
國家知道了有這個事件,就先省份上找,并按照省份名單順序, 國家問浙江: 浙江浙江,是你那邊的事嗎?? 浙江回答:不是.? ? 再問福建:
福建,是你那邊的事嗎?? 福建回答:是的.? 然后福建在問它下面的城市,以此類推,一直找到 最終的地方.
第二步 事件響應 (把事件交給合適的部門處理)
發生的地點把 這個事件, 交給了這個地點的部門,?? 地點的部門說對不起,我處理不了.??
地點部門交給了? 市部門,市部門收到事件,判斷能否處理(能處理則不傳遞上去,不能則傳給上級部門),以此類推,一直傳到國家(如果連國家都不能處理則 丟棄這個事情)
補充
處理部門判斷能否處理? 的標準是? 1手勢的響應2繼承于UIControl都會阻斷 響應者鏈條的往上傳遞(button繼承于UIControl)3繼承于UIScroll也會阻斷往上傳遞
即總的來說:能響應事件的對象通常默認取消事件往上傳遞(都有部門接收處理這個事件了,當然不用往上傳了,哪怕像button scroll這種流氓部門 不管有對這個事件只接收不處理,它都打斷了往上傳遞)
3-hitTest方法與PointInside方法
個人通俗理解
1順序:先執行hitTest 再PointInside?
2 hitTest作用:尋找個合適的View,先遍歷subView尋找,找到則返回view,找不到返回self
PointInside判斷點擊的點 是否在這個View上
作用:尋找最適合的View
參數:當前手指所在的點.產生的事件
返回值:返回誰, 誰就是最適合的View.
只要一個事件,傳遞給一個控件時, 就會調用這個控件的hitTest方法
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
作用:判斷point在不在方法調用者上
point:必須是方法調用者的坐標系
hitTest方法底層會調用這個方法,判斷點在不在控件上.
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event{
return YES;
}
hitTest底層實現:
1.判斷當前能不能接收事件
if(self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01)
return? nil;
2.判斷觸摸點在不在當前的控件上
if(![self pointInside:point withEvent:event]) return nil;
3.從后往前遍歷自己的子控件
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0;i-- ) {
UIView *childV = self.subviews[i];
把當前坐標系上的點轉換成子控件坐標系上的點.
CGPoint childP = [self convertPoint:point toView:childV];
判斷自己的子控件是不是最適合的View
UIView *fitView = [childV hitTest:childP withEvent:event];
如果子控件是最合適的View,直接返回
if (fitView) {
return? fitView;
}
}
(下圖代碼可以看出 hitTest里面 執行pointInSide)
4.自己就是最適合的View
return self.
4一個控件什么情況下不能夠接收事件.
1.不接收用戶交互時不能夠處理事件
userInteractionEnabled = NO
2.當一個控件隱藏的時候不能夠接收事件
Hidden = YES的時候
3.當一個控件為透明白時候也不能夠接收事件
注意:UIImageView的userInteractionEnabled默認就是NO,
因此UIImageView以及它的子控件默認是不能接收觸摸事件的
5響應鏈的實際運用
1放大button的作用域(超過它的frame仍能響應)
2點擊的view,卻讓其他view響應
6-hitTest練習1
業務邏輯:
底部一個按鈕, 按鈕的上面有一個View,遮擋在按鈕的上面.
點擊View時, View接收事件,當發現點擊的點在按鈕的位置時, 讓底部的按鈕處理事件.
實現思路:
實現View的touchBegain方法,先堅聽UIView的點擊.
并去實現UIView的HitTest方法, 在hitTest方法當中通過把當前點轉換成按鈕所在的坐標系
CGPoint btnP = [self convertPoint:point toView:self.btn];
轉換過后查看當前點在不在按鈕上,如果在按鈕上,就直接返回按鈕.
如果有在按鈕上,保持系統默認做法.
實現代碼:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
判斷當前點在不在按鈕上.
把當前點轉換成按鈕所在的坐標系
CGPoint btnP = [self convertPoint:point toView:self.btn];
if ([self.btn pointInside:btnP withEvent:event]) {
return self.btn;
}else{
return [super hitTest:point withEvent:event];
}
}
7-hitTest方法練習2
業務邏輯:
按鈕可以隨著手指拖動而拖動.拖動過程當中,按鈕當中的子控件也跟著拖動.
讓超過按鈕的子控件也能夠響應事件,一般情況下,當一個控件超過他的父控件的時候,是不能夠接收事件的.
現在要做的事情就讓超過父控件的按鈕也能夠響應事件.
實現思路:
先辦到讓按鈕能夠跟隨著手指移動而移動.
實現按鈕的touchesMoved方法,在touchesMoved方法當中,獲得當前手指所在的點.以前上一個點.
分別計算X軸的偏移量以及Y軸的偏移量.
然后修改當前按鈕的transform讓按鈕辦到能夠跟隨著手指移動而移動.
第二步, 實現按鈕的hitTest方法.
在該方法當中去判斷當前的點在不在按鈕的子控件上.
如果在按鈕的子控件上.就返回按鈕的子控件如果不在的話, 就保持系統的默認做法.
實現代碼:
第一步,讓按鈕能夠跟隨著手指移動而移動
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
獲取當前的手指
UITouch *touch = [touches anyObject];
獲取當前手指所在的點
CGPoint curP = [touch locationInView:self];
獲取當前手指的上一個點
CGPoint preP = [touch previousLocationInView:self];
計算X軸的偏移量
CGFloat offsetX = curP.x - preP.x;
計算Y軸的偏移量
CGFloat offsetY = curP.y - preP.y;
修改按鈕的形變,讓按鈕能夠移動.
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}
第二步,實現hitTest方法,判斷手指當前所在的點在不在按鈕的子控件上.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
把當前所在的點轉換成按鈕子控件上面的點
CGPoint chatP =? [self convertPoint:point toView:self.chatBtn];
判斷轉換后的點在不在按鈕的控件上.
if ([self.chatBtn pointInside:chatP withEvent:event]) {如果在
直接返回,也就意味著,當前最適合的View,就是這個按鈕
return self.chatBtn;
}else{如果不在,那么就保持系統原有做法.
return? [super hitTest:point withEvent:event];
}
}
相關知識擴充
關于手勢
值得注意的是:手勢識別器 是 先于touch方法(touch響應鏈!)?? 捕捉到touch object!!!
cancelsTouchesInView/delaysTouchesBegan/delaysTouchesEnded
(0)首先要知道的是
1.這3個屬性是作用于GestureRecognizers(手勢識別)與觸摸事件之間聯系的屬性。實際應用中好像很少會把它們放到一起,大多都只是運用手勢識別,所以這3個屬性應該很少會用到。
2.對于觸摸事件,window只會有一個控件來接收touch。這個控件是首先接觸到touch的并且重寫了觸摸事件方法(一個即可)的控件
3.手勢識別和觸摸事件是兩個獨立的事,只是可以通過這3個屬性互相影響,不要混淆。
4手勢是view外部來添加? ,? touch是view內部處理,兩個是分開,且手勢優先級比touch高
(1)在默認情況下(即這3個屬性都處于默認值的情況下)(這些屬性是 手勢對它自己的view!!!)
如果觸摸window,首先由window上最先符合條件的控件(該控件記為hit-test
view)接收到該touch并觸發觸摸事件touchesBegan。同時如果某個控件的手勢識別器接收到了該touch,就會進行識別。手勢識別成功之后發送觸摸事件touchesCancelled給hit-testview,hit-test
view不再響應touch。(即打斷 往上傳遞的響應鏈條)
(2)cancelsTouchesInView:(默認yes)
默認為YES,這種情況下當手勢識別器識別到touch之后,會發送touchesCancelled給hit-testview以取消hit-test view對touch的響應,這個時候只有手勢識別器響應touch。
當設置成NO時,手勢識別器識別到touch之后不會發送touchesCancelled給hit-test,這個時候手勢識別器和hit-test view均響應touch。
(3)delaysTouchesBegan:(默認no)
默認是NO,這種情況下當發生一個touch時,手勢識別器先捕捉到到touch,然后發給hit-testview,兩者各自做出響應。
如果設置為YES,手勢識別器在識別的過程中(注意是識別過程),不會將touch發給hit-test
view,即hit-testview不會有任何觸摸事件。!!只有在識別失敗之后才會將touch發給hit-testview,這種情況下hit-test
view的響應會延遲約0.15ms。
(4)delaysTouchesEnded:(默認yes)
默認為YES。這種情況下發生一個touch時,在手勢識別成功后,發送給touchesCancelled消息給hit-testview,手勢識別失敗時,會延遲大概0.15ms,期間沒有接收到別的touch才會發送touchesEnded。如果設置為NO,則不會延遲,即會立即發送touchesEnded以結束當前觸摸。
(5)手勢共存 與 排斥(以下是手勢對手勢!!)
1:[tapGesture? requireGestureRecognizerToFail:swipeGesture]
swipe判斷失敗后? 才判斷tap
2:(代理方法)-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
這里返回YES,代表跟別的手勢共存;如果返回NO,不一定代表不共存(可能另一個手勢返回yes就可以共存,只要兩個手勢任一返回yes就可以)
3:-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
另外一個手勢識別fail的時候,才會識別自己
4-
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer
*)otherGestureRecognizer
我被另外一個手勢變成Fail
(6)button 是用這個方法發送時間(補充)
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
關于scrollView
有很多新聞類的App頂部都有一個滑動菜單欄,主要模型可能是由一個UIScrollView包含多個UIButton控件組成;當你操作的時候,手指如果是很迅速的在上面劃過,會發現即使手指觸摸的地方有UIButton,但是并沒有觸發該UIButton的任何觸摸事件,這就是上面提到的case1;當你手指是緩慢劃過或根本就沒動,才會觸發UIButton的觸摸事件,這是case2的情況。
(0):scrollView默認封裝了兩個手勢 pan和pinch
(1):scrollView的touch事件傳遞的兩個重要的屬性delaysContentTouches和canCancelContentTouches
delaysContentTouches(默認yes)(這個屬性也是上面說scrollView默認阻斷 響應鏈的原因)
delay延遲對subView的touch響應,肯定會優先響應UIScrollview滑動事件
delaysContentTouches。默認值為YES;如果設置為NO,則無論手指移動的多么快,始終都會將觸摸事件傳遞給內部控件;設置為NO可能會影響到UIScrollView的滾動功能。
canCancelContentTouches(默認yes)
注意:如果scrollView(內部)- (BOOL)touchesShouldCancelInContentView:(UIView *)view
也設置,那兩個要配合,平時就不要寫這個方法
如果屬性值為YES并且跟蹤到手指正觸摸到一個內容控件,這時如果用戶拖動手指的距離足夠產生滾動,那么內容控件將收到一個touchesCancelled:withEvent:消息,而scroll
view將這次觸摸作為滾動來處理。如果值為NO,一旦content
view開始跟蹤(tracking==YES),則無論手指是否移動,scrollView都不會滾動。
簡單通俗點說,如果為YES,就會等待用戶下一步動作,如果用戶移動手指到一定距離,就會把這個操作作為滾動來處理并開始滾動,同時發送一個touchesCancelled:withEvent:消息給內容控件,由控件自行處理。如果為NO,就不會等待用戶下一步動作,并始終不會觸發scrollView的滾動了。
關于control
簡單區分UIResponder與UIControl
UIResponder類:上承NSObject,下接UIView ,UIVIewController ,UIApplacation;響應點,壓,滑;
UIControl類:上承UIView,下接UIButton等開關按鈕;
主要區別在于:
前者,主要是響應某個動作,執行某個行為--
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event;
后者,在繼承了前者的屬性基礎上,還能夠相應某個動作,為某個對象,添加動作--
- (void) addTarget:(id)target action:(SEL)action forControlEvents(UIControlEvents)controlEvents
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
返回值:YES 接受用戶通過addTarget:action:forControlEvents添加的事件繼續處理。
返回值:NO??則屏蔽用戶添加的任何事件
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event可以用這個追蹤 button上touch的改變? (有點類似于viewControl里面的touchMoved)
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 如果用戶重寫了該方法,則不會執行由用戶添加的其他事件,直接屏蔽了用戶的事件
分派事件
使用下面兩個方法分派事件給響應者處理:
- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
-
(void)sendActionsForControlEvents:(UIControlEvents)controlEvents;
// send all actions associated with events
事件與操作的區別:事件報告對屏幕的觸摸;操作報告對控件的操縱。