iOS-手勢

iOS中所有的手勢操作都繼承于UIGestureRecognizer,這個類本身不能直接使用沿侈。這個類中定義了這幾種手勢公有的一些屬性和方法谐区。

一.UIGestureRecognizer基類

1. 屬性和方法

// 指定初始化器
- (instancetype)initWithTarget:(nullable id)target action:(nullable SEL)action NS_DESIGNATED_INITIALIZER; 
// 添加 target/actions
- (void)addTarget:(id)target action:(SEL)action;    
// 移除target/actions
- (void)removeTarget:(nullable id)target action:(nullable SEL)action; 

// 手勢狀態(tài)
@property(nonatomic,readonly) UIGestureRecognizerState state;  
// 手勢代理
@property(nullable,nonatomic,weak) id <UIGestureRecognizerDelegate> delegate; 

//比如:關閉導航欄的側滑返回
//self.navigationController.interactivePopGestureRecognizer.enabled = NO;
 // 手勢是否可用,默認可用
@property(nonatomic, getter=isEnabled) BOOL enabled; 
 // 手勢被添加的視圖,一般拿到這個視圖做事情
@property(nullable, nonatomic,readonly) UIView *view;          

// 默認為YES,這種情況下當手勢識別器識別到touch之后鼻忠,會發(fā)送touchesCancelled給hit-test view以取消hit-test view對touch的響應,這個時候只有手勢識別器響應touch涧尿。設置為NO,好讓手勢傳播到其他控件上
@property(nonatomic) BOOL cancelsTouchesInView; 

// 默認是NO,這種情況下當發(fā)生一個touch時檬贰,手勢識別器先捕捉到到touch姑廉,然后發(fā)給hit-testview,兩者各自做出響應翁涤。如果設置為YES,手勢識別器在識別的過程中(注意是識別過程),不會將touch發(fā)給hit-test view哨啃,即hit-testview不會有任何觸摸事件迁沫。只有在識別失敗之后才會將touch發(fā)給hit-testview,這種情況下hit-test view的響應會延遲約0.15ms鸳粉。
@property(nonatomic) BOOL delaysTouchesBegan; 

// 默認為YES扔涧。這種情況下發(fā)生一個touch時,在手勢識別成功后,發(fā)送給touchesCancelled消息給hit-test view届谈,手勢識別失敗時枯夜,會延遲大概0.15ms,期間沒有接收到別的touch才會發(fā)送touchesEnded。如果設置為NO艰山,則不會延遲湖雹,即會立即發(fā)送touchesEnded以結束當前觸摸。
@property(nonatomic) BOOL delaysTouchesEnded; 

// 允許Touch的數(shù)組類型
@property(nonatomic, copy) NSArray<NSNumber *> *allowedTouchTypes NS_AVAILABLE_IOS(9_0); /

// 允許按壓的數(shù)組類型
@property(nonatomic, copy) NSArray<NSNumber *> *allowedPressTypes NS_AVAILABLE_IOS(9_0); 

//是否同時只接受一種觸摸類型, 默認YES
@property (nonatomic) BOOL requiresExclusiveTouchType NS_AVAILABLE_IOS(9_2); 

//這個方法可以指定某個手勢執(zhí)行的前提是后一個手勢識別失敗曙搬。(設置優(yōu)先級,后面優(yōu)先級大于前面的)
- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer;

// 手指在視圖上的位置(x,y)就是手指在視圖本身坐標系的位置
- (CGPoint)locationInView:(nullable UIView*)view;                               

// 觸摸點的個數(shù)
@property(nonatomic, readonly) NSUInteger numberOfTouches;                                          

//(touchIndex 是第幾個觸摸點)用來獲取多觸摸點在view上位置信息的方法  
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(nullable UIView*)view; // the location of a particular touch

// 給手勢加一個名字劝枣,以方便調(diào)式(iOS11 or later可以用)
@property (nullable, nonatomic, copy) NSString *name API_AVAILABLE(ios(11.0), tvos(11.0)); // name for debugging to appear in logging

重要屬性詳解:

① cancelsTouchesInView / delaysTouchesBegan / delaysTouchesEnded**

解釋說明
1.這3個屬性是作用于GestureRecognizers(手勢識別)與觸摸事件之間聯(lián)系的屬性。
2.對于觸摸事件织鲸,window只會有一個控件來接收touch舔腾。這個控件是首先接觸到touch的并且重寫了觸摸事件方法(一個即可)的控件。
3.手勢識別和觸摸事件是兩個獨立的事搂擦,只是可以通過這3個屬性互相影響稳诚。
在這3個屬性都處于默認值的情況下,如果觸摸window瀑踢,首先由window上最先符合條件的控件(該控件記為hit-test view)接收到該touch并觸發(fā)觸摸事件touchesBegan扳还。同時如果某個控件的手勢識別器接收到了該touch才避,就會進行識別。手勢識別成功之后發(fā)送觸摸事件touchesCancelled給hit-test view氨距,hit-test view不再響應touch桑逝。

cancelsTouchesInView:

默認為YES,這種情況下當手勢識別器識別到touch之后俏让,會發(fā)送touchesCancelled給hit-test view以取消hit-test view對touch的響應楞遏,這個時候只有手勢識別器響應touch。
當設置成NO時首昔,手勢識別器識別到touch之后不會發(fā)送touchesCancelled給hit-test寡喝,這個時候手勢識別器和hit-test view均響應touch。

舉個例子:
ViewController代碼如下,點擊空白

- (void)addPanGesture
{
    //拖動手勢
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandler:)];
    pan.cancelsTouchesInView = YES;
    [self.view addGestureRecognizer:pan];
}
- (void)panHandler:(UIPanGestureRecognizer *)sender
{
    NSLog(@"panHandler 調(diào)用了");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"touchesBegin調(diào)用了");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesMoved調(diào)用了");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"touchesEnded調(diào)用了");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"touchesCancelled調(diào)用了");
}

打印結果為:

pan.cancelsTouchesInView = YES;
2019-10-14 14:51:53.477277+0800 點擊事件測試[2968:5316495] touchesBegin調(diào)用了
2019-10-14 14:51:53.619030+0800 點擊事件測試[2968:5316495] touchesMoved調(diào)用了
2019-10-14 14:51:53.642291+0800 點擊事件測試[2968:5316495] touchesMoved調(diào)用了
2019-10-14 14:57:06.496434+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.518827+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:51:53.665728+0800 點擊事件測試[2968:5316495] panHandler 調(diào)用了
2019-10-14 14:51:53.666045+0800 點擊事件測試[2968:5316495] touchesCancelled調(diào)用了
2019-10-14 14:51:53.687465+0800 點擊事件測試[2968:5316495] panHandler 調(diào)用了
2019-10-14 14:51:53.687801+0800 點擊事件測試[2968:5316495] panHandler 調(diào)用了
2019-10-14 14:51:53.879677+0800 點擊事件測試[2968:5316495] panHandler 調(diào)用了
2019-10-14 14:51:53.879677+0800 點擊事件測試[2968:5316495] panHandler 調(diào)用了

pan.cancelsTouchesInView = NO;
2019-10-14 14:57:06.272598+0800 點擊事件測試[3074:5324828] touchesBegin調(diào)用了
2019-10-14 14:57:06.450891+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.472362+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.496434+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.518827+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.585744+0800 點擊事件測試[3074:5324828] panHandler 調(diào)用了
2019-10-14 14:57:06.586023+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.608386+0800 點擊事件測試[3074:5324828] panHandler 調(diào)用了
2019-10-14 14:57:06.608978+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.630915+0800 點擊事件測試[3074:5324828] panHandler 調(diào)用了
2019-10-14 14:57:06.631178+0800 點擊事件測試[3074:5324828] touchesMoved調(diào)用了
2019-10-14 14:57:06.834212+0800 點擊事件測試[3074:5324828] panHandler 調(diào)用了
2019-10-14 14:57:06.834497+0800 點擊事件測試[3074:5324828] touchesEnded調(diào)用了
  1. 例子中pan.cancelsTouchesInView = YES時勒奇,為什么會打印"touchesMoved調(diào)用了"呢预鬓?這是因為手勢識別是有一個過程的,拖拽手勢需要一個很小的手指移動的過程才能被識別為拖拽手勢赊颠,而在一個手勢觸發(fā)之前格二,是會一并發(fā)消息給事件傳遞鏈的,所以才會有最開始的幾個touchMoved方法被調(diào)用竣蹦,當識別出拖拽手勢以后顶猜,就會終止touch事件的傳遞。
  2. 當pan.cancelsTouchsInView = NO草添,不會終止touch事件的傳遞touchesMoved和panHandler依次被打印出來驶兜,touch事件繼續(xù)響應。
delaysTouchesBegan:

默認是NO远寸,這種情況下當發(fā)生一個touch時抄淑,手勢識別器先捕捉到touch,然后發(fā)給hit-testview驰后,兩者各自做出響應肆资。如果設置為YES,手勢識別器在識別的過程中(注意是識別過程)灶芝,不會將touch發(fā)給hit-test view郑原,即hit-testview不會有任何觸摸事件。只有在識別失敗之后才會將touch發(fā)給hit-testview夜涕,這種情況下hit-test view的響應會延遲約0.15ms犯犁,(就是設置為YES的時候,手勢識別器有優(yōu)先的感覺)女器。

舉個例子:

- (void)addPanGesture
{
    //拖動手勢
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandler:)];
    pan.delaysTouchesBegan = YES;
    [self.view addGestureRecognizer:pan];
}
- (void)panHandler:(UIPanGestureRecognizer *)sender
{
    NSLog(@"panHandler 調(diào)用了");
}

//pan.delaysTouchesBegan = YES;  控制臺輸出如下:
2018-07-26 16:06:59.682302+0800 GestureDemo[82294:1669777] panHandler 調(diào)用了
2018-07-26 16:06:59.689734+0800 GestureDemo[82294:1669777] panHandler 調(diào)用了
2018-07-26 16:06:59.689973+0800 GestureDemo[82294:1669777] panHandler 調(diào)用了
2018-07-26 16:06:59.697302+0800 GestureDemo[82294:1669777] panHandler 調(diào)用了
2018-07-26 16:06:59.697675+0800 GestureDemo[82294:1669777] panHandler 調(diào)用了

