Обработка событий для iOS - как связаны hitTest: withEvent: и pointInside: withEvent:? - PullRequest
142 голосов
/ 10 февраля 2011

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

В документе написано,

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

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

Ответы [ 6 ]

291 голосов
/ 10 февраля 2011

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

+----------------------------+
|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 будет представлением проверки удара
167 голосов
/ 10 февраля 2011

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

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

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

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

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

44 голосов
/ 15 июля 2015

Я считаю это Hit-Testing в iOS очень полезным

enter image description here

- (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
}
21 голосов
/ 27 июня 2013

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

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

Предположим, X - касание пользователя.pointInside:withEvent: on 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 голосов
/ 04 сентября 2013

Это выглядит как этот фрагмент!

- (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;
}
1 голос
/ 13 января 2016

Фрагмент @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 в вашем пользовательском интерфейсе следующим образом:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...