UIView与CALayer的关系
UIView:专门负责提供显示内容和事件传递与视图响应的
UILayer负责UI视图显示,
这样设计就体现了6大设计原则中的单一职责原则。
UITouch
UITouch对象
- 当用户用一根手指触摸屏幕时,会创建一个与手指相关联的UITouch对象
- 一根手指对应一个UITouch对象
UITouch作用
- 保存着跟手指相关的信息,比如触摸的位置、时间、阶段
- 当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触 摸位置
- 当手指离开屏幕时,系统会销毁相应的UITouch对象
- 一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
- 多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
- 每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
- 手指离开屏幕一段时间后,确定该UITouch对象不会再被更新将被释放。
_
//触摸的各个阶段状态
//例如当手指移动时,会更新phase属性到UITouchPhaseMoved;手指离屏后,更新到UITouchPhaseEnded
typedef NS_ENUM(NSInteger, UITouchPhase) {
UITouchPhaseBegan, // whenever a finger touches the surface.
UITouchPhaseMoved, // whenever a finger moves on the surface.
UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event.
UITouchPhaseEnded, // whenever a finger leaves the surface.
UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face)
};
**
//触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
//触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view;
//短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
//记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
//当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
//获取手指与屏幕的接触半径 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
//获取手指与屏幕的接触半径的误差 IOS8以后可用 只读
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
//获取触摸手势
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);
//获取触摸压力值,一般的压力感应值为1.0 IOS9 只读
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
//获取最大触摸压力值
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
//取得在指定视图的位置
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0,0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
- (CGPoint)locationInView:(nullable UIView *)view;
//该方法记录了前一个触摸点的位置
- (CGPoint)previousLocationInView:(nullable UIView *)view;
什么样的对象才能处理触摸事件呢?只有继承UIResponder的类或者子类才可以响应和处理事件
首先看一下UIView
,UIVIewController
,UIApplication
的继承,都有UIResponder这个类,都有那些事件处理呢?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesChanged:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)pressesCancelled:(NSSet<UIPress *> *)presses withEvent:(nullable UIPressesEvent *)event NS_AVAILABLE_IOS(9_0);
- (void)motionBegan:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionEnded:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(nullable UIEvent *)event NS_AVAILABLE_IOS(3_0);
- (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管理的事件队列中为什么是队列而不是栈?
因为队列的特点是先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
//事件类型
@property(nonatomic,readonly) UIEventType type;
//事件子类型
@property(nonatomic,readonly) UIEventSubtype subtype;
//事件产生的时间
@property(nonatomic,readonly) NSTimeInterval timestamp;
//返回值:返回与接收器相关联的所有触摸对象。
- (nullable NSSet <UITouch *> *)allTouches;
// 返回值:返回属于一个给定视图的触摸对象,用于表示由接收器所表示的事件。
- (nullable NSSet <UITouch *> *)touchesForView:(UIView *)view;
//返回值:返回属于一个给定窗口的接收器的事件响应的触摸对象。
- (nullable NSSet <UITouch *> *)touchesForWindow:(UIWindow *)window;
//返回值:返回触摸对象被传递到特殊手势识别
- (nullable NSSet <UITouch *> *)touchesForGestureRecognizer:(UIGestureRecognizer *)gesture
//开始接触屏幕,就会调用一次
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
//手指开始移动就会调用(这个方法会频繁的调用,其实一接触屏幕就会多次调用)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
//手指离开屏幕时,调用一次
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
//触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,或者view上面添加手势时,系统会自动调用view的下面方法
- (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)),事件最终被传递到了合适的应用。在应用内历经峰回路转的奇幻之旅后,最终被释放。大致经过如下图:
事件传递机制
事件出发后会封装成
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:系统实现原理
首先点击屏幕,事件传递给UIApplication,然后传给UIWindow, 然后UIWindow通过hitTest方法内部进行倒序遍历子视图, 假如子视图可以正常响应手势,就判断触摸点是否在子视图内,假如在的话,判断子视图内是否还有子视图,有的话,就递归查找,直到找到一个合适的视图来处理;假如没找到就让该子视图响应,假如都找不到的话就让window响应 使用场景: 1、扩大视图view的响应热区 2、缩小或指定view区域失去响应. 3、接触到的实际场景:地图撑开视图。扩大撑开视图的响应区。 4、超出父VIew的点击事件
演示代码:扩大按钮的点击响应区**
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGRect bounds = self.bounds;
bounds = CGRectInset(bounds, -100, -100);
return CGRectContainsPoint(bounds, point);
}
**演示代码:超出父VIew的点击事件
前提:黄色是父VIew 加了一个红色的子view和UILabel,想要触发红View的点击事件 思路:根据point位置,判断子视图是否包含,有就返回响应的子视图
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if (!self.userInteractionEnabled || self.isHidden == YES || self.alpha <= 0.01) {
return nil;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) { //倒序遍历
CGPoint newPoint = [subview convertPoint:point fromView:self]; //self中point转换成subview中坐标点
// CGPoint newPoint = [subview convertPoint:point toView:self]; subview中point转换成self中坐标点
UIView *testHitView = [subview hitTest:newPoint withEvent:event];
if (testHitView) {
return testHitView; //红色子View
}
}
return nil;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
//规则的矩形
return CGRectContainsPoint(newBounds, point); // newBounds为想要的触发范围
//圆形
return pointToCenterDistance < halfRadius; // point到圆心的距离小于半径
}
视图响应链机制
假如当前响应者(控件)不处理这个事件,就把这个事件响应交给父视图, 直到Window,UIApplication,UIApplicationDelegate,假如都不响应的话,忽略该事件. 经典使用场景: 根据响应链找到合适的视图进行处理某些事件,比如找到视图控制器viewController 或者 某个指定类型的视图
手势
UiGestureRecognizer是一个父类,而实际操作中我们要使用它的子类,较为常用的有以下几种:
UITapGestureRecognizer | 轻拍手势 |
UISwipeGestureRecognizer | 轻扫手势 |
UILongPressGestureRecognizer | 长按手势 |
UIPanGestureRecognizer | 平移手势 |
UIPinchGestureRecognizer | 捏合(缩放)手势 |
UIRotationGestureRecognizer | 旋转手势 |
UIScreenEdgePanGestureRecognizer | 屏幕边缘平移 |
UITapGestureRecognizer(点击手势)
//创建手势 使用initWithTarget:action:的方法创建
{
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapView:)];
//设置属性
//tap 手势一共两个属性,一个是设置轻拍次数,一个是设置点击手指个数
//设置轻拍次数
tap.numberOfTapsRequired = 2;
//设置手指字数
tap.numberOfTouchesRequired = 2;
//别忘了添加到testView上
[_testView addGestureRecognizer:tap];
}
-(void)tapView:(UITapGestureRecognizer *)sender{
//设置轻拍事件改变testView的颜色
_testView.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];
}
UISwipeGestureRecognizer (滑动手势)
{
//创建手势
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(swipeView:)];
//设置属性,swipe也是有两种属性设置手指个数及轻扫方向
swipe.numberOfTouchesRequired = 2;
//设置轻扫方向(默认是从左往右)
//direction是一个枚举值有四个选项,我们可以设置从左往右,从右往左,从下往上以及从上往下
//设置轻扫方向(默认是从左往右)
swipe.direction = UISwipeGestureRecognizerDirectionLeft;
// [_redView addGestureRecognizer:swipe];
}
#pragma mark swipe轻扫手势事件
-(void)swipeView:(UISwipeGestureRecognizer *)sender{
_testView.backgroundColor = [UIColor colorWithRed:arc4random()%256/255.0 green:arc4random()%256/255.0 blue:arc4random()%256/255.0 alpha:1];
}