//pan.delaysTouchesBegan = NO;  控制臺輸出如下:
2019-10-14 17:56:08.831884+0800 點擊事件測試[7174:5509207] -[ViewController touchesBegan:withEvent:]
2019-10-14 17:56:08.907463+0800 點擊事件測試[7174:5509207] -[ViewController touchesMoved:withEvent:]
2019-10-14 17:56:08.930231+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]
2019-10-14 17:56:08.930687+0800 點擊事件測試[7174:5509207] -[ViewController touchesCancelled:withEvent:]
2019-10-14 17:56:08.952144+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]
2019-10-14 17:56:08.952485+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]
2019-10-14 17:56:08.977125+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]
2019-10-14 17:56:08.997982+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]
2019-10-14 17:56:09.020301+0800 點擊事件測試[7174:5509207] -[ViewController panHandler:]

當delaysTouchesBegan 設置為YES時酸役,手勢識別成功之前都不會調(diào)用touches相關方法,因為手勢識別成功了,所以控制臺只打印了"panHandler 調(diào)用了"的信息涣澡,如果手勢識別失敗了贱呐,就會打印touchesMoved方法里的信息

delaysTouchesEnded:

默認為YES,這種情況下發(fā)生一個touch時入桂,在手勢識別成功后奄薇,發(fā)送給touchesCancelled消息給hit-test view,手勢識別失敗時抗愁,會延遲大概0.15ms馁蒂,期間沒有接收到別的touch才會發(fā)送touchesEnded。如果設置為NO驹愚,則不會延遲远搪,即會立即發(fā)送touchesEnded以結束當前觸摸劣纲。
舉個例子:

- (void)addTapGesture
{
     //連續(xù)三次點擊手勢
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHandler:)];
    tap.numberOfTapsRequired = 3;
    tap.delaysTouchesEnded = YES;
    [self.view addGestureRecognizer:tap];
}
- (void)tapHandler:(UITapGestureRecognizer *)sender
{
    NSLog(@"tapHandler 點擊了");
}

delaysTouchesEnded = YES
2019-10-14 15:42:52.693015+0800 點擊事件測試[4222:5380031] touchesBegin調(diào)用了
2019-10-14 15:42:52.713909+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:52.722584+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:52.731320+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:52.739306+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:52.747152+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:52.755081+0800 點擊事件測試[4222:5380031] touchesMoved調(diào)用了
2019-10-14 15:42:53.191136+0800 點擊事件測試[4222:5380031] tapHandler 點擊了
2019-10-14 15:42:53.191487+0800 點擊事件測試[4222:5380031] touchesCancelled調(diào)用了

delaysTouchesEnded = NO
2019-10-14 15:43:33.320133+0800 點擊事件測試[4272:5382236] touchesBegin調(diào)用了
2019-10-14 15:43:33.338324+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.346445+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.354754+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.363870+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.372995+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.397715+0800 點擊事件測試[4272:5382236] touchesEnded調(diào)用了
2019-10-14 15:43:33.534690+0800 點擊事件測試[4272:5382236] touchesBegin調(diào)用了
2019-10-14 15:43:33.557135+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.565328+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.573337+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.580876+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.597362+0800 點擊事件測試[4272:5382236] touchesEnded調(diào)用了
2019-10-14 15:43:33.712002+0800 點擊事件測試[4272:5382236] touchesBegin調(diào)用了
2019-10-14 15:43:33.732732+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.741955+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.750436+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.773844+0800 點擊事件測試[4272:5382236] touchesMoved調(diào)用了
2019-10-14 15:43:33.783851+0800 點擊事件測試[4272:5382236] tapHandler 點擊了
2019-10-14 15:43:33.784363+0800 點擊事件測試[4272:5382236] touchesCancelled調(diào)用了

設置為YES逢捺,會等待一個很短的時間,如果沒有接收到新的手勢識別任務癞季,才會發(fā)送touchesEnded消息到事件傳遞鏈劫瞳。
設置為NO,則會立馬調(diào)用touchEnd:withEvent這個方法

② requireGestureRecognizerToFail

用法:[A requireGestureRecognizerToFail:B] 當A绷柒、B兩個手勢同時滿足響應手勢方法的條件時志于,B優(yōu)先響應,A不響應废睦。如果B不滿足條件伺绽,A滿足響應手勢方法的條件,則A響應嗜湃。其實這就是一個設置響應手勢優(yōu)先級的方法奈应。

如果一個view上添加了多個手勢對象的,默認這些手勢是互斥的购披,一個手勢觸發(fā)了就會默認屏蔽其他手勢動作杖挣。比如,單擊和雙擊手勢并存時刚陡,如果不做處理惩妇,它就只能發(fā)送出單擊的消息。為了能夠優(yōu)先識別雙擊手勢筐乳,我們就可以用requireGestureRecognizerToFail:這個方法設置優(yōu)先響應雙擊手勢歌殃。

2. UIGestureRecognizerDelegate代理方法

這里講的不詳細,以后慢慢整理.

//手勢已經(jīng)識別蝙云,通過這個方法的返回值氓皱,看是否響應, YES響應, NO不響應
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;

//是否響應手勢的四個touch方法
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;

//同上
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;

//Simultaneously同時的
//兩個手勢是否共存(一起響應),A手勢和B手勢贮懈,只要這兩個手勢有一個手勢的這個代理方法返回的YES匀泊,那么就是共存
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;

//優(yōu)先級:gestureRecognizer它要響應优训,必須得滿足otherGestureRecognizer響應失敗,才可以各聘,otherGestureRecognizer的優(yōu)先級最高
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

//優(yōu)先級:otherGestureRecognizer它要響應揣非,需要gestureRecognizer響應失敗,才可以躲因,gestureRecognizer的優(yōu)先級最高
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer NS_AVAILABLE_IOS(7_0);

二. UIGestureRecognizer子類

在iOS中有六種手勢操作:
UITapGestureRecognizer : 點擊手勢
UIPinchGestureRecognizer : 捏合手勢
UIPanGestureRecognizer : 拖動手勢
UISwipeGestureRecognizer : 輕掃手勢
UIRotationGestureRecognizer : 旋轉(zhuǎn)手勢
UILongPressGestureRecognizer : 長按手勢
所有的手勢操作都繼承于UIGestureRecognizer

1. 手勢的分類

  • 離散手勢
    離散手勢只會觸發(fā)一次,而且一旦識別就無法取消,比如UITapGestureRecgnier
  • 連續(xù)手勢
    連續(xù)手勢會一直向action method發(fā)送消息,告訴值改變了,除UITapGestureRecgnier外的手勢都是連續(xù)手勢

    從下圖可以看出離散手勢和連續(xù)手勢action method的調(diào)用次數(shù)
    調(diào)用次數(shù)

2. 手勢的狀態(tài)

  • UIGestureRecognizerState
typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 尚未識別是何種手勢,但是可能已經(jīng)觸發(fā)觸摸事件,默認狀態(tài)
    UIGestureRecognizerStateBegan,      // 手勢已經(jīng)被識別,手勢開始,但這個過程可能發(fā)生改變,手勢操作尚未完成,action方法將在下一輪運行循環(huán)調(diào)用
    UIGestureRecognizerStateChanged,    // 手勢發(fā)生改變,action方法將在下一輪運行循環(huán)調(diào)用
    UIGestureRecognizerStateEnded,      // 手勢結束,action方法將在下一輪運行循環(huán)調(diào)用,之后變成默認狀態(tài)
    UIGestureRecognizerStateCancelled,  // 手勢取消,action方法將在下一輪運行循環(huán)調(diào)用,之后變成默認狀態(tài)
    UIGestureRecognizerStateFailed,     // 手勢失敗,不會調(diào)用action方法將,之后變成默認狀態(tài)
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded  //同UIGestureRecognizerStateEnded
};
  1. 對于離散型手勢UITapGestureRecognizer要么被識別早敬,要么失敗,點按(假設點按次數(shù)設置為1大脉,并且沒有添加長按手勢)下去一次不松開則此時什么也不會發(fā)生搞监,松開手指立即識別并調(diào)用操作事件,并且狀態(tài)為3(已結束)镰矿。
  2. 但是連續(xù)手勢要復雜一些琐驴,就拿旋轉(zhuǎn)手勢來說,如果兩個手指點下去不做任何操作秤标,此時并不能識別手勢(因為我們還沒有旋轉(zhuǎn))但是其實已經(jīng)出發(fā)了觸摸開始事件绝淡,此時處于狀態(tài)0;如果此時旋轉(zhuǎn)會被識別苍姜,也就會調(diào)用對應的操作事件牢酵,同時狀態(tài)變成1(手勢開始),但是狀態(tài)1只有一瞬間衙猪;緊接著變成狀態(tài)2(因為我們的旋轉(zhuǎn)需要持續(xù)一會)馍乙,并且重復調(diào)用操作事件(如果在事件中打印狀態(tài)會重復打印2);松開手指垫释,此時狀態(tài)變?yōu)?丝格,并調(diào)用1次操作事件。

為了大家更好的理解這個狀態(tài)的變化饶号,不妨在操作事件中打印事件狀態(tài)铁追,會發(fā)現(xiàn)在操作事件中的狀態(tài)永遠不可能為0(默認狀態(tài)),因為只要調(diào)用操作事件就說明已經(jīng)被識別了茫船。前面也說過琅束,手勢識別的根本還是調(diào)用觸摸事件而完成的,連續(xù)手勢之所以會發(fā)生狀態(tài)轉(zhuǎn)換完全是由于觸摸事件中的移動事件造成的算谈,沒有移動事件也就不存在這個過程中狀態(tài)變化涩禀。

大家通過蘋果官方的分析圖再理解一下手勢狀態(tài):
離散手勢和連續(xù)手勢

3. 各種手勢介紹

① 點擊手勢——UITapGestureRecognizer

//設置點擊次數(shù),默認為單擊
@property (nonatomic) NSUInteger  numberOfTapsRequired; 
//設置同時點擊的手指數(shù)
@property (nonatomic) NSUInteger  numberOfTouchesRequired;

② 捏合手勢——UIPinchGestureRecognizer

//設置縮放比例
@property (nonatomic)          CGFloat scale; 
//捏合速度,只讀
@property (nonatomic,readonly) CGFloat velocity;

