视图-①本质

视图-①本质

[toc]

目录

1
2
3
4
5
6
7
8
9
10
附:整个响应链及事件链
1、完善响应链查找知识点
2、基础概念等详解
2.1 响应者对象(UIResponder)
2.2、UITouch(点击对象)
2.2.1、UITouch的几个主要属性和方法
2.2.2、UITouch的生成场景
2.3、UIEvent(事件对象)
3、响应链的应用
>

一、在一个app中间有一个button,在你手触摸屏幕点击后,到这个button收到点击事件,中间发生了什么

这其实是一个事件传递和响应链的问题。(其实,按钮点击后,这里还包括runloop的唤醒等知识,不过这点我们放在下一大点讲)。

答:在我们点击按钮的时候,会产生了UITouch(点击对象)和UIEvent(事件对象),这两个对象组合成一个点击事件。而发生触摸事件后,

①消息循环(runloop)/系统就会接收到这个触摸事件,并将它放到一个由UIApplication管理的消息队列(先进先出)里。

②UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理。首先UIApplication将事件传递给的是UIWindow对象(即一般为应用程序的主窗口keyWindow)。

③然后,UIWindow(继承自UIView)对象会继续向它的子View对象传递,直到传递到最上层。(或者说UIWindow使用hitTest:withEvent:方法查找touch操作的所在的视图view)

其中的应用程序逐级寻找能够响应这个事件的对象,直到没有响应者响应。这一寻找的过程,被称作事件的响应链

二、事件的响应链

事件的响应链大概过程如下图所示:

事件的响应链

  • 1、在传递的过程中,下一响应者的查找是通过UIView里的- pointInside: withEvent:- hitTest: withEvent:两个方法来确定的。当从最初的只有一个响应者通过这样的方式不断的找到下一响应者后,这些响应者就组成了一个响应者链。

  • 2、当通过- hitTest: withEvent:找到第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。这个过程就是事件的传递过程。从这可以看出它的方向是跟响应链方向相反的。这里我们可以用UITableViewCell中点击上面的label来想象。

附:整个响应链及事件链

整个响应链(向下)及事件链(向上),大概如图所示:

响应链(向下)及事件链(向上)及事件链(向上).png)
在上图,当- hitTest: withEvent:方法沿着红色箭头方向寻找第一响应者后,若第一响应者没有处理事件,则沿着响应者链向上追溯寻找响应者(即灰色箭头方向)执行touches方法。
所以响应链为红色部分,事件链的顺序可以理解为图上的灰色箭头部分(个人理解)。

1、完善响应链查找知识点

我们已经知道响应者链是由多个响应者组合起来的链条。那么怎么找到这些相应者呢?

响应者的查找为通过UIView内部的下面两个方法来查找的

1
2
3
4
5
//根据点击坐标返回事件是否发生在本视图以内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event; // default returns YES if point is in bounds

// 返回响应点击事件的对象(当点击区域在分为内时候,如果有子视图则返回子视图里最终的响应者,如果没有子视图则返回自身)
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event; // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system

对于一个视图

①、若子视图中的- pointInside: withEvent:方法返回为NO,即判断用户点击的区域不在该子视图范围内的话,则停止对这个子视图里的子视图继续查找,- hitTest: withEvent:返回nil。

②、若子视图中的- pointInside: withEvent:方法返回为YES,即判断用户点击的区域在该子视图范围内的话,则继续往该子视图里的子视图查找,直到没有子视图,然后- hitTest: withEvent:返回这个子视图,而后之前的视图的- hitTest: withEvent:也返回这个子视图。

  • 1
     

hitTest-withEvent-查找过程举例,如下图

hitTest-withEvent-查找过程举例
图片中view等级

1
2
3
4
[ViewA addSubview:ViewB];
[ViewA addSubview:ViewC];
[ViewB addSubview:ViewD];
[ViewB addSubview:ViewE];

