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

Моя проблема: У меня есть супервизор EditView, который занимает в основном весь кадр приложения, а subview MenuView, который занимает только нижний ~ 20%, а затем MenuView содержит свой собственный subview 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, которые не дали мне понять.

Спасибо!

Ответ 1

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

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

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

Ответ 2

Хорошо, я немного поработал и тестировал, как работает 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];
}

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

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

Ответ 3

В Swift 4

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
}

Ответ 4

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

Ответ 5

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

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
}

Ответ 6

Если у вас есть много других объектов в представлении родителя, то, вероятно, большинство других интерактивных представлений не будут работать, если вы используете вышеуказанные решения, в этом случае вы можете использовать что-то вроде этого (в 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 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)
    }
}

Ответ 7

Поместите ниже строки кода в иерархию вашего представления:

- (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" в отношении "Пользовательская выноска: кнопка не является проблемой для кликов".

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