③ 滑動手勢——UIPanGestureRecognzer

//設置觸發(fā)拖拽的最少觸摸點然眼,默認為1
@property (nonatomic)          NSUInteger minimumNumberOfTouches; 
//設置觸發(fā)拖拽的最多觸摸點
@property (nonatomic)          NSUInteger maximumNumberOfTouches;  
//手指在視圖上移動的位置(x,y)向下和向右為正艾船,向上和向左為負
- (CGPoint)translationInView:(nullable UIView *)view;            
//設置當前移動位置
- (void)setTranslation:(CGPoint)translation inView:(nullable UIView *)view;
//手指在視圖上移動的速度(x,y), 正負也是代表方向,值得一提的是在絕對值上|x| > |y| 水平移動, |y|>|x| 豎直移動
- (CGPoint)velocityInView:(nullable UIView *)view;

更多信息可參考:
開發(fā)中的疑惑點---手勢位置locationInView屿岂、velocityInView践宴、translationInView

④ 旋轉(zhuǎn)手勢——UIRotationGestureRecognizer

//設置旋轉(zhuǎn)角度
@property (nonatomic)          CGFloat rotation;
//設置旋轉(zhuǎn)速度 
@property (nonatomic,readonly) CGFloat velocity;

⑤ 輕掃手勢——UISwipeGestureRecognizer

//設置觸發(fā)滑動手勢的觸摸點數(shù)
@property(nonatomic) NSUInteger                        numberOfTouchesRequired; 
//設置滑動方向
@property(nonatomic) UISwipeGestureRecognizerDirection direction;  
//枚舉如下
typedef NS_OPTIONS(NSUInteger, UISwipeGestureRecognizerDirection) {
    UISwipeGestureRecognizerDirectionRight = 1 << 0,
    UISwipeGestureRecognizerDirectionLeft  = 1 << 1,
    UISwipeGestureRecognizerDirectionUp    = 1 << 2,
    UISwipeGestureRecognizerDirectionDown  = 1 << 3
};

⑥ 長按手勢——UILongPressGestureRecognizer

//設置觸發(fā)前的點擊次數(shù)
@property (nonatomic) NSUInteger numberOfTapsRequired;    
//設置觸發(fā)的觸摸點數(shù)
@property (nonatomic) NSUInteger numberOfTouchesRequired; 
//設置最短的長按時間
@property (nonatomic) CFTimeInterval minimumPressDuration; 
//設置在按觸時時允許移動的最大距離 默認為10像素
@property (nonatomic) CGFloat allowableMovement;

⑦ 邊緣拖動手勢——UIScreenEdgePanGestureRecognizer

這是一個繼承UIPanGestureRecognizer的手勢,只有一個屬性,手勢方向

//手勢的方向
@property (readwrite, nonatomic, assign) UIRectEdge edges; //< The edges on which this gesture recognizes, relative to the current interface orientation

關于它的簡單使用如下圖:
邊緣拖動.png

self.view上添加黃色View,黃色View上面添加紅色View爷怀,紅色View上添加邊緣拖動手勢阻肩,從紅色View右側邊緣往左拖動,實現(xiàn)以上效果运授,代碼如下:

#import "EOCEventCase_ScreenGesture.h"
@interface EOCEventCase_ScreenGesture () {
    CGFloat center_x;
    CGFloat center_y;
}

@property(nonatomic, strong)UIView *backgroundView;
@property(nonatomic, strong)UIView *showView;
@end

@implementation EOCEventCase_ScreenGesture

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationItem.title = @"UIScreenPanGesture";
    
    center_x = self.view.bounds.size.width/2;
    center_y = self.view.bounds.size.height/2;
    
    [self.view addSubview:self.backgroundView];
    [self.backgroundView addSubview:self.showView];
    [self createScreenGestureView];

    NSLog(@"self.view是%p,紅色view是%p,黃色View是%p", self.view, self.showView, self.backgroundView);
}

- (void)createScreenGestureView {
    UIScreenEdgePanGestureRecognizer *screenEdgePanGesture = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
    
    //讓EdgePanGestureRecognizer優(yōu)先級最高
    NSArray *gestureArray = self.navigationController.view.gestureRecognizers;
    for (UIGestureRecognizer *gesture in gestureArray) {
        if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]]) {
            [gesture requireGestureRecognizerToFail:screenEdgePanGesture];
        }
    }
    
    screenEdgePanGesture.edges = UIRectEdgeRight;
    [self.showView addGestureRecognizer:screenEdgePanGesture];
}

#pragma mark - event response
- (void)panAction:(UIScreenEdgePanGestureRecognizer *)gesture
{
    UIView *view = [self.view hitTest:[gesture locationInView:gesture.view] withEvent:nil];
    NSLog(@"響應者view是%p", view);

    if (UIGestureRecognizerStateBegan == gesture.state || UIGestureRecognizerStateChanged == gesture.state) {
        CGPoint translationPoint = [gesture translationInView:gesture.view];
        _backgroundView.center = CGPointMake(center_x+translationPoint.x, center_y);
    } else {
        [UIView animateWithDuration:.3f animations:^{
            _backgroundView.center = CGPointMake(center_x, center_y); 
        }];
    }
}

#pragma mark get method
- (UIView *)showView {
    if (!_showView) {
        _showView = [[UIView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.frame.size.width, 200.f)];
        _showView.backgroundColor = [UIColor redColor];
    }
    return _showView;
}

- (UIView *)backgroundView {
    if (!_backgroundView) {
        _backgroundView = [[UIView alloc] initWithFrame:self.view.bounds];
        _backgroundView.backgroundColor = [UIColor yellowColor];
    }
    return _backgroundView;
}
@end

上面代碼烤惊,在紅色View右側一小塊區(qū)域向左滑就可以實現(xiàn)上面的效果。打印如下:

self.view是0x121f08870,紅色view是0x123501bd0,黃色View是0x123502610
......
響應者view是0x123501bd0
......
響應者view是0x121f08870

可以發(fā)現(xiàn)吁朦,在紅色view右側的一小塊區(qū)域向左滑動時柒室,響應者是紅色view,超出紅色view右側的那一小塊區(qū)域逗宜,響應者就是self.view雄右。

三. UIGestureRecognizer盈蛮、UIResponder、UIControl

先看繼承關系:

UIGestureRecognizer : NSObject
UILabel : UIView : UIResponder : NSObject
UIButton : UIControl : UIView : UIResponder : NSObject

可以看出UIGestureRecognizer和UIResponder都是直接繼承NSObject的
而UIButton最終繼承于UIResponder竿秆。

1. UIGestureRecognizer贷屎、UIResponder之間的優(yōu)先級

UIGestureRecognizer優(yōu)先級大于UIResponder

當前self.view上添加一個oneView, oneView上再添加一個tap手勢
oneView

代碼如下;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *one = [[oneView alloc]initWithFrame:CGRectMake(60, 60, 100, 100)];
    one.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:one];
    self.oenView = one;
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHandler:)];
    [self.oenView addGestureRecognizer:tap];
}
- (void)tapHandler:(UITapGestureRecognizer *)sender
{
    NSLog(@"tapHandler 點擊了");
}

oneView.m實現(xiàn)如下方法

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"one-touchesBegin調(diào)用了");
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"one-touchesMoved調(diào)用了");
}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"one-touchesEnded調(diào)用了");
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"one-touchesCancelled調(diào)用了");
}

點擊oneView打印結果如下:

2019-10-14 16:50:32.967674+0800 點擊事件測試[5637:5441763] one-touchesBegin調(diào)用了
2019-10-14 16:50:32.983451+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:32.991280+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:32.999145+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:33.007548+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:33.019674+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:33.023782+0800 點擊事件測試[5637:5441763] one-touchesMoved調(diào)用了
2019-10-14 16:50:33.050233+0800 點擊事件測試[5637:5441763] tapHandler 點擊了
2019-10-14 16:50:33.050510+0800 點擊事件測試[5637:5441763] one-touchesCancelled調(diào)用了

從日志上看出oneView最后cancel了對觸摸事件的響應,而正常應當是觸摸結束后矗烛,oneView的touchesEnded:withEvent:方法被調(diào)用才對。另外,期間還執(zhí)行了手勢識別器綁定的action则涯。對于這種現(xiàn)象,官方文檔上有這么一段描述:

A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and doesn’t recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled. The usual sequence of actions in gesture recognition follows a path determined by default values of the cancelsTouchesInView, delaysTouchesBegan, delaysTouchesEnded properties:

這段描述的意思是:UIWindow會先把touch事件分發(fā)給手勢識別器冲簿,然后再分發(fā)給hit-tested view即oneView粟判,如果一個手勢識別器分析了這一系列的點擊事件之后沒有識別出該手勢,hit-tested view將會接收完整的點擊事件峦剔。如果手勢識別器識別了該手勢档礁,hit-tested view將會取消這次點擊。由此可以看出:手勢識別器比UIResponder具有更高的事件響應優(yōu)先級吝沫。

按照這個解釋呻澜,UIWindow在將事件傳遞給hit-tested view即oneView之前,先傳遞給了手勢識別器惨险。手勢識別器成功識別了該事件羹幸,通知application取消oneView對事件的響應。

然而看日志辫愉,卻是oneView的touchesBegan:withEvent:先調(diào)用了栅受,既然手勢識別器先響應,不應該上面的action先執(zhí)行嗎?實際上這個認知是錯誤的屏镊。手勢識別器的action的調(diào)用時機并不是手勢識別器接收到事件的時機依疼,而是手勢識別器成功識別事件后的時機,即手勢識別器的狀態(tài)變?yōu)閁IGestureRecognizerStateRecognized而芥。要證明UIWindow先將事件傳遞給了手勢識別器涛贯,還是需要看手勢識別器中這四個熟悉的方法的調(diào)用結果。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

不過不要誤會蔚出,UIGestureRecognizer并不繼承于UIResponder類弟翘,他們只是方法名相同而已。
這樣骄酗,我們就可以自定義一個繼承自UITapGestureRecognizer的子類稀余,重寫這四個方法,觀察事件分發(fā)的順序趋翻。上面的四個方法聲明在SubTapGesture.h中

