UIView与CALayer的关系

image.png

UIView:专门负责提供显示内容和事件传递与视图响应的
UILayer负责UI视图显示,
这样设计就体现了6大设计原则中的单一职责原则。


UITouch

UITouch对象
  • 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
  • 一根手指对应一个UITouch对象

UITouch作用

  • 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
  • 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触 摸位置
  • 当手指离开屏幕时,系统会销毁相应的UITouch对象
  • 一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
  • 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
  • 每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
  • 手指离开屏幕一段时间后,确定该UITouch对象不会再被更新将被释放。

_

  1. //触摸的各个阶段状态
  2. //例如当手指移动时,会更新phase属性到UITouchPhaseMoved;手指离屏后,更新到UITouchPhaseEnded
  3. typedef NS_ENUM(NSInteger, UITouchPhase) {
  4. UITouchPhaseBegan, // whenever a finger touches the surface.
  5. UITouchPhaseMoved, // whenever a finger moves on the surface.
  6. UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
  7. UITouchPhaseEnded, // whenever a finger leaves the surface.
  8. UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
  9. };

**

  1. //触摸产生时所处的窗口
  2. @property(nonatomic,readonly,retain) UIWindow *window;
  3. //触摸产生时所处的视图
  4. @property(nonatomic,readonly,retain) UIView *view;
  5. //短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
  6. @property(nonatomic,readonly) NSUInteger tapCount;
  7. //记录了触摸事件产生或变化时的时间,单位是秒
  8. @property(nonatomic,readonly) NSTimeInterval timestamp;
  9. //当前触摸事件所处的状态
  10. @property(nonatomic,readonly) UITouchPhase phase;
  11. //获取手指与屏幕的接触半径 IOS8以后可用 只读
  12. @property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
  13. //获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
  14. @property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
  15. //获取触摸手势
  16. @property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);
  17. //获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
  18. @property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
  19. //获取最大触摸压力值
  20. @property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
  21. //取得在指定视图的位置
  22. // 返回值表示触摸在view上的位置
  23. // 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
  24. // 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
  25. - (CGPoint)locationInView:(nullable UIView *)view;
  26. //该方法记录了前一个触摸点的位置
  27. - (CGPoint)previousLocationInView:(nullable UIView *)view;

