博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
iOS —— 触摸事件传递及响应与手势
阅读量:6411 次
发布时间:2019-06-23

本文共 10428 字,大约阅读时间需要 34 分钟。

iOS 的事件分为三种,触摸事件(Touch Event)加速器事件(Motion Events)远程遥控事件(Remote Events)。这些事件对应的类为UIResponder。本文只探究触摸事件。

Tip:在模拟器中,按住option可以两根手指操作。同时按住option+shift可以移动两根手指。


触摸事件的处理

触摸事件可以分为两部分——传递和响应。

传递:系统把该事件传到最适合响应的对象。

响应:最适合响应的对象可以不响应,转给别的对象响应。

传递

我们偶尔会遇到显示一个小列表选择。这时候小列表超出了父view的范围。这时点击B是达不到预期效果的。

下面还是通过一份Demo来学习。

如图,redView是yellowView的父视图,redView加到控制器的view上。触摸点在A区域,红色view响应;触摸点在B区域,黄色view响应;触摸点在C区域,默认黄色view是不响应的。我们来实现让黄色view响应C区域事件。

传递步骤:

(有个有趣的地方,UIApplication和AppDelegate也继承于UIResponder)

简单地说,自下而上。AppDelegate -> UIApplication -> UIWindow -> UIViewController -> UIView(父view一直遍历到子view,同层的view按后添加的view先遍历)。其遵循的规则如下:

  1. 自己是否能接收触摸事件? 不能接收的情况有三种 一、userInteractionEnabled = NO 二、 hidden = YES 三、 alpha = 0.0 ~ 0.01
  2. 触摸点是否在自己身上?
  3. 从后往前遍历子控件,重复前两个步骤。 若父控件不能接收触摸事件,不会传递给子控件。
  4. 如果没有符合条件的子控件,那么就自己最适合处理。

在https://juejin.im/post/5b614924f265da0f774ac7be借了两张图能更直观地理解传递过程。

当事件传递给当前view时,当前view会调用- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法。寻找最适合的view。

返回谁,谁就是最合适的view。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {    // 如果在这里直接返回yellowView    // 区域A的事件也会由B响应    // 所以这里还是直接调用父类方法    return [super hitTest:point withEvent:event];    // 本Demo中,甚至控制器view的点击事件也会被B响应,因为控制器view会遍历子控件最后一个(红色view),红色view调用这方法返回yellowView}复制代码

Demo中,点击了C区域,传递给控制器view后,满足规则1.2,然后根据规则3传递给红色view。红色view满足规则1,但不满足规则2所以不符合。且这时控制器view没有其它子view(规则4),最终控制器view成为最适合的view。

因此我们还要修改触摸点是否在自己身上的方法(红色view的规则2),来让事件传递给黄色view。

在红色view中实现该方法后,就能满足规则2,从而把事件传递给黄色view。

// 红色view.m中// 红色view也引用着黄色view@implementation FatherView- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {    // 调用此方法的原因:红色view和黄色view的坐标系不一致    // 它们的坐标不是按控制器view的坐标    // 而是各自的坐标系    CGPoint yellowViewPoint = [self convertPoint:point toView:self.yellowView];        if ([self.yellowView pointInside:yellowViewPoint withEvent:event]) {        return YES;    }        return [super pointInside:point withEvent:event];}@end复制代码

最终,红色view满足规则2后,根据规则3遍历黄色view。黄色view能满足规则1.2,且没有更适合的子控件(规则4),所以成为了最适合的view。Demo的目的也就达成了。


响应

先了解有关的类,然后通过一个Demo来熟悉它们的使用。

UIResponder

@interface UIResponder : NSObject 
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;- (nullable UIResponder*)nextResponder;@property(nonatomic, readonly) BOOL canBecomeFirstResponder; // default is NO- (BOOL)canBecomeFirstResponder; // default is NO- (BOOL)becomeFirstResponder;@property(nonatomic, readonly) BOOL canResignFirstResponder; // default is YES- (BOOL)canResignFirstResponder; // default is YES- (BOOL)resignFirstResponder;@property(nonatomic, readonly) BOOL isFirstResponder;- (BOOL)isFirstResponder;// 触摸事件方法// 手指触摸- (void)touchesBegan:(NSSet
*)touches withEvent:(nullable UIEvent *)event;// 触摸时移动- (void)touchesMoved:(NSSet
*)touches withEvent:(nullable UIEvent *)event;// 手指离开屏幕- (void)touchesEnded:(NSSet
*)touches withEvent:(nullable UIEvent *)event;// 触摸状态下被系统事件(如电话等打断)- (void)touchesCancelled:(NSSet
*)touches withEvent:(nullable UIEvent *)event;- (void)touchesEstimatedPropertiesUpdated:(NSSet
*)touches NS_AVAILABLE_IOS(9_1);@end复制代码