#import "SubTapGesture.h"
@implementation SubTapGesture

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 這里需要調(diào)用一下父類的touchesBegan方法睛琳,否則事件會被攔截消耗掉
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s", __func__);
}
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesMoved:touches withEvent:event];
    NSLog(@"%s", __func__);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    NSLog(@"%s", __func__);
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"%s", __func__);
}
@end

最后打印結果為:

2019-10-14 17:20:04.007387+0800 點擊事件測試[6395:5471705] -[SubTapGesture touchesBegan:withEvent:]
2019-10-14 17:20:04.008743+0800 點擊事件測試[6395:5471705] -[oneView touchesBegan:withEvent:]
2019-10-14 17:20:04.052897+0800 點擊事件測試[6395:5471705] -[SubTapGesture touchesMoved:withEvent:]
2019-10-14 17:20:04.053203+0800 點擊事件測試[6395:5471705] -[oneView touchesMoved:withEvent:]
2019-10-14 17:20:04.078489+0800 點擊事件測試[6395:5471705] -[SubTapGesture touchesEnded:withEvent:]
2019-10-14 17:20:04.079131+0800 點擊事件測試[6395:5471705] tapHandler 點擊了
2019-10-14 17:20:04.079532+0800 點擊事件測試[6395:5471705] -[oneView touchesCancelled:withEvent:]

可以看到,確實是手勢識別器先接收到了事件踏烙,然后hit-tested view接收到事件师骗。接著手勢識別器識別了手勢,執(zhí)行action讨惩,再由Application取消了oneView對事件的響應辟癌。

UIGestureRecognizer分為離散型手勢和持續(xù)型手勢,我們上面的demo用的是離散型手勢荐捻,那么如果是持續(xù)型手勢又會有什么樣的結果呢黍少?我們把UITapGestureRecognizer用UIPanGestureRecognizer替換,然后在oneView上面執(zhí)行一次滑動处面,輸出結果如下

2020-08-17 19:41:24.101631+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesBegan:withEvent:]
2020-08-17 19:41:24.102897+0800 點擊事件測試[91897:16080172] -[oneView touchesBegan:withEvent:]
2020-08-17 19:41:24.121522+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesMoved:withEvent:]
2020-08-17 19:41:24.122074+0800 點擊事件測試[91897:16080172] -[oneView touchesMoved:withEvent:]
2020-08-17 19:41:24.195710+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesMoved:withEvent:]
2020-08-17 19:41:24.196211+0800 點擊事件測試[91897:16080172] -[oneView touchesMoved:withEvent:]
2020-08-17 19:41:24.212155+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesMoved:withEvent:]
2020-08-17 19:41:24.212670+0800 點擊事件測試[91897:16080172] tapHandler 點擊了
2020-08-17 19:41:24.213111+0800 點擊事件測試[91897:16080172] -[oneView touchesCancelled:withEvent:]
2020-08-17 19:41:24.221225+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesMoved:withEvent:]
2020-08-17 19:41:24.221555+0800 點擊事件測試[91897:16080172] tapHandler 點擊了
2020-08-17 19:41:24.237961+0800 點擊事件測試[91897:16080172] -[SubTapGesture touchesMoved:withEvent:]
2020-08-17 19:41:24.238276+0800 點擊事件測試[91897:16080172] tapHandler 點擊了

在一開始滑動的過程中厂置,手勢識別器處在識別手勢階段,滑動產(chǎn)生的連續(xù)事件既會傳遞給手勢識別器又會傳遞給oneView魂角,因此oneView的touchesMoved:withEvent:在開始一段時間內(nèi)會持續(xù)調(diào)用昵济;當手勢識別器成功識別了該滑動手勢時,手勢識別器的action開始調(diào)用野揪,同時通知Application取消oneView對事件的響應访忿。之后僅由滑動手勢識別器接收事件并響應,oneView不再接收事件囱挑。另外醉顽,在滑動的過程中,若手勢識別器未能識別手勢平挑,則事件在觸摸滑動過程中會一直傳遞給hit-tested view游添,直到觸摸結束系草。

總結:

總結一下UIGestureRecognizer與UIResponder對于事件響應的聯(lián)系:

  1. 當觸摸發(fā)生或者觸摸的狀態(tài)發(fā)生變化時,UIWindow都會傳遞事件尋求響應唆涝。
  2. UIWindow先將綁定了觸摸對象的事件傳遞給觸摸對象上綁定的手勢識別器找都,再發(fā)送給觸摸對象對應的hit-tested view。
  3. 手勢識別器識別手勢期間廊酣,若觸摸對象的觸摸狀態(tài)發(fā)生變化能耻,事件都是先發(fā)送給手勢識別器再發(fā)送給hit-test view。
  4. 手勢識別器若成功識別了手勢亡驰,則通知Application取消hit-tested view對于事件的響應晓猛,并停止向其發(fā)送事件。
  5. 若手勢識別器未能識別手勢凡辱,而此時觸摸并未結束戒职,則停止向手勢識別器發(fā)送事件,僅向hit-tested view發(fā)送事件透乾。
  6. 若手勢識別器未能識別手勢洪燥,且此時觸摸已結束,則向hit-tested view發(fā)送end狀態(tài)的touch事件以停止對事件的響應乳乌。

注意:手勢的種類怎么分辨出來:通過tap捧韵、pan、swipe手勢的touchesBegan:withEvent等四個方法來識別汉操。

2. 手勢沖突

① 同一個視圖中的不同手勢之間的沖突

如果在同一個視圖上添加不同的手勢時再来,也有可能會發(fā)生沖突。照例先上代碼:

#import "ViewController.h"
#import "oneView.h"
#import "twoView.h"
#import "SubTapGesture.h"

@interface ViewController ()
@property (nonatomic,strong) UIImageView *imageView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIImageView *imageV = [[UIImageView alloc] initWithFrame:CGRectMake(100, 200, 150, 150)];
    imageV.backgroundColor = [UIColor orangeColor];
    imageV.userInteractionEnabled = YES;
    [self.view addSubview:imageV];
    self.imageView = imageV;
    
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self.imageView addGestureRecognizer:pan];
    
    UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipe:)];
   // [pan requireGestureRecognizerToFail:swipe];
    [self.imageView addGestureRecognizer:swipe];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
        CGPoint point = [gestureRecognizer translationInView:self.imageView];
        self.imageView.transform = CGAffineTransformMakeTranslation(point.x, point.y);
    }else if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
        [UIView animateWithDuration:0.3 animations:^{
            self.imageView.transform = CGAffineTransformIdentity;
        }];
    }
    NSLog(@"%s", __func__);
}

- (void)swipe:(UISwipeGestureRecognizer *)gestureRecognizer
{
    static BOOL flag = NO;
    self.imageView.backgroundColor = flag ? [UIColor redColor] : [UIColor yellowColor];
    flag = !flag;
    NSLog(@"%s", __func__);
}
@end

代碼很簡單客情,最初我們的目的是在imageView上面添加一個拖動手勢其弊,一個輕掃手勢。拖動的時候改變圖片的位置膀斋,輕掃的時候切換圖片背景色。

可以發(fā)現(xiàn)只能拖動,并不能輕掃,打印結果如下:

2019-10-15 11:33:54.879326+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:54.921684+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:54.921876+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:54.946425+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:54.968451+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:54.991616+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.004011+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.914669+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.936952+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.937278+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.964659+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:55.997283+0800 點擊事件測試[12889:5831620] -[ViewController pan:]
2019-10-15 11:33:56.014573+0800 點擊事件測試[12889:5831620] -[ViewController pan:]

補充:手勢的狀態(tài)

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {
    UIGestureRecognizerStatePossible,   // 尚未識別是何種手勢,但是可能已經(jīng)觸發(fā)觸摸事件,默認狀態(tài)
    UIGestureRecognizerStateBegan,      // 手勢已經(jīng)被識別,手勢開始,但這個過程可能發(fā)生改變,手勢操作尚未完成,action方法將在下一輪運行循環(huán)調(diào)用
    UIGestureRecognizerStateChanged,    // 手勢發(fā)生改變,action方法將在下一輪運行循環(huán)調(diào)用
    UIGestureRecognizerStateEnded,      // 手勢結束,action方法將在下一輪運行循環(huán)調(diào)用,之后變成默認狀態(tài)
    UIGestureRecognizerStateCancelled,  // 手勢取消,action方法將在下一輪運行循環(huán)調(diào)用,之后變成默認狀態(tài)
    UIGestureRecognizerStateFailed,     // 手勢失敗,不會調(diào)用action方法將,之后變成默認狀態(tài)
    UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded  //同UIGestureRecognizerStateEnded
};

可以看到痹雅,盡管我減小了輕掃的幅度仰担,加快了速度,輕掃手勢依然沒有起作用绩社,就是因為輕掃和拖動這兩個手勢起了沖突摔蓝。沖突的原因很簡單,拖動手勢的操作事件是在手勢的開始狀態(tài)(狀態(tài)1)識別執(zhí)行的愉耙,而輕掃手勢的操作事件只有在手勢結束狀態(tài)(狀態(tài)3)才能執(zhí)行贮尉,因此輕掃手勢就作為了犧牲品沒有被正確識別。要解決這個沖突可以利用requireGestureRecognizerToFail:方法來完成朴沿,這個方法可以指定某個手勢執(zhí)行的前提是另一個手勢識別失敗猜谚。

這里我們把拖動手勢設置為輕掃手勢識別失敗之后執(zhí)行败砂,這樣一來我們手指輕輕滑動時系統(tǒng)會優(yōu)先考慮輕掃手勢,如果最后發(fā)現(xiàn)該操作不是輕掃魏铅,那么就會執(zhí)行拖動昌犹。
只需要添加代碼:

[pan requireGestureRecognizerToFail:swipe];

會發(fā)現(xiàn)就可以實現(xiàn)拖動圖片和輕掃改變圖片顏色.

② 不同視圖上的手勢沖突

在上面響應者鏈的學習中,我們知道了UIResponder響應事件的時候是有優(yōu)先級的览芳,上層觸摸事件執(zhí)行后就不再向下傳播斜姥。默認情況下手勢也是類似的,先識別的手勢會阻斷手勢識別操作繼續(xù)傳播沧竟。下面我們用代碼驗證一下:
我們在控制器的視圖上面添加一個黃色的子視圖铸敏,然后在黃色視圖上面添加一個自定義的滑動手勢,在控制器的view上面也添加一個自定義的滑動手勢悟泵。在自定義的滑動手勢里面重寫touchBegan:withEvent:這四個相關的方法.