什么样的对象才能处理触摸事件呢?只有继承UIResponder的类或者子类才可以响应和处理事件
首先看一下UIView,UIVIewController,UIApplication的继承,都有UIResponder这个类,都有那些事件处理呢?

  1. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
  2. - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
  3. - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
  4. - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
  5. - (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
  6. - (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
  7. - (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
  8. - (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
  9. - (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
  10. - (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
  11. - (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
  12. - (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
  13. - (void)remoteControlReceivedWithEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(4_0);
  • touch触摸事件
  • presss指压事件 (iOS 9 Deep press的相关方法)
  • motion加速计事件 (给陀螺仪和加速传感器使用的方法)
  • remote远程控制事件 (比如耳机控制)
  • 编辑菜单消息事件(editing menu messages)


UITouch与GestureRecognizer的区别?
系统内置的手势事件是对UIResponder touch事件的监测封装,通过不同的计算得出是否触发了某个手势,而根据不同的手势,触发的时机也不同**。


UIEvent

UIEvent:称为事件对象,记录事件产生的时刻和类型
当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信,事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。每产生一个事件,就会产生一个UIEvent对象,发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中为什么是队列而不是栈?
因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

  1. //事件类型
  2. @property(nonatomic,readonly) UIEventType type;
  3. //事件子类型
  4. @property(nonatomic,readonly) UIEventSubtype subtype;
  5. //事件产生的时间
  6. @property(nonatomic,readonly) NSTimeInterval timestamp;
  7. //返回值:返回与接收器相关联的所有触摸对象。
  8. - (nullable NSSet <UITouch *> *)allTouches;
  9. // 返回值:返回属于一个给定视图的触摸对象,用于表示由接收器所表示的事件。
  10. - (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
  11. //返回值:返回属于一个给定窗口的接收器的事件响应的触摸对象。
  12. - (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
  13. //返回值:返回触摸对象被传递到特殊手势识别
  14. - (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture


  1. //开始接触屏幕,就会调用一次
  2. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
  3. //手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
  4. - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
  5. //手指离开屏幕时,调用一次
  6. - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
  7. //触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
  8. - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
  • touches 里包含了一个或多UITouch对象,也即一个或多个手指同时触摸view,因此touches.count就是触摸的点数,是1就是单点触摸,大于1就是多点触摸。
  • 一次触摸过程中,只会产生一个事件对象,4个触摸方法都是同一个event参数,如果两根手指同时触摸一个view,那就是一个事件,view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
  • 如果这两根手指一前一后分开触摸同一个view,那就是两个事件,view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

事件的生命周期

当指尖触碰屏幕的那一刻,一个触摸事件就在系统中生成了。经过IPC进程间通信(进程间通信(IPC,Inter-Process Communication)),事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图:
image.png

事件传递机制

image.png

事件出发后会封装成UIEvent然后放入UIApplication管理的队列中(先进先出),UIApplication会从最前面取出事件,将它分发下去
其中,与传递响应相关的,UIView中有两个重要的方法: 1、-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 有事件传递给一个控件,控件就会触发这个方法 ,然后通过下面方法判断是否在范围内,返回NO则忽略整个view,

  • 通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

**2、-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event** 重写方法上面的方法来实现一些功能,要想重写都要自定义View(像扩大点击事件范围,点击超过父VIew范围) 有三个方法判断是否接受事件

  • userInteractionEnabled (默认view是YES ,表示接受事件)
  • alpha 透明度 (< 0.01不接受)
  • hidden 隐藏

hitTest:withEvent:系统实现原理

image.png 首先点击屏幕,事件传递给UIApplication,然后传给UIWindow, 然后UIWindow通过hitTest方法内部进行倒序遍历子视图, 假如子视图可以正常响应手势,就判断触摸点是否在子视图内,假如在的话,判断子视图内是否还有子视图,有的话,就递归查找,直到找到一个合适的视图来处理;假如没找到就让该子视图响应,假如都找不到的话就让window响应 使用场景: 1、扩大视图view的响应热区 2、缩小或指定view区域失去响应. 3、接触到的实际场景:地图撑开视图。扩大撑开视图的响应区。 4、超出父VIew的点击事件


演示代码:扩大按钮的点击响应区**

  1. - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  2. CGRect bounds = self.bounds;
  3. bounds = CGRectInset(bounds, -100, -100);
  4. return CGRectContainsPoint(bounds, point);
  5. }

**演示代码:超出父VIew的点击事件

image.png 前提:黄色是父VIew 加了一个红色的子view和UILabel,想要触发红View的点击事件 思路:根据point位置,判断子视图是否包含,有就返回响应的子视图

  1. -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
  2. if (!self.userInteractionEnabled || self.isHidden == YES || self.alpha <= 0.01) {
  3. return nil;
  4. }
  5. for (UIView *subview in [self.subviews reverseObjectEnumerator]) { //倒序遍历
  6. CGPoint newPoint = [subview convertPoint:point fromView:self]; //self中point转换成subview中坐标点
  7. // CGPoint newPoint = [subview convertPoint:point toView:self]; subview中point转换成self中坐标点
  8. UIView *testHitView = [subview hitTest:newPoint withEvent:event];
  9. if (testHitView) {
  10. return testHitView; //红色子View
  11. }
  12. }
  13. return nil;
  14. }
  15. - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
  16. //规则的矩形
  17. return CGRectContainsPoint(newBounds, point); // newBounds为想要的触发范围
  18. //圆形
  19. return pointToCenterDistance < halfRadius; // point到圆心的距离小于半径
  20. }

视图响应链机制

image.png 假如当前响应者(控件)不处理这个事件,就把这个事件响应交给父视图, 直到Window,UIApplication,UIApplicationDelegate,假如都不响应的话,忽略该事件. 经典使用场景: 根据响应链找到合适的视图进行处理某些事件,比如找到视图控制器viewController 或者 某个指定类型的视图

image.png
image.png

iOS触摸事件全家桶

手势

UiGestureRecognizer是一个父类,而实际操作中我们要使用它的子类,较为常用的有以下几种:

UITapGestureRecognizer 轻拍手势
UISwipeGestureRecognizer 轻扫手势
UILongPressGestureRecognizer 长按手势
UIPanGestureRecognizer 平移手势
UIPinchGestureRecognizer 捏合(缩放)手势
UIRotationGestureRecognizer 旋转手势
UIScreenEdgePanGestureRecognizer 屏幕边缘平移

UITapGestureRecognizer(点击手势)

  1. //创建手势 使用initWithTarget:action:的方法创建
  2. {
  3. UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapView:)];
  4. //设置属性
  5. //tap 手势一共两个属性,一个是设置轻拍次数,一个是设置点击手指个数
  6. //设置轻拍次数
  7. tap.numberOfTapsRequired = 2;
  8. //设置手指字数
  9. tap.numberOfTouchesRequired = 2;
  10. //别忘了添加到testView上
  11. [_testView addGestureRecognizer:tap];
  12. }
  13. -(void)tapView:(UITapGestureRecognizer *)sender{
  14. //设置轻拍事件改变testView的颜色
  15. _testView.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];
  16. }

UISwipeGestureRecognizer (滑动手势)

  1. {
  2. //创建手势
  3. UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeView:)];
  4. //设置属性,swipe也是有两种属性设置手指个数及轻扫方向
  5. swipe.numberOfTouchesRequired = 2;
  6. //设置轻扫方向(默认是从左往右)
  7. //direction是一个枚举值有四个选项,我们可以设置从左往右,从右往左,从下往上以及从上往下
  8. //设置轻扫方向(默认是从左往右)
  9. swipe.direction = UISwipeGestureRecognizerDirectionLeft;
  10. // [_redView addGestureRecognizer:swipe];
  11. }
  12. #pragma mark swipe轻扫手势事件
  13. -(void)swipeView:(UISwipeGestureRecognizer *)sender{
  14. _testView.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];
  15. }

UILongPressGestureRecognizer(长按手势)

UIPanGestureRecognizer(平移手势)

UIScreenEdgePanGestureRecognizer(屏幕边缘平移手势)

UIPinchGestureRecognizer(捏合手势)

UIRotationGestureRecognizer(旋转手势)