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!

toblerpwn
źródło

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.

Noam
źródło
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. :)

toblerpwn
źródło
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
}
duan
źródło
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.

mamackenzie
źródło
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)
    }
}
paras gupta
źródło
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
}
Synek
źródło
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 ... !!!

Sandip Patel - SM
źródło
Twój blog został usunięty, więc gdzie możemy znaleźć wyjaśnienie?
ishahak