@interface GestureRecognizer : UIPanGestureRecognizer
@property (nonatomic, strong) NSString *panName;
@end

@implementation GestureRecognizer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesBegan:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesMoved:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesEnded:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}

- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [super touchesCancelled:touches withEvent:event];
    NSLog(@"%s--%@", __func__, self.panName);
}
@end

ViewController.m代碼

@interface ViewController ()
@property (weak, nonatomic) IBOutlet YellowView *yellowView;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    GestureRecognizer *pan = [[GestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    pan.panName = @"第一個";
    [self.yellowView addGestureRecognizer:pan];

    GestureRecognizer *panBottom = [[GestureRecognizer alloc] initWithTarget:self action:@selector(panBottom:)];
    panBottom.panName = @"第二個";
    [self.view addGestureRecognizer:panBottom];
}

- (void)pan:(UIPanGestureRecognizer *)gestureRecognizer
{
    NSLog(@"%s", __func__);
}

- (void)panBottom:(UIGestureRecognizer *)gestureRecognizer{
    NSLog(@"%s", __func__);
}

在黃色視圖上滑動搞坝,輸出以下結果:
滑動黃色視圖.png

可以看出,在手勢識別期間魁袜,UIWindow會依次向兩個手勢識別器和hit-test view發(fā)送事件桩撮,在第一個滑動手勢(pan)識別成功后,UIWindow會停止向視圖上的第二個滑動手勢(panBottom)發(fā)送事件峰弹,所以導致其action無法被調(diào)用店量,從而產(chǎn)生沖突。

Window怎么知道要把事件傳遞給哪些手勢識別器鞠呈?
為什么停止接收事件的是第二個滑動手勢呢融师?

之前探討過Application怎么知道要把event傳遞給哪個Window,以及Window怎么知道要把event傳遞給哪個hit-tested view的問題蚁吝,答案是這些信息都保存在event所綁定的touch對象上旱爆。
手勢識別器也是一樣的,event綁定的touch對象上維護了一個手勢識別器數(shù)組窘茁,里面的手勢識別器毫無疑問是在hit-testing的過程中收集的怀伦。在自定義Window中重寫sendEvent方法, 打個斷點看一下touch上綁定的手勢識別器數(shù)組gestureRecognizers,手勢識別的優(yōu)先級跟數(shù)組的數(shù)據(jù)是保持一致的山林。


gestureRecognizers.png

我們可以看到房待,第一個滑動手勢儲存在數(shù)組的最前面,他的優(yōu)先級比較高驼抹,所以會首先被響應桑孩。

那么如何讓兩個有層次關系并且都添加了手勢的控件都能正確識別手勢呢?答案就是利用手勢代理的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:代理方法框冀。

蘋果官方是這么描述這個方法的:

Asks the delegate if two gesture recognizers should be allowed to recognize gestures simultaneously.
This method is called when recognition of a gesture by either gestureRecognizer or otherGestureRecognizer would block the other gesture recognizer from recognizing its gesture. Note that returning YES is guaranteed to allow simultaneous recognition; returning NO, on the other hand, is not guaranteed to prevent simultaneous recognition because the other gesture recognizer's delegate may return YES.

這個方法主要是為了詢問手勢的代理流椒,是否允許兩個手勢識別器同時識別該手勢。返回YES可以確保允許同時識別手勢明也,返回NO的話不能保證一定不能同時識別宣虾,因為其他手勢的代理也有可能返回YES惯裕。
在上面demo的ViewController中遵循UIGestureRecognizerDelegate協(xié)議,設置第一個手勢的代理為self安岂,添加如下代碼:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(nonnull UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

打印:
pan和panbottom都會響應.png

可以看到兩個手勢的action同時被調(diào)用了轻猖。

3. UIControl

先看繼承關系:

UIGestureRecognizer : NSObject
UILabel : UIView : UIResponder : NSObject
UIButton : UIControl : UIView : UIResponder : NSObject

UIControl是系統(tǒng)提供的能夠以target-action模式處理觸摸事件的控件,iOS中UIButton域那、UISegmentControl咙边、UISwitch等控件都是UIControl的子類。值得注意的是次员,UIControl是UIView的子類败许,因此本身也具有UIResponder的屬性。UIControl是一個抽象基類淑蔚,我們不能直接使用UIControl類來實例化控件市殷,它只是為控件子類定義一些通用的接口,并提供一些基礎實現(xiàn)刹衫。

UIControl作為能夠響應事件的控件醋寝,必然也需要待事件交互符合條件時才去響應,因此也會跟蹤事件發(fā)生的過程带迟,不同于UIResponder以及UIGestureRecognizer通過touches系列方法跟蹤音羞,UIControl有其獨特的跟蹤方式:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)event;
- (void)cancelTrackingWithEvent:(nullable UIEvent *)event;

乍一看,這四個方法和UIResponder那四個方法幾乎吻合仓犬,只不過UIControl只能接收單點觸控嗅绰,因此這四個方法的參數(shù)是單個的UITouch對象。這四個方法的職能也和UIResponder一致搀继,用來跟蹤觸摸的開始窘面、滑動、結束叽躯、取消财边。不過,UIControl本身也是UIResponder险毁,因此同樣有touches系列的4個方法制圈。事實上,UIControl的Tracking系列方法是在touch系列方法內(nèi)部調(diào)用的畔况。比如beginTrackingWithTouch是在touchesBegan方法內(nèi)部調(diào)用的。這個我們也可以驗證:
自定義一個繼承于UIControl的子類慧库,重寫beginTrackingWithTouch和touchesBegan:withEvent:方法跷跪,并且給重寫的Control添加action和target。
代碼如下:

//ViewController.m代碼
#import "ViewController.h"
#import "subControl.h"

@interface ViewController ()
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    subControl *control = [[subControl alloc]initWithFrame:CGRectMake(50, 50, 100, 100)];
    control.backgroundColor = UIColor.redColor;
    [control addTarget:self action:@selector(subControlClicked) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:control];
}

- (void)subControlClicked{
    NSLog(@"點擊了subcontrol");
}
@end

//subControl.m代碼
@implementation subControl
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"進入%s", __func__);
    [super touchesBegan:touches withEvent:event];
    NSLog(@"離開%s", __func__);
}

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
    return [super beginTrackingWithTouch:touch withEvent:event];
}
@end

點擊自定義的Control碾篡,輸出以下結果:

2019-10-15 13:57:33.457752+0800 自定義UICOntrol[14246:5894041] 進入-[subControl touchesBegan:withEvent:]
2019-10-15 13:57:33.458154+0800 自定義UICOntrol[14246:5894041] -[subControl beginTrackingWithTouch:withEvent:]
2019-10-15 13:57:33.472355+0800 自定義UICOntrol[14246:5894041] 離開-[subControl touchesBegan:withEvent:]
2019-10-15 13:57:33.585599+0800 自定義UICOntrol[14246:5894041] 點擊了subcontrol

可以看出厢蒜,先調(diào)用touchsBegin方法, touchesBegan方法再調(diào)用了beginTrackingWithTouch方法,最后再調(diào)控件的action方法逞泄。這也說明了另外一個問題橡羞,UIControl的touchesBegan方法的實現(xiàn)與UIResponder的touchesBegan方法是有區(qū)別的眯停。

當UIControl跟蹤事件的過程中,識別出事件交互符合響應條件卿泽,(就是根據(jù)四個touch方法來識別的)就會觸發(fā)target-action進行響應莺债。UIControl控件通過addTarget:action:forControlEvents:添加事件處理的target和action,當事件發(fā)生時签夭,UIControl會調(diào)用sendAction:to:forEvent:來將event發(fā)送給UIApplication對象齐邦,再由UIApplication對象調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上。
下面是方法調(diào)用棧:

方法調(diào)用棧.png

因此第租,我們可以通過重寫UIControl的sendAction:to:forEvent:或sendAction:to:from:forEvent:自定義事件執(zhí)行的target及action措拇。

If you specify nil for the target object, the control searches the responder chain for an object that defines the specified action method.

另外,若不指定target慎宾,即addTarget:action:forControlEvents:時target傳空丐吓,那么當事件發(fā)生時,UIControl會在響應鏈從下往上尋找定義了指定action方法的對象來響應該action趟据。

可以簡單總結button事件的響應流程

  1. 通過hitTest方法找到button
  2. 通過四個touch方法來識別action
  3. button再調(diào)用sendAction:to:forEvent:來將event發(fā)送給UIApplication對象
  4. UIApplication對象調(diào)用其sendAction:to:fromSender:forEvent:方法來將消息分發(fā)到指定的target上

4. UIResponder券犁、UIGestureRecognizer、UIControl之間的優(yōu)先級

上面我們已經(jīng)分析過了之宿,UIGestureRecognizer的優(yōu)先級是比UIResponder的優(yōu)先級高的族操,那么如果再加上一個UIControl呢?

我們先來比較一下UIControl和UIResponder之間的優(yōu)先級關系比被,這里的UIResponder我們用UIView來代替色难。

首先如果UIControl添加在UIView上面的時候,毋庸置疑等缀,UIControl會首先響應枷莉,參照button添加在視圖上。

那么如果把UIView添加在UIControl上面的時候尺迂,誰會響應事件呢笤妙?我們用代碼來驗證一下:
自定義一個UIView,重寫touchesBegan:方法

@implementation YellowView
- (void)touchesBegan:(NSSet<UITouch *> *)touches   withEvent:(UIEvent *)event
{
    NSLog(@"%s", __func__);
}
@end

自定義一個UIControl噪裕,里面什么都不用寫蹲盘。

在ViewController里面添加如下代碼:

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Control *control = [[Control alloc] initWithFrame:CGRectMake(100, 100, 100, 200)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:control];

    YellowView *yellowView = [[YellowView alloc] initWithFrame:CGRectMake(10, 10, 50, 50)];
    yellowView.backgroundColor = [UIColor yellowColor];
    [control addSubview:yellowView];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}
@end

效果圖:
control里面添加View.png

紅色是Control中間添加一個黃色的view,點擊黃色View和紅色Control打印如下:

