Захват касается подпредставления вне кадра его суперпредставления, используя hitTest:withEvent:

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

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

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

Пример:

  • (E) EditViewРодитель всех взглядов
  • (М) MenuView, подпредставление EditView
  • (Б) является ButtonView, подпредставление MenuView

Диаграмма:

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

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

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

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

Спасибо!

8 ответов

Решение

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

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

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

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

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

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

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

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

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

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

Представьте себе такую ​​ситуацию: представление с именем GrandParent имеет полностью в своих границах подпредставление с именем Parent. У этого Родителя есть подпредставление под названием Дочерний элемент, границы которого выходят за границы Родителя:

      -------------------------------------- -> Grand parent 
|              ------- -> Child      |
|              |     |               |
|          ----|-----|--- -> Parent  |
|          |   |   ^ |   |           |
|          |   |     |   |           |
|          ----|-----|---            |
|              | +   |               |
|     *        |-----|               |
|                                    |
--------------------------------------

и 3 различных пользовательских прикосновения. touch не распознается в дочернем элементе Всякий раз, когда вы касаетесь прародителя в любом месте его границы, прародитель будет вызывать все его прямые подвиды. .hitTest, независимо от того, где вы коснулись прародителя . Так вот, за 3 касания позвонит дедушка parent.hitTest().

Предполагается, что hitTest возвращает самого дальнего потомка, содержащего заданную точку (точка задается относительно собственных границ, в нашем примере Grand Parent вызывает Parent.hitTest() с точкой относительно родительских границ).

За *touch, parent.hitTest возвращает ноль, и это прекрасно. За ^touch, parent.hitTest возвращает Child, потому что он находится в его границах (реализация hitTest по умолчанию).

Но для +касание, parent.hitTest по умолчанию возвращает nil, так как касание не находится в границах родителя. Таким образом, нам нужна наша собственная реализация hitTest, чтобы в основном преобразовать точку относительно границы Родителя в точку относительно границ Дочернего элемента. Затем вызовите hitTest для дочернего элемента, чтобы увидеть, находится ли касание в границах дочернего элемента (предполагается, что дочерний элемент имеет реализацию hitTest по умолчанию, возвращающую самого дальнего потомка от него в пределах его границ, то есть самого себя).

Если у вас есть сложные вложенные подпредставления и если приведенное выше решение не очень хорошо работает для вас, это объяснение может быть полезно для создания собственной реализации, соответствующей вашей иерархии представлений.

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

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
}

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

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

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

Другие вопросы по тегам