触摸事件方法中有两个参数(NSSet<UITouch *> *)touches和(UIEvent *)event。

UITouch

  • UITouch对象记录 触摸的位置、时间、阶段。
  • 一根手指对应一个UITouch对象。
  • 手指移动时,系统会更新同一个UITouch对象。
  • 手指离开屏幕时,UITouch对象被销毁。
@interface UITouch : NSObject// 触摸产生时所处的窗口@property (nonatomic, readonly, retain) UIWindow *window;// 触摸产生时所处的视图@property (nonatomic, readonly, retain) UIView *view;// 短时间内点按屏幕的次数@property (nonatomic, readonly) NSUInteger tapCount;// 记录了触摸事件产生或变化的时间,单位:秒@property (nonatomic, readonly) NSTimeInterval timestamp;// 当前触摸事件所处的状态@property (nonatomic, readonly) UITouchPhase phase;typedef NS_ENUM(NSInteger, UITouchPhase) {    UITouchPhaseBegan,             //(触摸开始)    UITouchPhaseMoved,             // (接触点移动)    UITouchPhaseStationary,        // (接触点无移动)    UITouchPhaseEnded,             // (触摸结束)    UITouchPhaseCancelled,         // (触摸取消)};// 返回触摸在view上的位置// 相对view的坐标系// 如果参数为nil,返回的是在UIWindow的位置- (CGPoint)locationInView:(nullable UIView *)view;// 返回上一个触摸点的位置- (CGPoint)previousLocationInView:(nullable UIView *)view;@end复制代码

UIEvent

每产生一个事件,就会产生一个UIEvent对象。记录事件产生的时刻和类型。本文探究的都是触摸事件。

@interface UIEvent : NSObject// 事件类型@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);@property(nonatomic,readonly) UIEventSubtype  subtype NS_AVAILABLE_IOS(3_0);// 事件产生的事件@property(nonatomic,readonly) NSTimeInterval  timestamp;@end复制代码

接下来通过一个Demo来熟悉上面提到的类。

新建一个view,在.m文件中打入以下代码,手指拖拽着该view移动。

- (void)touchesMoved:(NSSet
*)touches withEvent:(nullable UIEvent *)event { NSLog(@"%s", __func__); // 因为只有一根手指,所以用anyObject UITouch *touch = [touches anyObject]; // 获取上一点 CGPoint previousPoint = [touch previousLocationInView:self]; // 获取当前点 CGPoint currentPoint = [touch locationInView:self]; // 计算偏移量 CGFloat offsetX = currentPoint.x - previousPoint.x; CGFloat offsetY = currentPoint.y - previousPoint.y; // view平移 self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);}复制代码

响应过程

  • 响应链

简单地说,传递到最合适的view后,如果有实现touches方法那么就由此 View 响应,如果没有实现,那么就会自下而上,传递给他的下一个响应者【子view -> 父view,控制器view -> 控制器-> UIWindow -> UIApplication -> AppDelegate】。

由这两张图,我们就可以知道每个UIResponder对象nextResponder指向谁。


通过touches方法,虽然能实现响应触摸事件,但对开发还是不友好,原因有以下三个:

  1. 要自定义view。
  2. 还要在实现文件中实现touches方法,由此在让外部监听到实现文件中的触摸事件,增强了耦合度。
  3. 不容易区分用户的具体手势行为。实现长按手势都能折腾。

所以苹果推出了UIGestureRecognizer手势识别器。对常用的手势进行了封装。

手势

手势识别和触摸事件是两个独立的概念。

UIGestureRecognizer简介

typedef NS_ENUM(NSInteger, UIGestureRecognizerState) {    UIGestureRecognizerStatePossible,         UIGestureRecognizerStateBegan,    UIGestureRecognizerStateChanged,     UIGestureRecognizerStateEnded,     UIGestureRecognizerStateCancelled,     UIGestureRecognizerStateFailed,        UIGestureRecognizerStateRecognized =       UIGestureRecognizerStateEnded};@interface UIGestureRecognizer : NSObject@property(nonatomic,readonly) UIGestureRecognizerState state;@property(nullable,nonatomic,weak) id 
delegate;@end复制代码

UIGestureRecognizer是一个抽象类,使用它的子类才能处理具体的手势。

UITapGestureRecognizer(敲击)UILongPressGestureRecognizer(长按)UISwipeGestureRecognizer(轻扫)UIRotationGestureRecognizer(旋转)UIPinchGestureRecognizer(捏合,用于缩放)UIPanGestureRecognizer(拖拽)复制代码

手势的使用

  • 点按手势
UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap:)];        [self.view addGestureRecognizer:tapGes];复制代码
  • 长按手势。