//點擊黃色View
2019-10-15 14:42:16.336325+0800 自定義UICOntrol[15540:5955840] -[YellowView touchesBegan:withEvent:]

//點擊紅色Control
2019-10-15 14:42:23.004017+0800 自定義UICOntrol[15540:5955840] click Control

可以看到點擊什么響應什么, 看來自定義的UIControl與UIResponder之間的優(yōu)先級還是遵循響應鏈的層級的,這就表示UIResponder和UIControl的優(yōu)先級是相同的膳音,而UIGestureRecognizer的優(yōu)先級比UIControl高召衔,由此推斷的話,UIGestureRecognizer的優(yōu)先級好像是比UIControl高的祭陷,具體是什么樣子的苍凛,我們還是來驗證一下趣席。

現(xiàn)在我們把層級交換一下,把UIControl添加到y(tǒng)ellowView上面醇蝴,然后給yellowView添加一個tap手勢宣肚,代碼如下:

#import "ViewController.h"
#import "SubControl.h"
#import "YellowView.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    YellowView * yellowView = [[YellowView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
    yellowView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview: yellowView];
    
    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];
    [blueView addGestureRecognizer:tap];
    
    SubControl *control = [[SubControl alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    control.backgroundColor = [UIColor redColor];
    [control addTarget:self action:@selector(clickControl:) forControlEvents:UIControlEventTouchUpInside];
    [yellowView addSubview:control];
}

- (void)clickControl:(UIControl *)control
{
    NSLog(@"click Control");
}

- (void)tap:(UIGestureRecognizer *)gestureRecognizer
{
    NSLog(@"tap");
}
@end

效果圖:
view里添加control.png

可以發(fā)現(xiàn)無論點擊紅色Control還是黃色View都打印:

2019-10-15 14:57:33.966344+0800 自定義UICOntrol[15953:5973486] tap
2019-10-15 14:57:34.732795+0800 自定義UICOntrol[15953:5973486] tap

可以看到,系統(tǒng)執(zhí)行了手勢的tap方法悠栓,并沒有執(zhí)行UIControl的action霉涨,這好像跟我們上面預測的手勢的優(yōu)先級比UIControl高是一致的。但是真的是這樣嗎闸迷?我們把自定義的UIControl替換成UIButton嵌纲,其它地方不變,再點擊一次button腥沽,打印結果變成了這樣:

//點擊button
2019-10-15 15:05:02.579222+0800 自定義UICOntrol[16155:5987541] button click
//點擊黃色View
2019-10-15 15:05:25.832473+0800 自定義UICOntrol[16155:5987541] tap

同樣都是繼承于UIControl逮走,這control和control的差別咋就那么大捏?今阳?师溅?
別急,蘋果爸爸已經(jīng)給了我們合理的解釋:

In iOS 6.0 and later, default control actions prevent overlapping gesture recognizer behavior. For example, the default action for a button is a single tap. If you have a single tap gesture recognizer attached to a button’s parent view, and the user taps the button, then the button’s action method receives the touch event instead of the gesture recognizer.This applies only to gesture recognition that overlaps the default action for a control, which includes:
A single finger single tap on a UIButton, UISwitch, UIStepper, UISegmentedControl, and UIPageControl.
A single finger swipe on the knob of a UISlider, in a direction parallel to the slider.
A single finger pan gesture on the knob of a UISwitch, in a direction parallel to the switch.

在iOS6以后盾舌,默認的control actions會阻止與該action操作相同的手勢的識別墓臭。例如:UIButton的默認操作是單擊,如果你在這個button的父視圖上面添加了一個tap手勢妖谴,用戶單擊button窿锉,系統(tǒng)會調(diào)用button的action而不是手勢的action。這種規(guī)則僅僅適用于手勢操作和UIcontrol的默認操作相同的情況下膝舅,包含以下幾種情況:

單擊:UIButton嗡载,UISwitch,UIStepper仍稀,UISegmentedControl 洼滚, UIPageControl,==> 手勢的tap單點操作
滑動:UISlider技潘,==> 手勢的滑動方向與slider平行
拖動:UISwitch遥巴,==> 手勢拖動方向與switch平行

這里提到了兩點:

  1. 第一是手勢和UIControl的默認操作相同,也就是說如果UIControl沒有默認操作(比如我們自定義的UIControl)或者是默認操作和添加的手勢不同享幽,那么手勢識別器的識別優(yōu)先級高铲掐,UIControl不會阻止手勢識別。
  2. 第二是在UIButton的父視圖上添加手勢值桩,如果你把一個添加了手勢的視圖蓋在UIButton上面迹炼,那么UIButton是不能阻止該手勢識別的。這兩點讀者可以自行驗證颠毙。

總結:

① UIGestureRecognizer優(yōu)先級最高, 自定義的UIControl和UIResponder的優(yōu)先級相同斯入。
② 有默認操作的UIControl會阻止添加在父視圖上面的有相同操作的手勢的識別(蘋果封裝好的控件不能不能點擊啊)。

5. tableView父視圖添加tap手勢分析

ViewController的View添加一個tap手勢和一個自定義的tableView,在tableView.m里面重寫touchsBegin方法
ViewController的View里面再添加一個自定義按鈕,并且在自定義按鈕里面重寫touchsBegin方法, 如下圖:


tableView.png

自定義tableView和自定義按鈕都添加如下代碼:

#import "MyTableView.h"
@implementation MyTableView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s調(diào)用", __func__);
    [super touchesBegan:touches withEvent:event];
}
//- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//    NSLog(@"%s調(diào)用", __func__);
//    [super touchesMoved:touches withEvent:event];
//}
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"%s調(diào)用", __func__);
    [super touchesEnded:touches withEvent:event];
}
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event {
    NSLog(@"%s調(diào)用", __func__);
    [super touchesCancelled:touches withEvent:event];
}
@end

控制器代碼如下:

#import "ViewController.h"
#import "MyTableView.h"
#import "Custombtn.h"

@interface ViewController () <UITableViewDelegate,UITableViewDataSource>

@property (nonatomic, strong) MyTableView *securityCenterTableView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(actionTapView)];
    //tap.cancelsTouchesInView = NO;
    [self.view addGestureRecognizer:tap];
    [self.view addSubview:self.securityCenterTableView];
    
    Custombtn *btn = [Custombtn buttonWithType:UIButtonTypeCustom];
    btn.frame = CGRectMake(200, 700, 100, 100);
    btn.backgroundColor = [UIColor blueColor];
    [btn addTarget:self action:@selector(btnClicked) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

- (void)actionTapView {
    NSLog(@"backView的tap手勢調(diào)用");
}

- (void)btnClicked {
    NSLog(@"點擊按鈕");
}

- (MyTableView *)securityCenterTableView {
    if (!_securityCenterTableView) {
        _securityCenterTableView = [[MyTableView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) style:UITableViewStyleGrouped];
        _securityCenterTableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
        _securityCenterTableView.showsVerticalScrollIndicator = NO;
        _securityCenterTableView.backgroundColor = [UIColor whiteColor];
        _securityCenterTableView.delegate = self;
        _securityCenterTableView.dataSource = self;
        _securityCenterTableView.accessibilityIdentifier = @"SecurityCenterTableView";
        _securityCenterTableView.estimatedRowHeight = 0;
        _securityCenterTableView.estimatedSectionFooterHeight = 0;
        _securityCenterTableView.estimatedSectionHeaderHeight = 0;
        if (@available(iOS 11.0, *)) {
            [_securityCenterTableView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
        }
    }
    return _securityCenterTableView;
}

#pragma mark - UITableViewDataSource & UITableViewDelegate

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 10;
}

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *rid = @"SecurityCenterCellIdentify";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:rid];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:rid];
    }
    cell.backgroundColor = [UIColor orangeColor];
    cell.textLabel.text = @"點我";
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"點擊了cell的didSelectRowAtIndexPath方法");
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 60;
}
@end

打印結果如下:
現(xiàn)象一:
快速點擊cell

2019-10-16 10:49:24.677693+0800 tableView點擊[27682:6352478] backView的tap手勢調(diào)用

現(xiàn)象二:
短按cell

2019-10-16 10:49:24.402479+0800 tableView點擊[27682:6352478] -[MyTableView touchesBegan:withEvent:]調(diào)用
2019-10-16 10:49:24.677693+0800 tableView點擊[27682:6352478] backView的tap手勢調(diào)用
2019-10-16 10:49:24.678054+0800 tableView點擊[27682:6352478] -[MyTableView touchesCancelled:withEvent:]調(diào)用

現(xiàn)象三:
長按cell

2019-10-16 10:50:27.551722+0800 tableView點擊[27682:6352478] -[MyTableView touchesBegan:withEvent:]調(diào)用
2019-10-16 10:50:31.244846+0800 tableView點擊[27682:6352478] -[MyTableView touchesEnded:withEvent:]調(diào)用
2019-10-16 10:50:31.245862+0800 tableView點擊[27682:6352478] 點擊了cell的didSelectRowAtIndexPath方法

現(xiàn)象四:
點擊按鈕

2019-10-16 11:27:27.007714+0800 tableView點擊[28595:6393497] -[Custombtn touchesBegan:withEvent:]調(diào)用
2019-10-16 11:27:27.099852+0800 tableView點擊[28595:6393497] -[Custombtn touchesEnded:withEvent:]調(diào)用
2019-10-16 11:27:27.100396+0800 tableView點擊[28595:6393497] btnClicked點擊了按鈕

先看現(xiàn)象二, 短按后蛀蜜,View上的手勢識別器先接收到事件刻两,之后事件傳遞給hit-tested view,作為響應者鏈中一員的myTableView的 touchesBegan:withEvent: 被調(diào)用滴某;而后手勢識別器成功識別了點擊事件磅摹,action執(zhí)行,同時通知Application取消響應鏈中的事件響應霎奢,myTableView的 touchesCancelled:withEvent: 被調(diào)用户誓。
因為事件被取消了,因此Cell無法響應點擊幕侠。

