事件描述
事件
:用户和应用程序之间的交互
IOS应用程序可以接收许多不同类型的事件吗,目前IOS事件分为4类:
public enum UIEventType : Int {
case touches // 触摸事件
case motion // 加速器事件
case remoteControl // 远程控制
@available(iOS 9.0, *)
case presses
}
UIEvent
每一个事件都对应着一个相应的描述对象UIEvent,它包含了事件的type
、timestamp
、allTouches
和一系列返回UITouch
序列的API
。
UITouch
表示屏幕上发生的触摸的位置
,大小
,移动
和力量
的对象。
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch
对象,一根手指对应一个UITouch。当触摸位置或其他参数的更改,UIKit会使用新信息更新相同的UITouch对象。
open var timestamp: TimeInterval { get } // 事件产生的时间戳
open var phase: UITouchPhase { get } // 触摸状态
@available(iOS 9.0, *)
open var type: UITouchType { get } // 触摸类型
open var tapCount: Int { get } // touch down within a certain point within a certain amount of time
open func location(in view: UIView?) -> CGPoint // 触摸在view上的位置
open func previousLocation(in view: UIView?) -> CGPoint // 记录前一个触摸点的位置
UIResponder
在iOS中不是任何对象都能接收并处理事件,只有继承了UIResponder
的响应者对象
才能接受并处理事件。通用的UIResponder子类包括UIView
,UIViewController
和UIApplication
。它提供了一系列常用的处理事件的Api:
open var canBecomeFirstResponder: Bool { get } // default is NO
open func becomeFirstResponder() -> Bool 成为第一响应者
open var canResignFirstResponder: Bool { get } // default is YES
open func resignFirstResponder() -> Bool 辞去第一响应者
open var isFirstResponder: Bool { get }
触摸事件
open func touchesBegan(_ touches: Set, with event: UIEvent?) 触摸开始
open func touchesMoved(_ touches: Set, with event: UIEvent?) 触摸点移动
open func touchesEnded(_ touches: Set, with event: UIEvent?) 触摸接触
open func touchesCancelled(_ touches: Set, with event: UIEvent?) 触摸取消
按压事件
@available(iOS 9.0, *)
open func pressesBegan(_ presses: Set, with event: UIPressesEvent?)
@available(iOS 9.0, *)
open func pressesChanged(_ presses: Set, with event: UIPressesEvent?)
@available(iOS 9.0, *)
open func pressesEnded(_ presses: Set, with event: UIPressesEvent?)
@available(iOS 9.0, *)
open func pressesCancelled(_ presses: Set, with event: UIPressesEvent?)
加速计事件
open func motionBegan(_ motion: UIEventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEventSubtype, with event: UIEvent?)
远程控制事件
open func remoteControlReceived(with event: UIEvent?)
事件的传递
事件的传递:从父控件向子控件传递,即自上而下传递。
- 当
事件
产生时,系统会将该事件
加入到一个由UIApplication管理的事件队列
中。队列是FIFO的,因此先加入的事件优先处理。 - UIApplication从队列中取出最前面的事件,并将事件分发给应用程序的主窗口
keyWindow
-
keyWindow
会在视图结构中找到一个最适合处理事件的视图,并将事件传递给该视图
应用程序是如何传递事件,并且找出最适合处理事件的视图?
事件传递的本质依然还是基于消息机制的发送消息
。继承自UIView的类拥有以下方法:
// 返回处理事件的视图,内部通过调用 -pointInside:withEvent: 判断
open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
// 判断触摸点是否在视图上
open func point(inside point: CGPoint, with event: UIEvent?) -> Bool
当控件调用 hitTest(_ , with:) -> UIView
函数时,它会调用point(inside:, with:) -> Bool
判断事件的触摸点是否在视图上。返回true
说明触摸点在视图上,向视图分支传递触摸事件;false
触摸点不在视图上,忽略视图及其内部结构分支。
hitTest(_ , with:) -> UIView
内部实现大致如下:
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// 1.判断下自己能否接收事件
guard isUserInteractionEnabled && !isHidden && alpha > 0.01 else {
return nil
}
// 2.判断下点在不在当前控件上
guard self.point(inside: point, with: event) else {
return nil
}
// 3.从后往前遍历自己的子控件,查找到更合适的view
for subview in subviews.reversed() {
let subPoint = convert(point, to: subview)
let festView = subview.hitTest(subPoint, with: event)
if let festView = festView {
return festView
}
}
// 4.如果没有比自己合适的子控件,最合适的view就是自己
return self
}
1、UIApplication
向keyWindow
发送 hitTest(_ , with:)
的消息
2、keyWindow(或控件)
接受消息后将消息转发给自己的子控件,转发的顺序为子控件数组逆顺序
(即从数组的最后一个子控件往前遍历),当某个子控件的hitTest(_ , with:)
返回不为nil
时,则说明该子控件最适合处理这个事件
3、将消息转发给找到的这个控件,返回2步骤
4、如果所有子控件hitTest(_ , with:)
都返回nil,则说明子控件中没有更适合处理事件的对象,最适合的处理事件的对象是自己,这时控件的hitTest(_ , with:) -> UIView
返回为自身
------------------------------让我们来做个实验:---------------------------------
自定义各个控件,并重写控件的hitTest(_ , with:)
var window: CustomWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = CustomWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
let mainSB = UIStoryboard(name: "Main", bundle: nil)
window?.rootViewController = mainSB.instantiateInitialViewController()
window?.makeKeyAndVisible()
return true
}
CustomWIndow:
class CustomWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("\(type(of: self))")
return super.hitTest(point, with: event)
}
@end
OrangeView:
class OrangeView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("\(type(of: self))")
return super.hitTest(point, with: event)
}
@end
...
其他颜色视图与 OrangeView 一样的定义,省略不写
注意: 以上视图结构图,橙色先于绿色加载在白色上,红色先于蓝色加载在橙色上
猜测:
- 点击 白色,事件传递顺序:
UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色 - 点击 绿色,事件传递顺序:
UIApplication -> UIWindow -> 白色 -> 绿色 - 点击 橙色:
UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色 -> 蓝色 -> 红色 - 点击 蓝色:
UIApplication -> UIWindow -> 白色 -> 绿色 -> 橙色 -> 蓝色 -> 黄色
逐一进行操作,并查看输出:
可见我们之间的结论是正确的:事件的传递:从父控件向子控件传递,即自上而下传递。
当某个控件可接收触摸事件时,则遍历它的子视图查看是否有更好的接收事件的视图。当某个控件不可接收事件时,则忽略它的内部视图分支。
事件拦截
在事件的传递中已经讲述了事件传递的实现和过程。基于此,可以实现事件的拦截实现:
- 重写
func hitTest(_ , with:) -> UIView
,可以返回希望的最适合处理事件的控件,return nil 则表示当前控件和它的子控件都不处理事件。 - 重写
func point(inside:, with:) -> Bool
,只要返回 false,不论触摸点是否在视图上,都不接收事件。 -
userInteractionEnable = false
或hidden = true
或alpha < 0.01
的控件会被忽略,不接收事件。
事件响应
事件的响应:从子控件向父控件传递,即自下而上传递。
响应者链
讲到事件的响应,不得不提响应者链。响应者链是由多个响应者对象链接起来的层次结构。
每个应用都存在着响应者链,它是由最表层的子控件途径各个控件直至UIApplication的一个响应者路径。根据第一响应者的不同,路径上的控件会跟着变化。
如何判断控件的下一响应者?(或如何辨析响应者链的组成?)
UIView对象。 如果视图是视图控制器的根视图,则下一个响应者是视图控制器; 否则,下一个响应者是视图的父视图。
UIViewController对象。如果视图控制器的视图是窗口的根视图,则下一个响应者是窗口对象。如果视图控制器由另一个视图控制器呈现,则下一个响应者是呈现视图控制器。
UIWindow对象。 窗口的下一个响应者是UIApplication对象。
UIApplication对象。 下一个响应者是app Delegate,但只有当app Delegate是UIResponder的实例并且不是view、viewController或者application自身时才成立
响应过程
事件产生并找到最适合处理事件的控件(即第一响应者
)后,就会沿着响应者链向上传递事件直到某一响应者调用相应的响应处理方法处理事件。见下图:
每类事件都有相应的响应处理方法,例如触摸事件为:
open func touchesBegan(_ touches: Set, with event: UIEvent?) 触摸开始
open func touchesMoved(_ touches: Set, with event: UIEvent?) 触摸点移动
open func touchesEnded(_ touches: Set, with event: UIEvent?) 触摸接触
open func touchesCancelled(_ touches: Set, with event: UIEvent?) 触摸取消
以上四个函数默认做法:将事件顺着响应者链向上传递,即touch方法默认不响应并处理事件,仅仅传递事件给下一响应者
。
对上面的例子进行改造来验证:
class ViewController: UIViewController {
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
print("\(type(of: self)) - touch")
}
}
重写白色视图的父控件viewController的touchesBegan(_: , with)
,接着点击黄色视图:
打印结果显示这时的响应对象为viewController。
可见YellowView并没有处理触摸事件,只是将触摸事件沿着响应者链向上传递,而其父视图BlueView同样没有处理事件继续传递,接着是依然没有处理事件的OrangeView和WhiteView,当事件传递给ViewController时,ViewController响应了事件,并实现了
func touchBegon(_: with:)
,至此事件传递结束。
继续修改黄色控件,实现func touchBegon(_: with:)
:
class YellowView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print("\(type(of: self))")
return super.hitTest(point, with: event)
}
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
print("\(type(of: self)) - touch")
}
}
@end
再次点击黄色视图:
事件响应者为YellowView。
结论:响应处理函数默认实现仅仅只是传递事件给下一响应者
注意:加速计事件不遵循响应链
,详情请参考 苹果官方文档
同一事件多个响应对象处理实现:
上面说过找到第一响应者之后,系统默认将事件沿着响应者链传递,既然如果,可以在某一响应者处理事件后,继续执行系统默认处理就能实现多个响应者处理事件:
class YellowView: UIView {
...
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
print("\(type(of: self)) - touch")
super.touchesBegan(touches, with: event)
}
}
扩大响应区域
扩大响应区域亦是对事件的拦截,在不改变控件尺寸的情况下实现如下:
- 重写
func hitTest(_ , with:) -> UIView
,直接返回控件自身
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
- 重写
func point(inside:, with:) -> Bool
,比较扩展的后的尺寸和触摸较。
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expand: CGFloat = 50
let newX = self.frame.midX + expand
let newY = self.frame.midY + expand
let newW = self.frame.width + 2 * expand
let newH = self.frame.height + 2 * expand
let expandRect = CGRect(x: newX, y: newY, width: newW, height: newH)
let newPoint = self.convert(point, to: self.superview)
return expandRect.contains(newPoint)
}
参考:Apple Document