1. iOS事件有哪一些
運(yùn)動(dòng)事件
- 傳感器、計(jì)數(shù)器、陀螺儀
遠(yuǎn)程控制事件
- 線控耳機(jī)
觸摸事件
- 本文核心分析
2. 事件傳遞和響應(yīng)
2.1 原理分析
- 當(dāng)我們手指觸碰到屏幕的時(shí)候,事件傳遞和響應(yīng)的流程是怎么樣的呢
- 事件的流程圖

流程.png
- IOKit.framework 為系統(tǒng)內(nèi)核的庫(kù)
- SpringBoard.app 相當(dāng)于手機(jī)的桌面
- Source1 主要接收系統(tǒng)的消息
- Source0 - UIApplication - UIWindow
- 從UIWindow 開(kāi)始步驟,見(jiàn)下圖

window流程.png
- 比如我們?cè)趕elf.view 上依次添加view1、view2、view3(3個(gè)view是同級(jí)關(guān)系),那么系統(tǒng)用
hitTest以及pointInside時(shí)會(huì)先從view3開(kāi)始便利,如果pointInside返回YES就繼續(xù)遍歷view3的subviews(如果view3沒(méi)有子視圖,那么會(huì)返回view3),如果pointInside返回NO就開(kāi)始便利view2。 - 反序遍歷,最后一個(gè)添加的subview開(kāi)始。也算是一種算法優(yōu)化
2.2 HitTest 、pointInside
- 上一段層級(jí)關(guān)系的簡(jiǎn)單示例代碼
EOCLightGrayView *grayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(50.f, 100.f, 260.f, 200.f)];
redView = [[EOCRedView alloc] initWithFrame:CGRectMake(0.f, 0.f, 120.f, 100.f)];
EOCBlueView *blueView = [[EOCBlueView alloc] initWithFrame:CGRectMake(140.f, 100.f, 100.f, 100.f)];
EOCYellowView *yellowView = [[EOCYellowView alloc] initWithFrame:CGRectMake(50.f, 360.f, 200.f, 200.f)];
[self.view addSubview:grayView];
[grayView addSubview:redView];
[grayView addSubview:blueView];
[self.view addSubview:yellowView];

布局.png

打印.png
- 點(diǎn)擊
red,由于yellow與grey同級(jí),yellow比grey后添加,所以先打印yellow,由于觸摸點(diǎn)不在yellow內(nèi),打印grey,然后遍歷grey,打印他的兩個(gè)subviews - 通過(guò)在
HitTest返回nil,pointInside并沒(méi)有執(zhí)行,我們可以得知,pointInside調(diào)用順序你在HitTest之后的。 -
pointInside的 參數(shù):(CGPoint)poinit的值是以自身為坐標(biāo)系的,判斷點(diǎn)是否view內(nèi)的范圍是以view自身的bounds為范圍,而非frame - 如果在
grey的hitTest返回[super hitTest:point event:event],則會(huì)執(zhí)行gery.subviews的遍歷(subviews 的 hitTest 與 pointInside),grey的pointInside是判斷觸摸點(diǎn)是否在grey的bounds內(nèi)(不準(zhǔn)確),grey的hitTest是判斷是否需要遍歷他的subviews. -
pointInside只是在執(zhí)行hitTest時(shí),會(huì)在hitTest內(nèi)部調(diào)用的一個(gè)方法 -
pointInside只是輔助hitTest的關(guān)系 -
hitTest是一個(gè)遞歸函數(shù)
2.3 hitTest 內(nèi)部實(shí)現(xiàn)代碼還原
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
///hitTest:判斷pointInside,是不是在view里?是的話(huà),遍歷,不是的話(huà)返回nil;假設(shè)我就是點(diǎn)擊灰色的,返回的是自己;
NSLog(@"%s",__func__);
NSArray *subViews = [[self.subviews reverseObjectEnumerator] allObjects];
UIView *tmpView = nil;
for (UIView *view in subViews) {
CGPoint convertedPoint = [self convertPoint:point toView:view];
if ([view pointInside:convertedPoint withEvent:event]) {
tmpView = view;
break;
}
}
if (tmpView) {
return tmpView;
} else if([self pointInside:point withEvent:event]) {
return self;
} else {
return nil;
}
return [self hitTest:point event:event]; //redView
///這里是hitTest的邏輯
///alpha(<=0.01)、userInterActionEnabled(NO)、hidden(YES) pointInside返回的為NO
}
2.4 實(shí)戰(zhàn)之?dāng)U大button點(diǎn)擊區(qū)域

bigbutton.png
- 紅色為button
- 藍(lán)色為放大后的目標(biāo)點(diǎn)擊區(qū)域
- 稍微注意是在bounds的基礎(chǔ)上修改
- button 內(nèi)部的
hitTest通過(guò)pointInside的確認(rèn),來(lái)決定是否返回自己
@implementation EOCCustomButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSLog(@"%s", __func__);
//擴(kuò)大它的響應(yīng)范圍
CGRect frame = [self getScaleFrame];
return CGRectContainsPoint(frame, point);
// return [super pointInside:point withEvent:event];
}
- (CGRect)getScaleFrame {
CGRect rect = self.bounds;
if (rect.size.width < 40.f) {
rect.origin.x -= (40-rect.size.width)/2;
}
if (rect.size.height < 40.f) {
rect.origin.y -= (40-rect.size.height)/2;
}
rect.size.width = 40.f;
rect.size.height = 40.f;
return rect;
}
2.5 UIRespond 與 響應(yīng)鏈的組成
- 當(dāng)我們通過(guò)
hitTest找到視圖后,我們產(chǎn)生的touch事件,他是怎么一層層響應(yīng)的?