再看現(xiàn)象三帝美,長按cell能夠響應
長按的過程中,一開始事件同樣被傳遞給手勢識別器和hit-tested view晤硕,作為響應鏈中一員的myTableView的 touchesBegan:withEvent: 被調(diào)用悼潭;此后在長按的過程中,手勢識別器一直在識別手勢舞箍,直到一定時間后手勢識別失敗舰褪,才將事件的響應權完全交給響應鏈。當觸摸結束的時候疏橄,myTableView的 touchesEnded:withEvent: 被調(diào)用占拍,同時Cell響應了點擊。

OK捎迫,現(xiàn)在回到現(xiàn)象一晃酒。按照之前的分析,快速點擊cell立砸,講道理不管是表現(xiàn)還是日志都應該和現(xiàn)象二一致才對掖疮。然而日志僅僅打印了手勢識別器的action執(zhí)行結果。分析一下原因:myTableView的 touchesBegan 沒有調(diào)用颗祝,說明事件沒有傳遞給hit-tested view浊闪。那只有一種可能,就是事件被某個手勢識別器攔截了螺戳。目前已知的手勢識別器攔截事件的方法搁宾,就是設置 delaysTouchesBegan 為YES,在手勢識別器未識別完成的情況下不會將事件傳遞給hit-tested view倔幼。然后事實上并沒有進行這樣的設置盖腿,那么問題可能出在別的手勢識別器上。
Window的 sendEvent: 打個斷點查看event上的touch對象維護的手勢識別器數(shù)組:

1510019-786581b9d7281d92.png

捕獲可疑對象:UIScrollViewDelayedTouchesBeganGestureRecognizer ,光看名字就覺得這貨脫不了干系翩腐。從類名上猜測鸟款,這個手勢識別器大概會延遲事件向響應鏈的傳遞。github上找到了該私有類的頭文件

@interface UIScrollViewDelayedTouchesBeganGestureRecognizer : UIGestureRecognizer {
    UIView<UIScrollViewDelayedTouchesBeganGestureRecognizerClient> * _client;
    struct CGPoint { 
        float x; 
        float y; 
    }  _startSceneReferenceLocation;
    UIDelayedAction * _touchDelay;
}
- (void).cxx_destruct;
- (id)_clientView;
- (void)_resetGestureRecognizer;
- (void)clearTimer;
- (void)dealloc;
- (void)sendDelayedTouches;
- (void)sendTouchesShouldBeginForDelayedTouches:(id)arg1;
- (void)sendTouchesShouldBeginForTouches:(id)arg1 withEvent:(id)arg2;
- (void)touchesBegan:(id)arg1 withEvent:(id)arg2;
- (void)touchesCancelled:(id)arg1 withEvent:(id)arg2;
- (void)touchesEnded:(id)arg1 withEvent:(id)arg2;
- (void)touchesMoved:(id)arg1 withEvent:(id)arg2;
@end

有一個_touchDelay變量茂卦,大概是用來控制延遲事件發(fā)送的何什。另外,方法列表里有個 sendTouchesShouldBeginForDelayedTouches: 方法等龙,聽名字似乎是在一段時間延遲后向響應鏈傳遞事件用的处渣。為一探究竟,我創(chuàng)建了一個類hook了這個方法:

//TouchEventHook.m
+ (void)load{
    Class aClass = objc_getClass("UIScrollViewDelayedTouchesBeganGestureRecognizer");
    SEL sel = @selector(hook_sendTouchesShouldBeginForDelayedTouches:);
    Method method = class_getClassMethod([self class], sel);
    class_addMethod(aClass, sel, class_getMethodImplementation([self class], sel), method_getTypeEncoding(method));
    exchangeMethod(aClass, @selector(sendTouchesShouldBeginForDelayedTouches:), sel);
}

- (void)hook_sendTouchesShouldBeginForDelayedTouches:(id)arg1{
    [self hook_sendTouchesShouldBeginForDelayedTouches:arg1];
}

void exchangeMethod(Class aClass, SEL oldSEL, SEL newSEL) {
    Method oldMethod = class_getInstanceMethod(aClass, oldSEL);
    Method newMethod = class_getInstanceMethod(aClass, newSEL);
    method_exchangeImplementations(oldMethod, newMethod);
}

斷點看一下點擊cell后 hook_sendTouchesShouldBeginForDelayedTouches: 調(diào)用時的信息:
1510019-bb69397f67b4eb44.png

可以看到這個手勢識別器的 _touchDelay 變量中蛛砰,保存了一個計時器罐栈,以及一個長得很像延遲時間間隔的變量m_delay。現(xiàn)在泥畅,可以推測該手勢識別器截斷了事件并延遲0.15s才發(fā)送給hit-tested view荠诬。為驗證猜測,我分別在Window的 sendEvent: 涯捻,hook_sendTouchesShouldBeginForDelayedTouches: 以及TableView的 touchesBegan: 中打印時間戳浅妆,若猜測成立,則應當前兩者的調(diào)用時間相差0.15s左右障癌,后兩者的調(diào)用時間很接近凌外。短按Cell后打印結果如下(不能快速點擊,否則還沒過延遲時間觸摸就結束了涛浙,無法驗證猜測):

-[GLWindow sendEvent:]調(diào)用時間戳 :
525252194779.07ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]調(diào)用時間戳 :
525252194930.91ms
-[TouchEventHook hook_sendTouchesShouldBeginForDelayedTouches:]調(diào)用時間戳 :
525252194931.24ms
-[myTableView touchesBegan:withEvent:]調(diào)用時間戳 :
525252194931.76ms

這樣就都解釋得通了康辑。現(xiàn)象一由于點擊后,UIScrollViewDelayedTouchesBeganGestureRecognizer 攔截了事件并延遲了0.15s發(fā)送轿亮。又因為點擊時間比0.15s短疮薇,在發(fā)送事件前觸摸就結束了,因此事件沒有傳遞到hit-tested view我注,導致TableView的 touchBegin 沒有調(diào)用按咒。而現(xiàn)象二,由于短按的時間超過了0.15s但骨,手勢識別器攔截了事件并經(jīng)過0.15s后励七,觸摸還未結束,于是將事件傳遞給了hit-tested view奔缠,使得TableView接收到了事件掠抬。

至于現(xiàn)象四 ,你現(xiàn)在應該已經(jīng)覺得理所當然了才對校哎。

但是現(xiàn)在又有兩個問題了:

  1. 如果想要點擊cell時候tap和cellselected都響應两波,怎么辦?

可以設置tap.cancelsTouchesInView = NO; 如果為YES瞳步,手勢識別了,會取消touch事件腰奋。

設置之后打印如下, 可以發(fā)現(xiàn)都響應了

2019-10-16 16:02:06.473031+0800 tableView點擊[33500:6608421] backView的tap手勢調(diào)用
2019-10-16 16:02:06.476210+0800 tableView點擊[33500:6608421] -[MyTableView touchesBegan:withEvent:]調(diào)用
2019-10-16 16:02:06.477194+0800 tableView點擊[33500:6608421] -[MyTableView touchesEnded:withEvent:]調(diào)用
2019-10-16 16:02:06.479013+0800 tableView點擊[33500:6608421] 點擊了cell的didSelectRowAtIndexPath方法
  1. 如何點擊cell的時候只讓cell的didSelected方法響應单起,不讓tap方法響應。

可以在tap的代理方法里面寫如下代碼, 根據(jù)點擊的是哪個View來讓手勢是否失效

//- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
//    //找到你這個手勢的view氛堕,如果這個view是cell馏臭,那么手勢不響應
//    UIView *view = gestureRecognizer.view;  //gestureRecognizer.view = 永遠獲取的是你這個手勢綁定的view,并不是點擊的View. 所以不用這個方法
//    return NO;
//}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    UIView *view = touch.view;
    //如果點擊的了cell 就返回NO
    //實現(xiàn)點擊cell調(diào)用didselectedCell方法,不調(diào)用tap方法
    return ![view isKindOfClass:NSClassFromString(@"UITableViewCellContentView")];
}

6. scrollView相關分析

① 寫一個簡單的輪播圖

效果圖如下:
簡單輪播圖.png

上面虛線區(qū)域是scrollView的區(qū)域讼稚,圖片區(qū)域是scrollView的x坐標往右移動20px的區(qū)域,我們想讓scrollView可點擊的區(qū)域變成整個屏幕的寬度绕沈,代碼如下:

ViewController.m代碼如下:

#import "EOCEventCase_ScrollView.h"
#import "EOCScrollView.h"
@interface EOCEventCase_ScrollView () {
    NSArray *imageArr;
}
@end

@implementation EOCEventCase_ScrollView

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationItem.title = @"簡單輪播圖";
    imageArr = @[@"0", @"1", @"2", @"3", @"4"];
    [self scrollViewDemo];  
}

- (void)scrollViewDemo {
    NSInteger count = 5;
    //側滑返回不顯示圖片,裁剪掉
    self.view.clipsToBounds = YES; 
    
    EOCScrollView *scrollView = [[EOCScrollView alloc] initWithFrame:CGRectMake(20.f, 100.f, self.view.eocW-60.f, (self.view.eocW-80.f)/2)];
    scrollView.pagingEnabled = YES;
    scrollView.contentSize = CGSizeMake((self.view.eocW-60.f)*5, (self.view.eocW-80.f)/2);
    scrollView.clipsToBounds = NO;  //不自動裁剪,好讓左右都顯示
    [self.view addSubview:scrollView];
    
    // 手勢的互斥 滑動手勢優(yōu)先級大于左側滑返回
    [self.navigationController.interactivePopGestureRecognizer requireGestureRecognizerToFail:scrollView.panGestureRecognizer];
    
    //添加圖片
    for (int i =0; i<count; i++) {
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(20.f+i*(self.view.eocW-60.f), 0.f, self.view.eocW-80.f, (self.view.eocW-80.f)/2)];
        imageView.image = [UIImage imageNamed:imageArr[I]];
        [scrollView addSubview:imageView];
    }
}
@end

自定義的ScrollView代碼如下:

//把scrollView的可點擊區(qū)域變大,左右都可以點擊
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    
    CGRect rect = self.bounds;
    rect.origin.x -= 20.f; //讓響應區(qū)域x變大
    rect.size.width = [UIScreen mainScreen].bounds.size.width;  //讓響應的區(qū)域?qū)挾茸優(yōu)檎麄€屏幕的寬度
    if (CGRectContainsPoint(rect, point)) {
        return YES;
    }
    
    return [super pointInside:point withEvent:event];
}

