Захват касаний к подпредставлению вне кадра супервизора с помощью 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, в которой мне неясно.

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

Ответы:


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

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

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


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

Когда кадр субпредставления находился за рамкой родительского представления, событие щелчка не отвечало! Ваше решение устранило эту проблему. Большое спасибо :-)
— 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» относительно «Пользовательское выноска: кнопка не активируется».

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


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