respondch.png
- 響應(yīng)鏈?zhǔn)峭ㄟ^(guò)
nextResponder屬性組成的一個(gè)鏈表 - 點(diǎn)擊的 view 有 superView ,nextResponder 就是 superView ;
-
view.nextResponder .nextResponder是viewController 或者是 view.superView. view -
view.nextResponder .nextResponder. nextResponder是 UIWindow (非嚴(yán)謹(jǐn),便于理解) -
view.nextResponder .nextResponder. nextResponder. nextResponder是UIApplication 、UIAppdelate、直到nil (非嚴(yán)謹(jǐn),便于理解) -
touch事件就是根據(jù)響應(yīng)鏈的關(guān)系來(lái)層層調(diào)用(我們重寫(xiě)touch 要記得super調(diào)用,不然響應(yīng)鏈會(huì)中斷) - 比如我們監(jiān)聽(tīng)
self.view的touch事件,也是因?yàn)?code>subviews的touch都在同一個(gè)響應(yīng)鏈里
3. 手勢(shì)事件
3.1 手勢(shì) 與 hitTest 的關(guān)系
- 相同上面的學(xué)習(xí)我們可以推測(cè)出,手勢(shì)的響應(yīng)也得必須經(jīng)過(guò)
hitTest先找到視圖才能觸發(fā)(已驗(yàn)證)
3.2 手勢(shì)與 觸摸事件的關(guān)系
-
touch事件是UIView內(nèi)部的東西,而手勢(shì)疊加上去的觸摸事件 -
subview會(huì)響應(yīng)superview的手勢(shì), 但是同級(jí)的subview不會(huì)響應(yīng)
3.3 系統(tǒng)如何分辨手勢(shì)種類(lèi)
- 首先我們想在手勢(shì)中調(diào)用
touches方法必須要導(dǎo)入
#import <UIKit/UIGestureRecognizerSubclass.h>
因?yàn)?code>gesture繼承的是NSObject而不是UIRespond - 通過(guò)嘗試不調(diào)用 tap手勢(shì) 的
touchesBegan,發(fā)現(xiàn)tap手勢(shì)無(wú)法響應(yīng) - 通過(guò)嘗試調(diào)用
touchesBegan,但是不調(diào)用 pan手勢(shì) 的touchesMoved,發(fā)現(xiàn)pan手勢(shì)無(wú)法響應(yīng) - 我們通過(guò)
UITouch的實(shí)例,可以看到里面有很多屬性,比如點(diǎn)擊的次數(shù),上次的位置等,結(jié)合這個(gè)屬性系統(tǒng)與touches方法就可以判斷出你使用的是什么手勢(shì)
3.4 手勢(shì)與view的touches事件的關(guān)系
- 首先通過(guò)觸摸事件,先響應(yīng)
touchesBegan以及touchesMoved,直到手勢(shì)被識(shí)別出來(lái),調(diào)用touchesCancelled,全權(quán)交給手勢(shì)處理。 - 但是我們可以改變這種關(guān)系
下面是系統(tǒng)的默認(rèn)設(shè)置
tapGesture.delaysTouchesBegan = NO;
///是否延遲view的touch事件識(shí)別;如果延遲了(YES),并且手勢(shì)也識(shí)別到了,touch事件會(huì)被拋棄
tapGesture.cancelsTouchesInView = YES;
///識(shí)別手勢(shì)之后,是否取消view的touch事件
// 如果為NO, touchesCancelled 不會(huì)調(diào)用,取而代之的是手勢(shì)結(jié)束后touchesEnd
4. button事件
4.1 系統(tǒng)是如何分辨UIControlEvent
- 我們還是通過(guò)
button內(nèi)部的touches來(lái)實(shí)踐 - 實(shí)踐過(guò)程略,與手勢(shì)同理
- 比如說(shuō)
touchUpInside,通過(guò)查看堆棧調(diào)用,我們發(fā)現(xiàn)在touchesEnd后完成對(duì)touchUpInside的識(shí)別,然后再調(diào)起sendAction:方法
5. 觸摸事件的運(yùn)用
- 上文已經(jīng)講過(guò)一個(gè)button點(diǎn)擊范圍擴(kuò)大的案例,再講一個(gè)案例

case2.png
- 與上個(gè)例子不同的是,當(dāng)我們點(diǎn)擊
黑色的時(shí)候,因?yàn)樵?code>greyview的外面,別說(shuō)響應(yīng)黑色button了,我們直接不會(huì)響應(yīng)greyview了,怎么辦? - 一種是在
self.view的-pointInside返回 YES, 不過(guò)這種在交互復(fù)雜的場(chǎng)景不存在實(shí)用性 - 我們可以重寫(xiě)
self.view的-hitTest, 把當(dāng)前觸摸的點(diǎn)分別轉(zhuǎn)化為subviews上的坐標(biāo)系的點(diǎn),在用subviews的pointinside判斷此點(diǎn),然后返回對(duì)應(yīng)的subviews