Обработка событий для iOS - как связаны hitTest: withEvent: и pointInside: withEvent:?


145

Хотя большинство документов Apple написаны очень хорошо, я думаю, что « Руководство по обработке событий для iOS » является исключением. Мне трудно ясно понять, что там описано.

В документе говорится:

При тестировании попаданий окно вызывает hitTest:withEvent:самый верхний вид иерархии представлений; этот метод выполняется путем рекурсивного вызова pointInside:withEvent:каждого представления в иерархии представлений, которое возвращает YES, и далее по иерархии, пока не найдет подпредставление, в пределах которого произошло касание. Этот вид становится тестом на попадание.

Так это похоже на то hitTest:withEvent:, что система вызывает только самый верхний вид, который вызывает pointInside:withEvent:все подпредставления, и если возвращение из конкретного подпредставления - ДА, то вызовы pointInside:withEvent:подклассов этого подпредставления?


3
Очень хороший урок, который помог мне с ссылкой
anneblue

Эквивалентным новым документом для этого теперь может быть developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Ответы:


173

Это кажется довольно основным вопросом. Но я согласен с вами, документ не так понятен, как другие документы, поэтому вот мой ответ.

Реализация hitTest:withEvent:в UIResponder делает следующее:

  • Он вызывает pointInside:withEvent:изself
  • Если возврат НЕТ, hitTest:withEvent:возвращается nil. конец истории.
  • Если возвращение - ДА, оно отправляет hitTest:withEvent:сообщения своим подпредставлениям. оно начинается с подпредставления верхнего уровня и продолжается в других представлениях, пока подпредставление не возвращает nilнеобъект или пока все подпредставления не получат сообщение.
  • Если подпредставление возвращает nilнеобъект в первый раз, первое hitTest:withEvent:возвращает этот объект. конец истории.
  • Если никакое подпредставление не возвращает nilнеобъект, первое hitTest:withEvent:возвращаетself

Этот процесс повторяется рекурсивно, поэтому обычно в конечном итоге возвращается листовое представление иерархии представлений.

Тем не менее, вы можете изменить, hitTest:withEventчтобы сделать что-то по-другому. Во многих случаях переопределение pointInside:withEvent:проще и все же предоставляет достаточно возможностей для настройки обработки событий в вашем приложении.


Вы имеете hitTest:withEvent:в виду, что все подпредставления выполняются в конце концов?
realstuff02

2
Да. Просто переопределите hitTest:withEvent:ваши представления (и, pointInsideесли хотите), распечатайте журнал и позвоните, [super hitTest...чтобы узнать, чей hitTest:withEvent:вызов вызывается в каком порядке.
MHC

не должен шаг 3, где вы упоминаете: «Если возвращаемое значение - ДА, оно отправляет hitTest: withEvent: ... не должно ли это быть pointInside: withEvent? Я думал, что оно отправляет pointInside всем подпредставлениям?
prostock

Еще в феврале он впервые отправил hitTest: withEvent:, в котором pointInside: withEvent: был отправлен самому себе. Я не перепроверил это поведение со следующими версиями SDK, но я думаю, что отправка hitTest: withEvent: имеет больше смысла, поскольку он обеспечивает более высокий уровень контроля того, принадлежит ли событие представлению или нет; pointInside: withEvent: сообщает, находится ли местоположение события в представлении или нет, а не принадлежит ли событие представлению. Например, подпредставление может не захотеть обрабатывать событие, даже если его местоположение находится в подпредставлении.
MHC

1
Сессия WWDC2014 235. Расширенные обзоры прокрутки и методы сенсорной обработки дают отличное объяснение и пример этой проблемы.
antonio081014

297

Я думаю, что вы путаете подклассы с иерархией представления. Документ говорит следующее. Скажем, у вас есть эта иерархия представлений. По иерархии я говорю не об иерархии классов, а о представлениях внутри иерархии представлений, а именно:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

Скажем, ты вставил палец внутрь D. Вот что произойдет:

  1. hitTest:withEvent:вызывается A, самый верхний вид иерархии представлений.
  2. pointInside:withEvent: вызывается рекурсивно на каждом представлении.
    1. pointInside:withEvent:вызывается Aи возвращаетYES
    2. pointInside:withEvent:вызывается Bи возвращаетNO
    3. pointInside:withEvent:вызывается Cи возвращаетYES
    4. pointInside:withEvent:вызывается Dи возвращаетYES
  3. На возвращенных представлениях YESон будет смотреть вниз на иерархию, чтобы увидеть подпредставление, где произошло касание. В этом случае от A, Cи D, это будет D.
  4. D будет тест на попадание

Спасибо за ответ. То, что вы описали, также является тем, что было у меня на уме, но @MHC говорит hitTest:withEvent:о B, C и D, также вызываемых. Что произойдет, если D является подпредставлением C, а не A? Я думаю, что я запутался ...
realstuff02

2
На моем рисунке D - это подпредставление C.
pgb

1
Не Aвернется YESтак же, как Cи Dделает?
Мартин Уикман,

2
Не забывайте, что представления, которые являются невидимыми (либо с помощью .hidden или opacity ниже 0,1), либо с отключенным взаимодействием с пользователем, никогда не будут реагировать на hitTest. Я не думаю, что hitTest вызывается для этих объектов в первую очередь.
Джонни

Просто хотел добавить, что hitTest: withEvent: может вызываться во всех представлениях в зависимости от их иерархии.
Адития

47

Я считаю, что это Хит-тестирование в 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
}

Таким образом, вам нужно добавить это в подкласс UIView и получить от него все представления в вашей иерархии?
Guig

21

Спасибо за ответы, они помогли мне решить ситуацию с «оверлейными» взглядами.

+----------------------------+
|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;
}
  1. Мы не должны отправлять сенсорные события для скрытых или прозрачных представлений или представлений с userInteractionEnabledустановленным значением NO;
  2. Если прикосновение внутри self, selfбудет рассматриваться как потенциальный результат.
  3. Проверьте рекурсивно все подпредставления для попадания. Если есть, верните его.
  4. В противном случае вернуть себя или ноль в зависимости от результата из шага 2.

Обратите внимание, [self.subviewsreverseObjectEnumerator]необходимо следовать иерархии представления сверху вниз. И проверьте, clipsToBoundsчтобы не проверять скрытые подпредставления.

Использование:

  1. Импорт категории в подклассе.
  2. Заменить hitTest:withEvent:на это
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Официальное руководство Apple также содержит несколько хороших иллюстраций.

Надеюсь, это кому-нибудь поможет.


Удивительный! Спасибо за ясную логику и отличный фрагмент кода, решил мой головной убор!
Томпсон

@ Лев, Хороший ответ. Также вы можете проверить равенство, чтобы очистить цвет на первом этапе.
aquarium_moose

3

Это показывает, как этот фрагмент!

- (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;
}

Я считаю, что это самый простой для понимания ответ, и он очень близко соответствует моим наблюдениям за фактическим поведением. Единственное отличие состоит в том, что подпредставления нумеруются в обратном порядке, поэтому подпредставления ближе к фронту получают прикосновения в предпочтении перед братьями и сестрами позади них.
Дуглас Хилл

@DouglasHill благодаря вашей коррекции. С наилучшими пожеланиями
бегемот

1

Фрагмент @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)
}

0

Диаграмма классов

Тестирование Хита

Найти First Responder

First Responderв этом случае самый глубокий UIView point()метод которого вернул true

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)
    }
}

[Android на ощупь]

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.