Przechwytywanie dotknięć widoku podrzędnego poza ramką jego podglądu za pomocą funkcji hitTest: withEvent:


94

Mój problem: mam nadzór, EditViewktóry zajmuje w zasadzie całą ramkę aplikacji i podwidok, MenuViewktóry zajmuje tylko dolne ~ 20%, a następnie MenuViewzawiera własny podwidok, ButtonViewktóry faktycznie znajduje się poza MenuViewgranicami aplikacji (coś takiego:) ButtonView.frame.origin.y = -100.

(uwaga: EditViewma inne widoki podrzędne, które nie są częścią MenuViewhierarchii widoków, ale mogą wpływać na odpowiedź).

Prawdopodobnie już znasz ten problem: kiedy ButtonViewznajduje się w granicach MenuView(a dokładniej, kiedy moje dotknięcia są w MenuViewgranicach), ButtonViewreaguje na zdarzenia dotykowe. Kiedy moje dotknięcia są poza MenuViewjego granicami (ale nadal w ButtonViewgranicach), żadne zdarzenie dotykowe nie jest odbierane przez ButtonView.

Przykład:

  • (E) jest EditViewrodzicem wszystkich poglądów
  • (M) jest MenuViewpodziałem widoku EditView
  • (B) jest ButtonViewpodziałem widoku MenuView

Diagram:

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

Ponieważ (B) jest poza ramką (M), dotknięcie w regionie (B) nigdy nie zostanie wysłane do (M) - w rzeczywistości (M) nigdy nie analizuje dotyku w tym przypadku, a dotknięcie jest wysyłane do następny obiekt w hierarchii.

Cel: Rozumiem, że nadpisywanie hitTest:withEvent:może rozwiązać ten problem, ale nie rozumiem dokładnie, jak. W moim przypadku powinno hitTest:withEvent:być nadpisane w EditView(mój nadzór „nadrzędny”)? A może powinno zostać pominięte w MenuViewbezpośrednim nadzorze nad przyciskiem, który nie jest dotknięty? A może źle o tym myślę?

Jeśli wymaga to długiego wyjaśnienia, pomocne byłoby dobre źródło informacji online - z wyjątkiem dokumentów Apple UIView, które nie wyjaśniły mi tego jasno.

Dzięki!

Odpowiedzi:


145

Zmodyfikowałem kod zaakceptowanej odpowiedzi, aby był bardziej ogólny - obsługuje przypadki, w których widok zawiera podglądy klipów do swoich granic, może być ukryty, a co ważniejsze: jeśli podwidoki są złożonymi hierarchiami widoków, zostanie zwrócony prawidłowy widok podrzędny.

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

Mam nadzieję, że pomoże to każdemu, kto spróbuje użyć tego rozwiązania w bardziej złożonych przypadkach użycia.


Wygląda dobrze i dziękuję za zrobienie tego. Jednak miałem jedno pytanie: dlaczego robisz return [super hitTest: point withEvent: event]; ? Czy nie zwróciłbyś po prostu zera, gdyby nie było podglądy, która uruchamia dotyk? Apple mówi, że hitTest zwraca zero, jeśli żaden podgląd podrzędny nie zawiera dotyku.
— Ser Pounce

Hm ... Tak, to brzmi właściwie. Ponadto, aby zapewnić prawidłowe zachowanie, obiekty muszą być iterowane w odwrotnej kolejności (ponieważ ostatni jest wizualnie najwyżej położony). Zmodyfikowany kod w celu dopasowania.
— Noam

3
Właśnie użyłem twojego rozwiązania, aby UIButton przechwytywał dotyk i znajduje się wewnątrz UIView, który znajduje się wewnątrz UICollectionViewCell wewnątrz (oczywiście) UICollectionView. Musiałem podklasę UICollectionView i UICollectionViewCell, aby zastąpić hitTest: withEvent: w tych trzech klasach. I działa jak urok! Dzięki !!
— Daniel García,

3
W zależności od pożądanego użycia, powinien zwrócić [super hitTest: point withEvent: event] lub nil. Powrócenie do siebie spowoduje, że otrzyma wszystko.
— Noam