上圖的虛線區(qū)域是scrollView的區(qū)域锐想,但是我們想讓scrollView的可點擊區(qū)域變?yōu)檎麄€屏幕的寬度,所以重寫scrollView的pointInside:withEvent:方法乍狐,如上赠摇。

② scrollView上添加子視圖

self.view上添加灰色View,灰色View上添加紅色scrollView浅蚪,紅色scrollView上添加白色View藕帜,如下圖:

示例.png

代碼如下:

#import "EOCEventCase_ScrollView.h"
#import "EOCScrollView.h"
#import "EOCLightGrayView.h"
#import "EOCView.h"

@interface EOCEventCase_ScrollView ()
@property(nonatomic, strong)EOCLightGrayView *lightGrayView;
@property(nonatomic, strong)EOCView *customView;
@end

@implementation EOCEventCase_ScrollView

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.lightGrayView];

    EOCScrollView *scrollView = [[EOCScrollView alloc] initWithFrame:self.view.bounds];
    scrollView.contentSize = CGSizeMake(self.view.eocW, 2*self.view.eocH);
    scrollView.backgroundColor = [UIColor redColor];

    //這兩個屬性控制滑動白色View的時候scrollView不動,滑動其他部分scrollView會動
    //類似于scrollView上添加sliderView
    scrollView.delaysContentTouches = NO;
    scrollView.canCancelContentTouches = NO;

    [self.lightGrayView addSubview:scrollView];
    [scrollView addSubview:self.customView];
}

- (EOCView *)customView {
    if (!_customView) {
        _customView = [[EOCView alloc] initWithFrame:CGRectMake(0.f, 100.f, self.view.eocW, (self.view.eocW-60.f)/2)];
        _customView.backgroundColor = [UIColor whiteColor];
    }
    return _customView;
}

- (EOCLightGrayView *)lightGrayView {
    if (!_lightGrayView) {
        CGFloat width = [UIScreen mainScreen].bounds.size.width;
        CGFloat height = [UIScreen mainScreen].bounds.size.height;
        _lightGrayView = [[EOCLightGrayView alloc] initWithFrame:CGRectMake(0.f, 0.f, width, height)];
        _lightGrayView.backgroundColor = [UIColor lightGrayColor];
    }
    return _lightGrayView;
}
@end

上面代碼有兩點可說:

  1. 設置scrollView.delaysContentTouches = NO;和scrollView.canCancelContentTouches = NO;是為了達到惜傲,滑動白色View的時候scrollView不動洽故,滑動其他部分scrollView會動。關于這兩個屬性更詳細的解釋盗誊,可以參考:delaysContentTouches和canCancelContentTouches

  2. 點擊白色View时甚,最后面的lightGrayView不會調(diào)用touchesBegan方法,因為scrollView內(nèi)部給阻止了哈踱,如果想要lightGrayView也調(diào)用touchesBegan方法荒适,可以重寫scrollView的touchesBegan方法,如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"EOCScrollView touchBegan");
    [self.nextResponder touchesBegan:touches withEvent:event];
}

同理:如果我們在scrollView上添加一個slider开镣,如果想要實現(xiàn)滑動slider的時候scrollView不滾動刀诬,也可以設置scrollView.delaysContentTouches = NO;和scrollView.canCancelContentTouches = NO,代碼如下:

#import "EOCEventCase_UISlider.h"

@interface EOCEventCase_UISlider ()

@property(nonatomic, strong)UIScrollView *scrollView;
@property(nonatomic, strong)UISlider *slider;

@end

@implementation EOCEventCase_UISlider

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.navigationItem.title = @"UISlider的事件處理";
    
    [self.view addSubview:self.scrollView];
    [self.scrollView addSubview:self.slider];
    
    //實現(xiàn)滑動sliderView后面的scrollView不滑動
    self.scrollView.delaysContentTouches = NO;
    self.scrollView.canCancelContentTouches = NO;
}

#pragma mark - event response
- (void)valueChange
{
    NSLog(@"valueChange");
}

#pragma mark - getter方法
- (UIScrollView *)scrollView
{
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0.f, 0.f, self.view.eocW, self.view.eocH)];
        _scrollView.backgroundColor = [UIColor lightGrayColor];
        _scrollView.contentSize = CGSizeMake(2*self.view.eocW, self.view.eocH);
    }
    return _scrollView;
}

- (UISlider *)slider {
    
    if (!_slider) {
        _slider = [[UISlider alloc] initWithFrame:CGRectMake(50.f, 100.f, 200.f, 40.f)];
        [_slider addTarget:self action:@selector(valueChange) forControlEvents:UIControlEventValueChanged];
    }
    return _slider;
}
@end

四. 觸摸事件的生命周期

看完本文和上一篇文章iOS觸摸事件應該就能理解觸摸事件的生命周期了邪财。
手指觸摸屏幕陕壹,觸摸事件的傳遞大概經(jīng)歷了3個階段,系統(tǒng)響應階段-->SpringBoard.app處理階段-->前臺App處理階段卧蜓,大致的流程如下圖:

觸摸事件的生命周期.png

起始:cpu處于睡眠階段帐要,等待事件發(fā)生,然后手指觸摸屏幕

1. 系統(tǒng)響應階段

  1. 屏幕感應到觸摸事件弥奸,并將感應到的事件傳遞給IOKit(用來操作硬件和驅(qū)動的框架榨惠,這是一個私有API,知道這個是干嘛的就行了)。
  2. IOKit.framework封裝整個觸摸事件為IOHIDEvent對象赠橙,直接通過mach port轉(zhuǎn)發(fā)給SpringBoard.app耽装。(Mach屬于硬件層,僅提供了諸如處理器調(diào)度期揪、IPC進程通信等非常少量的基礎服務掉奄。在Mach中,所有的東西都是通過自己的對象實現(xiàn)的凤薛,進程浊猾、線程和虛擬內(nèi)存都被稱為“對象”,Mach的對象間不能直接調(diào)用幕与,只能通過消息傳遞的方式實現(xiàn)對象間的通信坛怪。消息是Mach中最基礎的概念,消息在兩個端口(port)之間傳遞活玲,mach port就是IPC進程間通信的核心涣狗。更多內(nèi)容請查看這篇文章)。

2. SpringBoard.app處理階段

  1. SpringBoard.app的主線程Runloop收到IOKit.framework轉(zhuǎn)發(fā)來的消息蘇醒舒憾,并觸發(fā)對應mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()镀钓。
  2. 如果SpringBoard.app監(jiān)測到有App在前臺(記為xxx.app),SpringBoard.app再通過mach port轉(zhuǎn)發(fā)給xxx.app镀迂,如果SpringBoard.app監(jiān)測到前臺沒有App運行丁溅,則SpringBoard.app進入App內(nèi)部響應階段,觸發(fā)自身主線程runloop的Source0時間源的回調(diào)招拙。

SpringBoard.app是一個系統(tǒng)進程唧瘾,可以理解為桌面系統(tǒng),可以統(tǒng)一管理和分發(fā)系統(tǒng)接收到的觸摸事件别凤。

3. App內(nèi)部響應階段

  1. 前臺App主線程Runloop收到SpringBoard.app轉(zhuǎn)發(fā)來的消息而蘇醒饰序,并觸發(fā)對應mach port的Source1回調(diào)__IOHIDEventSystemClientQueueCallback()。
  2. Source1回調(diào)內(nèi)部规哪,觸發(fā)Source0回調(diào)__UIApplicationHandleEventQueue()求豫。
  3. Source0回調(diào)內(nèi)部,封裝IOHIDEvent為UIEvent诉稍。
  4. Source0回調(diào)內(nèi)部蝠嘉,調(diào)用UIApplication的sendEvent:方法,將UIEvent傳給UIWindow杯巨,接下來就是尋找最佳響應者的過程蚤告,也就是命中測試hit-testing。
  5. 尋找到最佳響應者后服爷,接下來就是事件在響應鏈中的傳遞和響應了杜恰。需要注意的是获诈,事件除了可以被響應者處理之外,還有可能被手勢識別器或者target-action捕捉并處理心褐,這涉及到一個優(yōu)先級的問題(文章前面已經(jīng)解釋過了)舔涎,如果觸摸事件在響應鏈中沒有找到能夠響應該事件的對象,最終將被釋放逗爹。
  6. 事件被處理或者釋放之后亡嫌,runloop如果沒有其他事件進行處理,將會再次進入休眠狀態(tài)掘而。

Demo地址:https://github.com/iamkata/gesture

本文部分參考以下鏈接挟冠,如有侵權請聯(lián)系刪除。
iOS觸摸事件處理
iOS觸摸事件全家桶

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末镣屹,一起剝皮案震驚了整個濱河市圃郊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌女蜈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件色瘩,死亡現(xiàn)場離奇詭異伪窖,居然都是意外死亡,警方通過查閱死者的電腦和手機居兆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門覆山,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人泥栖,你說我怎么就攤上這事簇宽。” “怎么了吧享?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵魏割,是天一觀的道長。 經(jīng)常有香客問我钢颂,道長钞它,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任殊鞭,我火速辦了婚禮遭垛,結果婚禮上,老公的妹妹穿的比我還像新娘操灿。我一直安慰自己锯仪,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布趾盐。 她就那樣靜靜地躺著庶喜,像睡著了一般小腊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上溃卡,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天溢豆,我揣著相機與錄音,去河邊找鬼瘸羡。 笑死漩仙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的犹赖。 我是一名探鬼主播队他,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼峻村!你這毒婦竟也來了麸折?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤粘昨,失蹤者是張志新(化名)和其女友劉穎垢啼,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體张肾,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡芭析,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吞瞪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片馁启。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芍秆,靈堂內(nèi)的尸體忽然破棺而出惯疙,到底是詐尸還是另有隱情,我是刑警寧澤妖啥,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布霉颠,位于F島的核電站,受9級特大地震影響迹栓,放射性物質(zhì)發(fā)生泄漏掉分。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一克伊、第九天 我趴在偏房一處隱蔽的房頂上張望酥郭。 院中可真熱鬧,春花似錦愿吹、人聲如沸不从。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椿息。三九已至歹袁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間寝优,已是汗流浹背条舔。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留乏矾,地道東北人孟抗。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像钻心,于是被迫代替她去往敵國和親凄硼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355