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

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

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

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

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

Ответ 1

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

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

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

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

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

Ответ 2

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

+----------------------------+
|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, он будет смотреть вниз на иерархию, чтобы увидеть subview, где произошло касание. В этом случае из A, C и D это будет D.
  • D будет хитом-тестом

Ответ 3

Я нахожу этот Хит-тестирование в 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;
}

Ответ 4

Спасибо за ответы, они помогли мне решить ситуацию с "наложением".

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

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

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

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

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

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

Ответ 5

Он отображается как этот фрагмент!

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

Ответ 6

Отрывок из @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)
}