-  (void)viewDidLoad {  [super viewDidLoad];    // 创建手势  UITapGestureRecognizer *longPressGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGes:)];  // 添加手势  [view addGestureRecognizer:longPressGes];}// 长按手势分状态,长按移动时,也会调用- (void)longPressGes:(UILongPressGestureRecoginzer *)longPressGes {     if (longPressGes.state == UIGestureRecognizerStateBegan) {// 长按开始    } else if (longPressGes.state == UIGestureRecognizerStateChanged) {// 长按移动    } else if (longPressGes.state == UIGestureRecognizerStateEnded) {// 长按结束    }复制代码
  • 轻扫手势 默认是向右轻扫手势。如果要向左轻扫,需要设置轻扫方向。
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeGes:)];    //注意点:一个轻扫手势只能对应一个方向,不要用或。    // 要多个方向就创建多个手势。    swipe.direction = UISwipeGestureRecognizerDirectionLeft;/*  typedef NS_OPTIONS(NSUInteger, UISwipeGestureRecognizerDirection) {    UISwipeGestureRecognizerDirectionRight = 1 << 0,    UISwipeGestureRecognizerDirectionLeft  = 1 << 1,    UISwipeGestureRecognizerDirectionUp    = 1 << 2,    UISwipeGestureRecognizerDirectionDown  = 1 << 3};*/        [self.view addGestureRecognizer:swipe];复制代码
  • 拖拽手势

上面的Demo中提到的平移,需要获取上一个点和当前点计算偏移量。拖拽手势内部有方法能直接获取相对于最原始的点的偏移量。

- (void)viewDidLoad {    [super viewDidLoad];        UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];        [self.view addGestureRecognizer:pan];}- (void)pan:(UIPanGestureRecognizer *)pan {    // 获取偏移量    CGPoint transP = [pan translationInView:self.view];    self.view.transform = CGAffineTransformTranslate(self.view, transP.x, transP.y);        // 清0    [pan setTranslation:CGPointMake(0, 0) inView:self.view];}复制代码
  • 旋转手势

同理,旋转手势内部有方法能直接获取相对于最原始的点的旋转量。

- (void)viewDidLoad {    [super viewDidLoad];        UIRotationGestureRecognizer *rotation = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:@selector(rotation:)];        [self.view addGestureRecognizer:rotation];}- (void)rotation:(UIRotationGestureRecognizer *)rotationGes {        // 获取旋转角度(已经是弧度)    CGFloat rotation = rotationGes.rotation;        self.view.transform = CGAffineTransformRotate(self.view.transform, rotation);        // 清0    [rotationGes setRotation:0.f];    }复制代码
  • 捏合手势

同理,捏合手势内部有方法能直接获取相对于最原始的缩放比例。

- (void)viewDidLoad {    [super viewDidLoad];        UIPinchGestureRecognizer *rotation = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];        [self.view addGestureRecognizer:rotation];}- (void)pinch:(UIPinchGestureRecognizer *)pinchGes {        // 放大,缩小    CGFloat scale = pinchGes.scale;    self.view.transform = CGAffineTransformScale(self.view.transform, scale, scale);        // 清0    [pinchGes setScale:0];    }复制代码

手势的常用代理方法

// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch// 在touchesBegan之前,是否允许该手势接收事件- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;// called before pressesBegan:withEvent: is called on the gesture recognizer for a new press. return NO to prevent the gesture recognizer from seeing this press// 在touchesBegan之前,是否允许该手势接收事件- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press;// 是否允许同时支持多个手势- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer;复制代码

大家应该试过视频左边手势调亮度,右边调音量。就可以在代理方法中实现。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {  // 获取当前的点  CGPoint curP = [touch locationInView:view];  // 判断在左边还是右边  if (curP.x > view.bounds.size.width * 0.5) {// 在左边  } else {// 在右边  }  return YES;}复制代码

手势默认是不能同时进行的(例如上面的旋转和捏合手势),如果要同时识别,需要实现代理方法。

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {    return YES;}复制代码

参考

转载地址:http://mkkra.baihongyu.com/

你可能感兴趣的文章
python序列化数据本地存放
查看>>
#CCNA#IP地址、子网划分参考资料网址
查看>>
比较不错的图片上传插件
查看>>
判偶不判奇
查看>>
Sequelize 数据库的支持
查看>>
BigDecimal类的加减乘除
查看>>
node.js发送邮件email
查看>>
查看nginx配置文件路径的方法
查看>>
接口性能调优方案探索
查看>>
kali安装包或更新时提示“E: Sub-process /usr/bin/dpkg return”
查看>>
网站管理后台模板 Charisma
查看>>
EL:empty的用法
查看>>
Saltstack配置之 nodegroups
查看>>
Servlet和JSP优化经验总结
查看>>
squid使用rotate轮询(分割)日志
查看>>
VS2015安装EF Power Tools
查看>>
MySQL主从复制(笔记)
查看>>
keepalived高可用集群的简单配置
查看>>
Android Java Framework显示Toast(无Activity和Service)
查看>>
通过 SignalR 类库,实现 ASP.NET MVC 的实时通信
查看>>