Обработка событий для iOS - как связаны hitTest:withEvent: и pointInside:withEvent:?
Хотя большинство документов Apple написаны очень хорошо, я думаю, что " Руководство по обработке событий для iOS" является исключением. Мне трудно ясно понять, что там описано.
В документе говорится,
В хит-тестировании окно вызывает
hitTest:withEvent:
на самом верхнем представлении иерархии представлений; этот метод выполняется путем рекурсивного вызоваpointInside:withEvent:
на каждом представлении в иерархии представлений, которое возвращает YES, продолжая вниз по иерархии, пока не найдет подпредставление, в пределах которого произошло касание. Этот вид становится тестом на попадание.
Так это только так hitTest:withEvent:
самый верхний вид вызывается системой, которая вызывает pointInside:withEvent:
всех подпредставлений, и если возвращение из определенного подпредставления - ДА, то вызывает pointInside:withEvent:
подклассов этого подпредставления?
6 ответов
Это кажется довольно основным вопросом. Но я согласен с вами, что документ не так понятен, как другие документы, поэтому вот мой ответ.
Реализация hitTest:withEvent:
в UIResponder делает следующее:
- Это вызывает
pointInside:withEvent:
изself
- Если возврат НЕТ,
hitTest:withEvent:
возвращаетсяnil
, конец истории. - Если возвращается ДА, он отправляет
hitTest:withEvent:
сообщения для его подпредставлений. он начинается с подпредставления верхнего уровня и продолжается в других представлениях, пока подпредставление не возвращаетnil
объект или все подпредставления получают сообщение. - Если подпредставление возвращает не
nil
объект в первый раз, первыйhitTest:withEvent:
возвращает этот объект. конец истории. - Если никакое подпредставление не возвращает
nil
объект, первыйhitTest:withEvent:
возвращаетсяself
Этот процесс повторяется рекурсивно, поэтому обычно в конечном итоге возвращается листовое представление иерархии представлений.
Тем не менее, вы можете переопределить hitTest:withEvent
сделать что-то по-другому. Во многих случаях переопределение pointInside:withEvent:
проще и все же предоставляет достаточно возможностей для настройки обработки событий в вашем приложении.
Я думаю, что вы путаете подклассы с иерархией представления. Документ говорит следующее. Скажем, у вас есть эта иерархия представлений. По иерархии я говорю не об иерархии классов, а о представлениях внутри иерархии представлений следующим образом:
+----------------------------+
|A |
|+--------+ +------------+ |
||B | |C | |
|| | |+----------+| |
|+--------+ ||D || |
| |+----------+| |
| +------------+ |
+----------------------------+
Скажи, что ты засунул палец внутрь D
, Вот что произойдет:
hitTest:withEvent:
называется наA
самый верхний вид иерархии представлений.pointInside:withEvent:
вызывается рекурсивно на каждом представлении.pointInside:withEvent:
называется наA
и возвращаетYES
pointInside:withEvent:
называется наB
и возвращаетNO
pointInside:withEvent:
называется наC
и возвращаетYES
pointInside:withEvent:
называется наD
и возвращаетYES
- О взглядах, которые вернулись
YES
, он будет смотреть вниз на иерархию, чтобы увидеть подпредставление, где произошло касание. В этом случае изA
,C
а такжеD
, это будетD
, D
будет тест на попадание
Я считаю, что это Хит-тестирование в iOS очень полезно
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event]) {
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
if (hitTestView) {
return hitTestView;
}
}
return self;
}
return nil;
}
Редактировать Swift 4:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) {
return super.hitTest(point, with: event)
}
guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
return nil
}
for subview in subviews.reversed() {
let convertedPoint = subview.convert(point, from: self)
if let hitView = subview.hitTest(convertedPoint, with: event) {
return hitView
}
}
return nil
}
Спасибо за ответы, они помогли мне решить ситуацию с "оверлейными" взглядами.
+----------------------------+
|A +--------+ |
| |B +------------------+ |
| | |C X | |
| | +------------------+ |
| | | |
| +--------+ |
| |
+----------------------------+
Предполагать X
- прикосновение пользователя. pointInside:withEvent:
на B
возвращается NO
, так hitTest:withEvent:
возвращается A
, Я написал категорию на UIView
чтобы решить проблему, когда вам нужно прикоснуться к верхней части наиболее видимого вида.
- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1
if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
return nil;
// 2
UIView *hitView = self;
if (![self pointInside:point withEvent:event]) {
if (self.clipsToBounds) return nil;
else hitView = nil;
}
// 3
for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
CGPoint insideSubview = [self convertPoint:point toView:subview];
UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
if (sview) return sview;
}
// 4
return hitView;
}
- Мы не должны отправлять сенсорные события для скрытых или прозрачных представлений или представлений с
userInteractionEnabled
установлен вNO
; - Если касание внутри
self
,self
будет рассматриваться как потенциальный результат. - Проверьте рекурсивно все подпредставления для попадания. Если есть, верните его.
- В противном случае вернуть себя или ноль в зависимости от результата из шага 2.
Заметка, [self.subviewsreverseObjectEnumerator]
необходимо следовать иерархии просмотра сверху вниз. И проверить на clipsToBounds
чтобы не проверять маскированные подпредставления.
Использование:
- Импорт категории в подклассе.
- замещать
hitTest:withEvent:
с этим
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
return [self overlapHitTest:point withEvent:event];
}
Официальное руководство Apple также содержит несколько хороших иллюстраций.
Надеюсь, это кому-нибудь поможет.
Диаграмма классов
Проверка попадания
Найди First Responder
First Responder
в этом случае самый глубокий UIView
point()
метод которого вернул истину
func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool
Внутри hitTest()
похоже
hitTest() {
if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }
for subview in subviews {
if subview.hitTest() != nil {
return subview
}
}
return nil
}
Отправить событие касания в First Responder
//UIApplication.shared.sendEvent()
//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)
//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
Давайте посмотрим на пример
Цепочка ответчика
//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
Взгляните на пример
class AppDelegate: UIResponder, UIApplicationDelegate {
@objc
func foo() {
//this method is called using Responder Chain
print("foo") //foo
}
}
class ViewController: UIViewController {
func send() {
UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
}
}
Это показывает, как этот фрагмент!
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
{
return nil;
}
if (![self pointInside:point withEvent:event])
{
return nil;
}
__block UIView *hitView = self;
[self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
CGPoint thePoint = [self convertPoint:point toView:obj];
UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];
if (theSubHitView != nil)
{
hitView = theSubHitView;
*stop = YES;
}
}];
return hitView;
}
Фрагмент @lion работает как шарм. Я портировал его на swift 2.1 и использовал как расширение для UIView. Я публикую это здесь на случай, если кому-то это понадобится.
extension UIView {
func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
// 1
if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
return nil
}
//2
var hitView: UIView? = self
if !self.pointInside(point, withEvent: event) {
if self.clipsToBounds {
return nil
} else {
hitView = nil
}
}
//3
for subview in self.subviews.reverse() {
let insideSubview = self.convertPoint(point, toView: subview)
if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
return sview
}
}
return hitView
}
}
Чтобы использовать его, просто переопределите hitTest:point:withEvent в вашем uiview следующим образом:
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
let uiview = super.hitTest(point, withEvent: event)
print("hittest",uiview)
return overlapHitTest(point, withEvent: event)
}