那么点击viewE后,发生的过程是怎样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.A 是UIWindow的根视图,首先对A进行hitTest:withEvent:
2.判断A的userInteractionEnabled,如果为NO,A的hitTest:withEvent返回nil;
3.pointInside:withEvent:方法判断用户点击是否在A的范围内,显然返回YES
4.遍历A的子视图B和C。由于从后向前遍历,因此先查看C,再查看B。
>
4.1 查看C:
调用C的hitTest:withEvent方法:pointInside:withEvent:方法判断用户点击是否在C的范围内,不在返回NO,C对应的hitTest:withEvent: 方法return nil;
>
4.2 再查看B
调用B的hitTest:withEvent方法:pointInside:withEvent:判断用户点击是否在B的返回内,在返回YES。
>遍历B的子视图D和E,从后向前遍历,所以先查看E,再查看D。
4.2.1先查看E,调用E的hitTest:withEvent方法:pointInside:withEvent:方法 判断用户点击是否在E的范围内,在返回YES,E没有子视图,因此E对应的hitTest:withEvent方法返回E,再往前回溯,就是B的hitTest:withEvent方法返回E,因此A的hitTest:withEvent方法返回E。
4.2.2查看D,略
>
至此,点击事件的第一响应者就找到了。

2、基础概念等详解

iOS中的事件可以分为3大类型:

  1. 触摸事件
  2. 加速计事件
  3. 远程控制事件

在iOS中不是任何对象都能处理事件,能接受并这些处理事件的对象只有直接或间接继承自UIResponder的对象,我们称之为“响应者对象”。

2.1 响应者对象(UIResponder)

①、为什么只有继承自UIResponder的类才能够接收并处理事件呢?因为处理这些事件的方法是卸载UIResponder中的啊。详细的UIResponder中提供的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
4个处理触摸事件的对象方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;

和3个处理加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;

以及1个处理远程控制事件的方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;

附:如何实现UIView的拖拽呢?即让UIView随着手指的移动而移动。

答: 重写touchsMoved:withEvent:方法

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);
}

②、那么iOS中能接收并处理事件或者说继承自UIResponder的类有哪些呢?

1
2
3
> iOS中能接收并处理事件或者说继承自UIResponder的类有:
UIApplication、UIWindow、UIViewController和所有继承UIView的UIKit类都直接或间接的继承自UIResponder。
>

从UIResponder内部提供的方法可以看出,触摸方法接收两个参数,一个UITouch对象的集合,还有一个UIEvent对象。这两个参数分别代表的是点击对象和事件对象。

2.2、UITouch(点击对象)

UITouch表示单个点击,其类文件中存在枚举类型UITouchPhase的属性,用来表示当前点击的状态。这些状态包括点击开始、移动、停止不动、结束和取消五个状态。每次点击发生的时候,点击对象都放在一个集合中传入UIResponder的回调方法中。

2.2.1、UITouch的几个主要属性和方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@property(nonatomic,readonly) NSTimeInterval      timestamp;    // 记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) UITouchPhase phase; // 当前触摸事件所处的状态
@property(nonatomic,readonly) NSUInteger tapCount; // 短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) UITouchType type NS_AVAILABLE_IOS(9_0);


@property(nullable,nonatomic,readonly,strong) UIWindow *window; //触摸产生时所处的窗口
@property(nullable,nonatomic,readonly,strong) UIView *view; //触摸产生时所处的视图
@property(nullable,nonatomic,readonly,copy) NSArray <UIGestureRecognizer *> *gestureRecognizers NS_AVAILABLE_IOS(3_2);

/**
* 获取当前点击位置的坐标点
*
* @param view 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
*
* @return 返回值表示触摸在view上的位置点(这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0)))
*/
- (CGPoint)locationInView:(nullable UIView *)view;

/// 获取前一个触摸点位置的坐标点
- (CGPoint)previousLocationInView:(nullable UIView *)view;
2.2.2、UITouch的生成场景:

前言:每根手指触摸屏幕时都会创建一个与该手指相关的UITouch对象。一根手指对应一个UITouch对象。每个UITouch对象保存着跟手指相关的信息,比如触摸的位置、时间、阶段。
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置。
当手指离开屏幕时,系统会销毁相应的UITouch对象

实际调用现象举例:

①、当用户用一根手指触摸屏幕时,view会调用1次touchesBegan:withEvent:方法。touches参数中装着1个UITouch对象。

②、如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象。

③、如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

2.3、UIEvent(事件对象)

iOS使用UIEvent表示用户交互的事件对象,在UIEvent.h文件中,我们可以看到有一个UIEventType类型的属性,这个属性表示了当前的响应事件类型。分别有多点触控、摇一摇以及远程操作(在iOS之后新增了3DTouch事件类型)。在一个用户点击事件处理过程中,UIEvent对象是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@property(nonatomic,readonly) UIEventType     type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);


typedef NS_ENUM(NSInteger, UIEventType) {
UIEventTypeTouches,
UIEventTypeMotion,
UIEventTypeRemoteControl,
UIEventTypePresses NS_ENUM_AVAILABLE_IOS(9_0),
};

