Обробка подій для iOS - як hitTest: withEvent: і pointInside: withEvent: пов'язані?


145

Хоча більшість яблучних документів дуже добре написані, я вважаю, що « Посібник з обробки подій для iOS » є винятком. Мені важко зрозуміти, що там описано.

У документі сказано:

Під час тестування хітів вікно викликає hitTest:withEvent:найвищий вигляд ієрархії перегляду; цей метод триває шляхом рекурсивного виклику pointInside:withEvent:кожного перегляду в ієрархії подання, яка повертає ТАК, ідучи вниз по ієрархії до тих пір, поки він не знайде підвид, в межах яких відбувся дотик. Цей вид стає переглядом тестування.

Тож чи схоже, що 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 для всіх підпоглядів?
простоck

Ще в лютому він вперше надіслав hitTest: withEvent :, в якому точкаInside: 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 або непрозорістю нижче 0,1) або вимкнено взаємодію користувачів, ніколи не відгукнуться на HitTest. Я не думаю, що hitTest викликається в першу чергу на цих об'єктах.
Джоні

Просто хотів додати той HitTest: withEvent: може бути викликано всіма переглядами залежно від їх ієрархії.
Адітія

47

Я вважаю це тестування Hit в 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 і чи всі спадки у вашій ієрархії успадковуються від нього?
Гіг

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. Інше повернення self або nil залежно від результату кроку 2.

Примітка. [self.subviewsreverseObjectEnumerator]Потрібно дотримуватися ієрархії подання зверху вниз. І clipsToBoundsпереконайтеся, що не перевіряти замасковані підгляди.

Використання:

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

Офіційний посібник Apple також дає хороші ілюстрації.

Сподіваюся, це комусь допомагає.


Дивовижний! Дякую за чітку логіку та ВЕЛИКИЙ фрагмент коду.
Томпсон

@ Лев, приємна відповідь. Також ви можете перевірити рівність, щоб очистити колір на першому кроці.
акваріум_моос

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 працює як шарм. Я переніс його для швидкого перегляду 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()метод, який повернув істину

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

}

Надіслати подію Touch на 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 onTouch]

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.