Захват касаний к подпредставлению вне кадра супервизора с помощью hitTest: withEvent:

94

Моя проблема: у меня есть супервизор, EditViewкоторый занимает в основном весь фрейм приложения, и подпредставление, MenuViewкоторое занимает только нижние ~ 20%, а затем MenuViewсодержит собственное подпредставление, ButtonViewкоторое фактически находится за пределами MenuViewграниц (что-то вроде этого:) ButtonView.frame.origin.y = -100.

(примечание: EditViewесть другие подпредставления, которые не являются частью MenuViewиерархии представлений пользователя, но могут повлиять на ответ.)

Вероятно, вы уже знаете проблему: когда ButtonViewнаходится в пределах MenuView(или, более конкретно, когда мои касания находятся в MenuViewпределах), ButtonViewреагирует на события касания. Когда мои касания выходят за MenuViewпределы (но все еще в ButtonViewпределах), событие касания не принимается ButtonView.

Пример:

  • (E) является EditViewродителем всех представлений
  • (M) - MenuViewэто подвид EditView
  • (B) - ButtonViewэто подвид MenuView

Диаграмма:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Поскольку (B) находится за пределами кадра (M), касание в области (B) никогда не будет отправлено на (M) - фактически, (M) никогда не анализирует касание в этом случае, и касание отправляется на следующий объект в иерархии.

Цель: я полагаю, что переопределение hitTest:withEvent:может решить эту проблему, но я не понимаю, как именно. В моем случае следует hitTest:withEvent:переопределить в EditView(моем «главном» супервизоре)? Или его следует переопределить в MenuViewпрямом наблюдении за кнопкой, которая не получает касаний? Или я неправильно об этом думаю?

Если для этого требуется подробное объяснение, будет полезен хороший онлайн-ресурс - за исключением документации Apple UIView, в которой мне неясно.

Благодарность!

Toblerpwn
источник

Ответы:

145

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

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

Надеюсь, это поможет любому, кто пытается использовать это решение для более сложных случаев использования.

Ноам
источник
Выглядит хорошо, спасибо за это. Однако возник один вопрос: почему вы делаете return [super hitTest: point withEvent: event]; ? Разве вы не могли бы просто вернуть nil, если нет подпредставления, запускающего касание? Apple заявляет, что hitTest возвращает nil, если ни одно подпредставление не содержит касания.
Ser Pounce
Хм ... Да, это звучит примерно правильно. Кроме того, для правильного поведения объекты необходимо повторять в обратном порядке (поскольку последний визуально является самым верхним). Отредактированный код для соответствия.
Ноам
3
Я только что использовал ваше решение, чтобы UIButton захватил прикосновение, и он находится внутри UIView, который находится внутри UICollectionViewCell внутри (очевидно) UICollectionView. Мне пришлось создать подклассы UICollectionView и UICollectionViewCell, чтобы переопределить hitTest: withEvent: в этих трех классах. И это работает как шарм !! Благодарность !!
Daniel García
3
В зависимости от желаемого использования он должен либо вернуть [super hitTest: point withEvent: event], либо nil. Возвращение себя заставит его получить все.
Ноам
1
Вот техническая документация Apple по этому же методу от Apple: developer.apple.com/library/ios/qa/qa2013/qa1812.html
Джеймс Куанг,
33

Хорошо, я немного покопался и протестировал, вот как hitTest:withEventработает - по крайней мере, на высоком уровне. Представьте этот сценарий:

  • (E) - EditView , родитель всех представлений
  • (M) - MenuView , часть EditView.
  • (B) - это ButtonView , подвид MenuView

Диаграмма:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Поскольку (B) находится за пределами кадра (M), касание в области (B) никогда не будет отправлено на (M) - фактически, (M) никогда не анализирует касание в этом случае, и касание отправляется на следующий объект в иерархии.

Однако, если вы реализуете hitTest:withEvent:в (M), нажатия в любом месте приложения будут отправляться в (M) (или, по крайней мере, он знает о них). Вы можете написать код для обработки прикосновения в этом случае и вернуть объект, который должен получить прикосновение.

Более конкретно: цель hitTest:withEvent:состоит в том, чтобы вернуть объект, который должен получить попадание. Итак, в (M) вы можете написать такой код:

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

Я все еще новичок в этом методе и этой проблеме, поэтому, если есть более эффективные или правильные способы написания кода, прокомментируйте.

Я надеюсь, что это поможет любому, кто задаст этот вопрос позже. :)

Toblerpwn
источник
Спасибо, это сработало для меня, хотя мне пришлось добавить еще немного логики, чтобы определить, какое из вложенных представлений должно действительно получать обращения. Я удалил ненужный перерыв из вашего примера, кстати.
Даниэль Саиди
Когда кадр субпредставления находился за рамкой родительского представления, событие щелчка не отвечало! Ваше решение устранило эту проблему. Большое спасибо :-)
Jeevan 08
@toblerpwn (забавный псевдоним :)), вы должны отредактировать этот отличный старый ответ, чтобы было очень ясно (ИСПОЛЬЗУЙТЕ БОЛЬШИЕ ЖИРНЫЕ ЗАГЛАВКИ), в каком классе вы должны это добавить. Ура!
Fattie
26

В Swift 5

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}
дуан
источник
Это будет работать только в том случае, если вы примените переопределение к прямому контролю «неправильного поведения» представления
Худи Ильфельд
2

Что бы я сделал, так это чтобы ButtonView и MenuView существовали на одном уровне в иерархии представлений, поместив их в контейнер, чей фрейм полностью соответствует им обоим. Таким образом, интерактивная область вырезанного элемента не будет игнорироваться из-за границ супервизора.

Мамаккензи
источник
Я тоже думал об этом обходном пути - это означает, что мне придется продублировать некоторую логику размещения (или реорганизовать какой-нибудь серьезный код!), но это действительно может быть моим лучшим выбором в конце ..
toblerpwn 02
1

Если у вас есть много других вложенных представлений внутри родительского представления, то, вероятно, большинство других интерактивных представлений не будут работать, если вы используете вышеуказанные решения, в этом случае вы можете использовать что-то вроде этого (в Swift 3.2):

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}
парас гупта
источник
0

Если кому-то это нужно, вот быстрая альтернатива

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}
Сынок
источник
0

Поместите следующие строки кода в свою иерархию представлений:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

Для большей ясности в моем блоге было объяснено: «goaheadwithiphonetech» относительно «Пользовательское выноска: кнопка не активируется».

Надеюсь, это вам поможет ... !!!

Сандип Патель - SM
источник
Ваш блог был удален, пожалуйста, где мы можем найти объяснение?
ishahak