iOS事件處理-
用戶使用App產(chǎn)生的事件及響應(yīng)方法:
iOS中不是任何對象都能處理事件,只有繼承UIResponder的對象才能接受并處理事件--稱為響應(yīng)者對象;例如:UIApplication,UIView,UIViewController.
1.觸摸事件-touch
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;//被動取消,可能會經(jīng)歷.
2.加速計事件(搖一搖)
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
3遠(yuǎn)程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
1. 觸摸事件:
1.1 簡介:
以觸摸事件為例:當(dāng)一根手指或多根觸摸屏幕,就會創(chuàng)建一個與之相關(guān)的UITouch對象,存在NSSet集合中.用touches anyObjiect即可獲取.
如果兩根手指同時觸摸一個view,那么view只會調(diào)用一次touchesBegan:withEvent:方法,touches參數(shù)中裝著2個UITouch對象
如果這兩根手指一前一后分開觸摸同一個view,那么view會分別調(diào)用2次 touchesBegan:withEvent:方法,并且每次調(diào)用時的touches參數(shù)中只包含一個UITouch對象
根據(jù)touches中UITouch的個數(shù)可以判斷出是單點(diǎn)觸摸還是多點(diǎn)觸摸
一次完整的觸摸過程中,只會產(chǎn)生一個事件對象,4個觸摸方法都是同一個event參數(shù)
touch作用:
- 保存跟相關(guān)信息:觸摸位置,事件,階段;
- 當(dāng)手指移動時,系統(tǒng)會更新同一個UITouch對象,使之能夠一直保存當(dāng)前位置.
- 手指離開屏幕,系統(tǒng)才會銷毀相應(yīng)的UItouch對象--所以,要避免使用雙擊事件.
UITouch屬性:
- @property(nonatomic,readonly,retain) UIWindow *window;//觸摸時所處窗口
- @property(nonatomic,readonly,retain) UIView *view;//觸摸時所處實(shí)處
- @property(nonatomic,readonly) NSUInteger tapCount;//短時間內(nèi)點(diǎn)擊屏幕次數(shù).
- @property(nonatomic,readonly) NSTimeInterval timestamp;//記錄觸摸產(chǎn)生或變化時的事件 /秒
- @property(nonatomic,readonly) UITouchPhase phase;//觸摸事件所處狀態(tài);根據(jù)此屬性,判斷當(dāng)前調(diào)用哪個方法.
UITouch方法:
-(CGPoint)locationInView:(UIView *) view; //返回當(dāng)前觸摸點(diǎn),以view坐標(biāo)系為準(zhǔn),View參數(shù)為nil的時候,默認(rèn)為UIWindow上.
- (CGPoint)previousLocationInView:view //記錄上一個觸摸點(diǎn)位置
UIEvent:
每產(chǎn)生一個事件,就會產(chǎn)生一個UIEvent對象,記錄事件產(chǎn)生的時間和類型.
屬性:
@property(nonatomic,readonly) UIEventType type;//類型
@property(nonatomic,readonly) UIEventSubtype subtype;//遠(yuǎn)程控制事件的多種情況.
@property(nonatomic,readonly) NSTimeInterval timestamp; //時間
? UIEvent還提供了相應(yīng)的方法可以獲得在某個view上面的觸摸對象(UITouch)
- (nullable NSSet <UITouch *> *)allTouches;
1.2 一般使用:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
//1. 取得一個觸摸對象(對于多點(diǎn)觸摸可能有多個對象)
UITouch *touch=[touches anyObject];
//NSLog(@"%@",touch);
//2. 取得當(dāng)前位置
CGPoint current=[touch locationInView:self.view];
//取得前一個位置
//CGPoint previous=[touch previousLocationInView:self.view];
//移動前的中點(diǎn)位置
CGPoint center=_image.center;
//移動偏移量
CGPoint offset=CGPointMake(current.x-previous.x, current.y-previous.y);
//3. 重新設(shè)置新位置
_image.center=CGPointMake(center.x+offset.x, center.y+offset.y);
2. 事件的傳遞和響應(yīng):
2.1 事件的傳遞
事件傳遞 過程;
發(fā)生觸摸后,事件會加入到UIApplication事件隊列---->UIApplication會從事件隊列取出最前面的事件,分發(fā)下去處理,通常發(fā)送到App的主窗口(keyWindow);---->主窗口會調(diào)用hitTest:withEvent:方法在視圖層級中找一個最合適的視圖來處理觸摸事件;
視圖層級事件傳遞的 邏輯:
- 自己是否能接受觸摸事件---->不能,事件傳遞結(jié)束.
- 觸摸點(diǎn)是否在自己身上------>不在,事件傳遞結(jié)束.
- 從后往前(同級最上方的開始)遍歷子控件-重復(fù)1和2的判斷.只要有通過的,就繼續(xù)遍歷通過控件的子控件.如果沒有符合條件的子控件,那么自己最合適處理.
不能接受觸摸的三種情況:
- userInteractionEnabled = NO,
- hidden = YES,
- alpha < 0.01;
視圖層級事件傳遞的 方法:
系統(tǒng)會調(diào)用:
hitTest: withEvent:
作用:需找最合適的View, return 這個View.
調(diào)用時候:當(dāng)事件傳遞給控件的時候系統(tǒng)調(diào)用
-(UIView*)hitTest: WithEvent {
//1.判斷當(dāng)前控件能不能接受事件:
if(上面三個條件)
//2.判斷這個點(diǎn)在不在當(dāng)前控件上
pointInside: withEvent:
//3.從后往前遍歷子控件
for i - -;取出子控件
把當(dāng)前坐標(biāo)系上的點(diǎn)轉(zhuǎn)換成子控件坐標(biāo)系上的點(diǎn) convertPoint: toView:
讓子控件找最合適view, hitTest: withEvent:
if(view) return view;
事件傳遞到某個控件,就會調(diào)用HitTest,我們可以重寫此方法,改變事件傳遞鏈.使用情況比較少,一般用于自定義手勢
上面的步驟就是點(diǎn)擊檢測的過程,其實(shí)就是查找事件觸發(fā)者的過程。觸摸對象并非就是事件的響應(yīng)者,檢測到了觸摸的對象之后,事件到底是如何響應(yīng)呢?這個過程就必須引入一個新的概念“響應(yīng)者鏈”。
2.2 事件的響應(yīng)
響應(yīng)者鏈條.
作用:讓多個view響應(yīng)一個事件
響應(yīng)者鏈條 (一般限限子父關(guān)系) - 當(dāng)touch方法中調(diào)用父類的touch方法, 說明自己這個控件不處理事件,交還給上一個響應(yīng)者,讓它決定處理還是繼續(xù)傳遞.
- 如果當(dāng)前的view是控制器的view,那么控制器就是上一個響應(yīng)者.直到Root控制器(結(jié)尾—最后到window->application->銷毀)
- 如果不是,則父控件是上一個響應(yīng)者.
事件傳遞:hiTest 沒有控制器參與.
響應(yīng)者鏈條: touch 如果不實(shí)現(xiàn),不執(zhí)行事件,系統(tǒng)默認(rèn)找父類控件.一直到控制器--最后--> window-> app-> 如果都不響應(yīng)就會銷毀
3. 手勢識別
3.1 簡介
在iOS3.2之后蘋果引入了手勢識別,對于用戶常用的手勢操作進(jìn)行了識別并封裝成具體的類供開發(fā)者使用,這樣在開發(fā)過程中我們就不必再自己編寫算法識別用戶的觸摸操作了。在iOS中有六種手勢操作:
手勢 | 說明 |
---|---|
UITapGestureRecognizer | 點(diǎn)按手勢 |
UIPinchGestureRecognizer | 捏合手勢 |
UIPanGestureRecognizer | 拖動手勢 |
UISwipeGestureRecognizer | 輕掃手勢,支持四個方向的輕掃,但是不同的方向要分別定義輕掃手勢 |
UIRotationGestureRecognizer | 旋轉(zhuǎn)手勢 |
UILongPressGestureRecognizer | 長按手勢 |
* tap(代理:左邊不能點(diǎn),右邊能點(diǎn))
* longPress(allowableMovement:觸發(fā)之前,最大的移動范圍)
> 默認(rèn)調(diào)用兩次,開始一次,結(jié)束一次。
* swipe:(一個手勢只能識別一個方向)
* 旋轉(zhuǎn):
基于上一次旋轉(zhuǎn)
注意:通過transform形變,需要去掉autolayout,才準(zhǔn)確
* 復(fù)位:(手勢的取值都是相對最原始的位置,我們應(yīng)該是需要相對上一次,因此每次調(diào)用,就復(fù)位一下,每次都是從零開始旋轉(zhuǎn)角度)
縮放:復(fù)位
* 如何同時支持旋轉(zhuǎn)和縮放?默認(rèn)不支持多個手指,
Simultaneously:同時
當(dāng)使用一個手勢的時候會調(diào)用代理的Simultaneously方法,詢問是否支持多個手勢
* pan
獲取平移的位置:translationInView
復(fù)位:setTranslation:inView: 需要傳一個view,因?yàn)辄c(diǎn)的位置跟坐標(biāo)系有關(guān)系,看他是基于哪個坐標(biāo)系被清空的。
所有的手勢操作都繼承于UIGestureRecognizer,這個類本身不能直接使用。這個類中定義了這幾種手勢共有的一些屬性和方法(下表僅列出常用屬性和方法):
屬性 | 說明 |
---|---|
@property(nonatomic,readonly) UIGestureRecognizerState state; | 手勢狀態(tài) |
@property(nonatomic, getter=isEnabled) BOOL enabled; | 手勢是否可用 |
@property(nonatomic,readonly) UIView *view; | 觸發(fā)手勢的視圖(一般在觸摸執(zhí)行操作中我們可以通過此屬性獲觸摸視圖進(jìn)行操作 |
@property(nonatomic) BOOL delaysTouchesBegan; | 手勢識別失敗前不執(zhí)行觸摸開始事件,默認(rèn)為NO;如果為YES,那么成功識別則不執(zhí)行觸摸開始事件,失敗則執(zhí)行觸摸開始事件;如果為NO,則不管成功與否都執(zhí)行觸摸開始事件; |
方法 | 說明 |
---|---|
-(void)addTarget:(id)target action:(SEL)action; | 添加觸摸執(zhí)行事件 |
-(void)removeTarget:(id)target action:(SEL)action; | 移除觸摸執(zhí)行事件 |
-(NSUInteger)numberOfTouches; | 觸摸點(diǎn)的個數(shù)(同時觸摸的手指數(shù)) |
-(CGPoint)locationInView:(UIView * )view; | 在指定視圖中的相對位置 |
-(CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView * )view; | 觸摸點(diǎn)相對于指定視圖的位置 |
-(void)requireGestureRecognizerToFail:(UIGestureRecognizer * )otherGestureRecognizer; | 指定一個手勢需要另一個手勢執(zhí)行失敗才會執(zhí)行 |
代理方法 | |
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer * )otherGestureRecognizer; | 一個控件的手勢識別后是否阻斷手勢識別繼續(xù)向下傳播,默認(rèn)為NO;如果返回YES,響應(yīng)者鏈上層對象觸發(fā)手勢識別后,如果下層對象也添加了手勢并成功識別也會繼續(xù)執(zhí)行,否則上層對象識別后則不再繼續(xù)傳播; |
3.2 手勢狀態(tài)
在六種手勢識別中,只有一種手勢是離散手勢,它就是UITapGestureRecgnier。離散手勢特點(diǎn)就是一旦識別無法取消,而且只會調(diào)用一次手勢操作事件. 換句話說其他五種手勢是連續(xù)手勢,連續(xù)手勢的特點(diǎn)就是會多次調(diào)用手勢操作事件,而且在連續(xù)手勢識別后可以取消手勢。
所以state-手勢狀態(tài)分為如下幾種:
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
UIGestureRecognizerStatePossible, // 尚未識別是何種手勢操作(但可能已經(jīng)觸發(fā)了觸摸事件),默認(rèn)狀態(tài)
UIGestureRecognizerStateBegan, // 手勢已經(jīng)開始,此時已經(jīng)被識別,但是這個過程中可能發(fā)生變化,手勢操作尚未完成
UIGestureRecognizerStateChanged, // 手勢狀態(tài)發(fā)生轉(zhuǎn)變
UIGestureRecognizerStateEnded, // 手勢識別操作完成(此時已經(jīng)松開手指)
UIGestureRecognizerStateCancelled, // 手勢被取消,恢復(fù)到默認(rèn)狀態(tài)
UIGestureRecognizerStateFailed, // 手勢識別失敗,恢復(fù)到默認(rèn)狀態(tài)
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded // 手勢識別完成,同UIGestureRecognizerStateEnded
};
- 對于離散手勢,tap;要么被識別,要么失敗.點(diǎn)按下去一次不松開則此時什么也不會發(fā)生,松開手指立即識別并調(diào)用操作事件,并且狀態(tài)為3(已完成)
- 連續(xù)手勢要復(fù)雜一些.拿旋轉(zhuǎn)手勢為例.如果兩個手指點(diǎn)下去不做任何操作,此時并不能識別手勢.但是已經(jīng)觸發(fā)了觸摸開始事件此時狀態(tài)為0;如果此時旋轉(zhuǎn)被識別,也就會調(diào)用對于的操作時間,同時狀態(tài)更新為1(手勢開始.)但是狀態(tài)1只有一瞬間,緊接著變?yōu)闋顟B(tài)2,并持續(xù); 松開手指后,此時狀態(tài)變?yōu)?,并調(diào)用1次操作事件.
通過蘋果官方分析圖理解:
3.3 使用手勢
一般用于UIView,不能add target的控件添加點(diǎn)擊事件.
一般步驟:
- 創(chuàng)建對應(yīng)手勢對象;
- 設(shè)置手勢識別屬性(可選)
- 添加手勢到指定對象
- 編寫手勢操作方法.
示例代碼:
/*創(chuàng)建手勢對象*/
UITapGestureRecognizer *tapGesture = [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapImage:)];
/*設(shè)置手勢屬性*/
tapGesture.numberOfTapsRequired=1;//設(shè)置點(diǎn)按次數(shù),默認(rèn)為1,注意在iOS中很少用雙擊操作
tapGesture.numberOfTouchesRequired=1;//點(diǎn)按的手指數(shù)
/*添加手勢到對象*/
[self.imageView addGestureRecognizer:tapGesture];
/*編寫手勢操作方法*/
-(void)tapImage:(UITapGestureRecognizer *)gesture {
//編寫希望執(zhí)行的操作.
}
//其他手勢注意點(diǎn):
流程基本相同,但是因?yàn)閷儆谶B續(xù)手勢.操作方法會調(diào)用多次,所以需要判斷其手勢狀態(tài).
PS.輕掃手勢雖然是連續(xù)手勢但是它的操作事件只會在識別結(jié)束時調(diào)用一次,此外輕掃手勢支持四個方向,但是如果要支持多個方向需要添加多個輕掃手勢。
3.4 手勢沖突.
在iOS中,如果一個手勢A的識別部分是另一個手勢B的子部分時,默認(rèn)情況下A會先識別,B就無法識別了;例:拖動手勢的操作事件是在手勢的開始狀態(tài)(狀態(tài)1)識別執(zhí)行的,而輕掃手勢的操作事件只有在手勢結(jié)束狀態(tài)(狀態(tài)3)才能執(zhí)行,因此輕掃手勢就作為了犧牲品沒有被正確識別。
解決方法:
//在添加手勢之后調(diào)用requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer 方法
//這個方法可以指定某個手勢執(zhí)行的前提是另一個手勢失敗才會識別執(zhí)行,即指定拖動手勢的執(zhí)行前提為輕掃手勢失敗;這樣一來,當(dāng)我們的滑動時,系統(tǒng)會優(yōu)先考慮輕掃手勢,如果發(fā)現(xiàn)不是輕掃,那么會執(zhí)行拖動.
[panGesture requireGestureRecognizerToFail:swipeGestureToRight];
//解決滑動與右輕掃沖突.
3.5 兩個不同控件的手勢同時執(zhí)行
在iOS觸摸事件中,事件觸發(fā)時根據(jù)響應(yīng)者鏈條進(jìn)行的,上層觸摸事件執(zhí)行后就不會向下傳播;默認(rèn)情況下手勢也是類似的,先識別的手勢會阻斷手勢識別操作繼續(xù)傳播。那么.如何讓兩個有層次的關(guān)系并且都添加了手勢的控件都能正確識別手勢呢?--自己做不到...找代理.
//手勢代理方法.
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
if ([otherGestureRecognizer.view isKindOfClass:[UIImageView class]])
{return YES;} //表示只有在UIImageView的手勢才能向下傳播.
return NO;
}
4. 加速計事件-運(yùn)動事件
4.1 簡介
iOS中和運(yùn)動相關(guān)的有三個事件:開始運(yùn)動、結(jié)束運(yùn)動、取消運(yùn)動。
監(jiān)聽運(yùn)動事件對于UI控件有個前提: 監(jiān)聽對象必須是第一響應(yīng)者.(對于UIViewController視圖控制器和UIApplication沒有此要求);所以 如果簡體一個UI控件,那么使之-(BOOL)canBecomeFirstResponder 返回YES;然后設(shè)置為第一響應(yīng)者
注意:設(shè)置第一響應(yīng)者后,如果控件不顯示了,要注銷控件的第一響應(yīng)者身份.所以常規(guī)設(shè)置響應(yīng)者的代碼如下:
KCImageView中
//設(shè)置控件可以成為第一響應(yīng)者
-(BOOL)canBecomeFirstResponder {
return YES;
}
#pragma mark 運(yùn)動開始
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event{
//這里只處理搖晃事件
if (motion==UIEventSubtypeMotionShake) {
self.image=[self getImage];
}
}
#pragma mark 運(yùn)動結(jié)束
-(void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event{
}
控制器中
#pragma mark 視圖顯示時讓控件變成第一響應(yīng)者
-(void)viewDidAppear:(BOOL)animated{
_imageView=[[KCImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
_imageView.userInteractionEnabled=true;
[self.view addSubview:_imageView];
[_imageView becomeFirstResponder];
}
#pragma mark 視圖不顯示時注銷控件第一響應(yīng)者的身份
-(void)viewDidDisappear:(BOOL)animated{
[_imageView resignFirstResponder];
}
5. 遠(yuǎn)程控制事件
5.1 簡介
遠(yuǎn)程控制,遠(yuǎn)程控制事件這里主要說的就是耳機(jī)線控操作。
監(jiān)聽遠(yuǎn)程控制事件有三個前提:
- 啟用遠(yuǎn)程事件接收.(使用[UIApplication shareApplication] beginReceivingRemoteControlEvents;方法);
- 對于UI控件同樣要求必須是第一響應(yīng)者(對于視圖控制器UIViewController或者應(yīng)用程序UIApplication對象監(jiān)聽無此要求)。
- 應(yīng)用程序必須是當(dāng)前音頻的控制者,也就是在iOS 7中通知欄中當(dāng)前音頻播放程序必須是我們自己開發(fā)程序。
基于第三點(diǎn)我們必須明確,如果我們的程序不想要控制音頻,只是想利用遠(yuǎn)程控制事件做其他的事情,例如模仿iOS7中的按音量+鍵拍照是做不到的,目前iOS7給我們的遠(yuǎn)程控制權(quán)限還僅限于音頻控制(當(dāng)然假設(shè)我們確實(shí)想要做一個和播放音頻無關(guān)的應(yīng)用但是又想進(jìn)行遠(yuǎn)程控制,也可以隱藏一個音頻播放操作,拿到遠(yuǎn)程控制操作權(quán)后進(jìn)行遠(yuǎn)程控制)。
運(yùn)動事件中我們也提到一個枚舉類型UIEventSubtype,而且我們利用它來判斷是否運(yùn)動事件,在枚舉中還包含了我們運(yùn)程控制的子事件類型,我們先來熟悉一下這個枚舉(從遠(yuǎn)程控制子事件類型也不難發(fā)現(xiàn)它和音頻播放有密切關(guān)系):
詳細(xì)內(nèi)容參考博客
抽屜效果
步驟思路
添加子視圖
* 簡單的滑動效果
* 監(jiān)聽控制器處理事件方法
* 獲取x軸偏移量
* 改變主視圖的frame
* 利用KVO做視圖切換
往左移動,顯示右邊,隱藏左邊
往右移動,顯示左邊,隱藏右邊
* 復(fù)雜的滑動效果,PPT講解(根據(jù)手指每移動一點(diǎn),x軸的偏移量算出當(dāng)前視圖的frame)
假設(shè)x移到320時,y移動到60,算出沒移動一點(diǎn)x,移動多少y
offsetY = offsetX * 60 / 320 手指每移動一點(diǎn),x軸偏移量多少,y偏移多少
為了好看,x移動到320,距離上下的高度需要保持一致,而且有一定的比例去縮放他的尺寸。
怎么根據(jù)之前的frame,算出當(dāng)前的frame,touchMove只能拿到之前的frame.
當(dāng)前的高度 = 之前的高度 * 這個比例
縮放比例:當(dāng)前的高度/之前的高度 (screenH - 2 * offsetY) / screenH
當(dāng)前的寬度也一樣求。
y值,計算比較特殊,不能直接用之前的y,加上offsetY,往左滑動,主視圖應(yīng)該往下走,但是offsetX是負(fù)數(shù),導(dǎo)致主視圖會往上走。
y = (screenH - 當(dāng)前的高度)* 0.5
getCurrentFrameWithOffsetX
* 定位(滑動松開手指的時候,移動到目標(biāo)點(diǎn))
移動到左右目標(biāo)點(diǎn),根據(jù)偏移量 = 當(dāng)前目標(biāo)點(diǎn)的x - 之前視圖的x,計算移動到目標(biāo)點(diǎn)的frame
還原:當(dāng)沒有移動到目標(biāo)點(diǎn),就把主視圖還原。
* 復(fù)位(當(dāng)主視圖不在原始的位置,點(diǎn)擊屏幕,恢復(fù)原來位置)
判斷手指是否移動,移動的時候就自動定位,不需要手動復(fù)位。