1
Oto dokument techniczny z pytaniami i odpowiedziami firmy Apple dotyczący tej samej techniki: developer.apple.com/library/ios/qa/qa2013/qa1812.html
— James Kuang

33

Ok, trochę wykopałem i przetestowałem, oto jak hitTest:withEventdziała - przynajmniej na wysokim poziomie. Wyobraź sobie ten scenariusz:

  • (E) to EditView , element nadrzędny wszystkich widoków
  • (M) to MenuView , podwidok EditView
  • (B) to ButtonView , widok podrzędny MenuView

Diagram:

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

Ponieważ (B) jest poza ramką (M), dotknięcie w regionie (B) nigdy nie zostanie wysłane do (M) - w rzeczywistości (M) nigdy nie analizuje dotyku w tym przypadku, a dotknięcie jest wysyłane do następny obiekt w hierarchii.

Jeśli jednak zaimplementujesz hitTest:withEvent:w (M), stuknięcia w dowolnym miejscu w aplikacji zostaną wysłane do (M) (a przynajmniej on o nich wie). Możesz napisać kod obsługujący dotyk w takim przypadku i zwrócić obiekt, który powinien otrzymać dotyk.

Dokładniej: celem programu hitTest:withEvent:jest zwrócenie obiektu, który powinien otrzymać trafienie. Więc w (M) możesz napisać taki kod:

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

Wciąż jestem bardzo nowy w tej metodzie i tym problemie, więc jeśli istnieją bardziej wydajne lub poprawne sposoby pisania kodu, proszę o komentarz.

Mam nadzieję, że pomoże to każdemu, kto później odpowie na to pytanie. :)


Dzięki, to zadziałało dla mnie, chociaż musiałem zrobić trochę więcej logiki, aby określić, który z podglądów podrzędnych powinien faktycznie otrzymywać trafienia. Przy okazji usunąłem niepotrzebną przerwę z twojego przykładu.
— Daniel Saidi

Kiedy ramka widoku podrzędnego znajdowała się wokół ramki widoku nadrzędnego, zdarzenie kliknięcia nie odpowiadało! Twoje rozwiązanie rozwiązało ten problem.
— Wielkie

@toblerpwn (zabawny pseudonim :)) powinieneś edytować tę doskonałą starszą odpowiedź, aby było bardzo jasne (UŻYJ DUŻYCH POGŁUSZONYCH LITEREK), w której klasie musisz to dodać. Twoje zdrowie!
— Fattie

26

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

To zadziała tylko wtedy, gdy zastosujesz nadpisanie na bezpośrednim nadzorze widoku „niewłaściwego zachowania”
— Hudi Ilfeld

2

Chciałbym, aby zarówno ButtonView, jak i MenuView istniały na tym samym poziomie w hierarchii widoku, umieszczając je oba w kontenerze, którego ramka całkowicie pasuje do nich obu. W ten sposób interaktywny obszar przyciętego elementu nie będzie ignorowany ze względu na granice superwiewu.


Myślałem również o tym obejściu - oznacza to, że będę musiał zduplikować jakąś logikę umieszczania (lub zmienić jakiś poważny kod!), ale może to być rzeczywiście mój najlepszy wybór w końcu ..
— toblerpwn

1

Jeśli masz wiele innych podglądów podrzędnych w widoku nadrzędnym, prawdopodobnie większość innych widoków interaktywnych nie zadziała, jeśli użyjesz powyższych rozwiązań, w takim przypadku możesz użyć czegoś takiego (w 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

Jeśli ktoś tego potrzebuje, oto szybka alternatywa

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

Umieść poniższe wiersze kodu w hierarchii widoku:

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

Dla dokładniejszego wyjaśnienia zostało to wyjaśnione na moim blogu: „goaheadwithiphonetech” w odniesieniu do „Niestandardowe objaśnienie: problem z przyciskiem nie jest klikalny”.

Mam nadzieję, że to ci pomoże ... !!!


Twój blog został usunięty, więc gdzie możemy znaleźć wyjaśnienie?
— ishahak
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.