typedef NS_ENUM(NSInteger, UIEventSubtype) {
// available in iPhone OS 3.0
UIEventSubtypeNone = 0,

// for UIEventTypeMotion, available in iPhone OS 3.0
UIEventSubtypeMotionShake = 1,

// for UIEventTypeRemoteControl, available in iOS 4.0
UIEventSubtypeRemoteControlPlay = 100,
UIEventSubtypeRemoteControlPause = 101,
UIEventSubtypeRemoteControlStop = 102,
UIEventSubtypeRemoteControlTogglePlayPause = 103,
UIEventSubtypeRemoteControlNextTrack = 104,
UIEventSubtypeRemoteControlPreviousTrack = 105,
UIEventSubtypeRemoteControlBeginSeekingBackward = 106,
UIEventSubtypeRemoteControlEndSeekingBackward = 107,
UIEventSubtypeRemoteControlBeginSeekingForward = 108,
UIEventSubtypeRemoteControlEndSeekingForward = 109,
};

介绍了以上响应者对象(UIResponder)及其相关的UITouch(点击对象)和UIEvent(事件对象)相关概念后,我们就知道了用户点击后,会产生了UITouch(点击对象)和UIEvent(事件对象)并打包发送,最后由响应者对象(UIResponder)来处理这些事件。

现在的问题是你知道它是怎么通过用户的点击位置找到处理该点击事件的响应者对象吗?

3、响应链的应用

既然已经知道了系统是怎么获取响应视图的流程了,那么我们可以通过重写查找事件处理者的方法来实现不规则形状点击。

最常见的不规则视图就是圆形视图,在demo中我设置view的宽高为200,那么重写方法事件如下:

1
2
3
4
5
6
7
8
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
const CGFloat halfWidth = 100;
CGFloat xOffset = point.x - 100;
CGFloat yOffset = point.y - 100;
CGFloat radius = sqrt(xOffset * xOffset + yOffset * yOffset);
return radius <= halfWidth;
}

最终的效果图如下:
响应链的应用1_点击不规则图形

前面说过按钮点击后,这里还包括一些runloop相关的知识,如唤醒等,所以下面我们就专门开讲一件Runloop。

三、CALayer和UIView

UIView与CALayer是什么关系。

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

UIViewCALayerdelegate(CALayerDelegate)

UIView继承自UIResponder类,可以响应事件

CALayer直接继承自NSObject类,不可以响应事件

UIView主要处理事件,CALayer负责绘制

每个UIView内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的Layer所提供。两者都有树状层级结构,Layer内部有SubLayersView内部有SubViews,但是LayerView多了个AnchorPoint

CALayer的结构图如下:

CALayer的结构图

在 iOS 系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互。 但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer。

CALayer类的概念与UIView非常类似,它也具有树形的层级关系,并且可以包含图片文本、背景色等。它与UIView最大的不同在于它不能响应用户交互,可以说它根本就不知道响应链的存在。

在每一个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来在屏幕上显示的,UIView仅仅是对它的一层封装,实现了CALayer的delegate,提供了处理事件交互的具体功能,还有动画底层方法的高级 API。

以上摘要来自:内存恶鬼drawRect - 谈画图功能的内存优化中的CALayer和UIView介绍部分。

CALayer有三个视觉元素:背景色、内容和边框,其中,内容的本质是一个CGImage

CALayer和UIView

简述CALayer和UIView的关系

答:UIView和CALayer是相互依赖的关系。UIView依赖与calayer提供的内容,CALayer依赖uivew提供的容器来显示绘制的内容。归根到底CALayer是这一切的基础,如果没有CALayer,UIView自身也不会存在,UIView是一个特殊的CALayer实现,添加了响应事件的能力

结论:
UIView来自CALayer,高于CALayer,是CALayer高层实现与封装。UIView的所有特性来源于CALayer支持。

常见笔试/面试题

< 返回目录

问:UIButton从子类到父类依次继承自什么?

答:UIControl-> UIView-> UIResponder。

哪些视图的设置能禁止其相应事件

1、userInterface = NO;
2、hidden = YES;
3、当UIBUTTON透明度为0就不响应事件了,当UIBUTTON透明度为0就不响应事件了。

更多参考:iOS开发经验:button不能响应的原因

离屏渲染

在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所有就需要在屏幕外的上下文中渲染,即离屏渲染。

更多参考:iOS离屏渲染之优化分析该文非常重要。

END

< 返回目录