Obsługa zdarzeń dla iOS - jak hitTest: withEvent: i pointInside: withEvent: są powiązane?

145

Chociaż większość dokumentów Apple jest bardzo dobrze napisana, myślę, że „ Przewodnik obsługi zdarzeń na iOS ” jest wyjątkiem. Trudno mi dokładnie zrozumieć, co zostało tam opisane.

Dokument mówi:

W testowaniu trafień okno wywołuje hitTest:withEvent:najwyższy widok hierarchii widoków; ta metoda jest pointInside:withEvent:kontynuowana przez rekurencyjne wywoływanie każdego widoku w hierarchii widoków, który zwraca YES, przechodzenie w dół hierarchii, aż znajdzie podwidok, w którego granicach nastąpiło dotknięcie. Ten widok staje się widokiem testu trafień.

Czy jest więc tak, że tylko hitTest:withEvent:najwyższy widok jest wywoływany przez system, który wywołuje pointInside:withEvent:wszystkie widoki podrzędne, a jeśli wynik z określonego widoku podrzędnego jest TAK, to wywołania pointInside:withEvent:podklas tego widoku podrzędnego?

realstuff02
źródło
3
Bardzo dobry tutorial, który pomógł mi znaleźć link
anneblue
Równoważnym nowszym dokumentem dla tego może być teraz developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Odpowiedzi:

173

Wydaje się to dość podstawowe pytanie. Ale zgadzam się z tobą, dokument nie jest tak jasny jak inne dokumenty, więc oto moja odpowiedź.

Wdrożenie hitTest:withEvent:w UIResponder polega na:

  • Apeluje pointInside:withEvent:oself
  • Jeśli zwrot jest NIE, hitTest:withEvent:zwraca nil. koniec opowiadania.
  • Jeśli zwrot jest TAK, wysyła hitTest:withEvent:wiadomości do swoich podglądów podrzędnych. zaczyna się od widoku podrzędnego najwyższego poziomu i kontynuuje do innych widoków, aż widok podrzędny zwróci element niebędący nilobiektem lub wszystkie podglądy podrzędne otrzymają wiadomość.
  • Jeśli widok podrzędny zwraca nilobiekt niebędący obiektem po raz pierwszy, pierwszy hitTest:withEvent:zwraca ten obiekt. koniec opowiadania.
  • Jeśli żaden widok podrzędny nie zwróci nilobiektu niebędącego przedmiotem, pierwszy hitTest:withEvent:zwracaself

Ten proces powtarza się rekurencyjnie, więc zwykle w końcu zwracany jest widok liścia hierarchii widoków.

Możesz jednak zastąpić, hitTest:withEventaby zrobić coś inaczej. W wielu przypadkach nadpisywanie pointInside:withEvent:jest prostsze i nadal zapewnia wystarczającą liczbę opcji, aby dostosować obsługę zdarzeń w aplikacji.

MHC
źródło
Czy masz na myśli, hitTest:withEvent:że wszystkie podglądy są w końcu wykonywane?
realstuff02
2
Tak. Po prostu nadpisz hitTest:withEvent:w swoich widokach (i pointInsidejeśli chcesz), wydrukuj dziennik i zadzwoń, [super hitTest...aby dowiedzieć się, kto hitTest:withEvent:jest wywoływany, w jakiej kolejności.
MHC
nie powinien być krok 3, w którym wspominasz "Jeśli zwrot jest TAK, wysyła hitTest: withEvent: ... czy nie powinien to być pointInside: withEvent? Myślałem, że wysyła pointInside do wszystkich podwidoków?
prostock
W lutym po raz pierwszy wysłał hitTest: withEvent :, w którym wysłano do siebie pointInside: withEvent:. Nie sprawdzałem ponownie tego zachowania z następującymi wersjami SDK, ale myślę, że wysłanie hitTest: withEvent: ma większy sens, ponieważ zapewnia kontrolę wyższego poziomu, czy zdarzenie należy do widoku, czy nie; pointInside: withEvent: mówi, czy lokalizacja zdarzenia jest w widoku, czy nie, a nie, czy zdarzenie należy do widoku. Na przykład widok podrzędny może nie chcieć obsługiwać zdarzenia, nawet jeśli jego lokalizacja znajduje się w podglądzie.
MHC
1
WWDC2014 Session 235 - Advanced Scrollviews and Touch Handling Techniques (zaawansowane techniki przewijania i obsługi dotykowej) daje świetne wyjaśnienie i przykład tego problemu.
antonio081014
297

Myślę, że mylisz podklasy z hierarchią widoków. To, co mówi doktor, jest następujące. Powiedzmy, że masz taką hierarchię widoków. Według hierarchii Nie mówię o hierarchii klas, ale o widokach w hierarchii widoków, jak następuje:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

Powiedz, że włożyłeś palec do środka D. Oto, co się stanie:

  1. hitTest:withEvent:jest wywoływany A, najwyższy widok hierarchii widoków.
  2. pointInside:withEvent: jest wywoływana rekurencyjnie w każdym widoku.
    1. pointInside:withEvent:jest wezwany Ai wracaYES
    2. pointInside:withEvent:jest wezwany Bi wracaNO
    3. pointInside:withEvent:jest wezwany Ci wracaYES
    4. pointInside:withEvent:jest wezwany Di wracaYES
  3. W przypadku widoków, które powróciły YES, spogląda w dół na hierarchię, aby zobaczyć widok podrzędny, w którym miało miejsce dotknięcie. W tym przypadku, z A,C a D, to będzie D.
  4. D będzie widokiem testu trafień
pgb
źródło
Dziękuję za Twoją odpowiedź. To, co opisałeś, jest również tym, co było w mojej głowie, ale @MHC mówi, że hitTest:withEvent:B, C i D są również przywoływane. Co się stanie, jeśli D jest podziałem na C, a nie A? Chyba się
pogubiłem
2
Na moim rysunku D jest
podziałem
1
Nie Awróciłby YESrównież, tak jak Ci Drobi?
Martin Wickman,
2
Nie zapominaj, że widoki, które są niewidoczne (przez .hidden lub nieprzezroczystość poniżej 0,1) lub mają wyłączoną interakcję z użytkownikiem, nigdy nie odpowiedzą na hitTest. Nie sądzę, aby hitTest był wywoływany na tych obiektach w pierwszej kolejności.
Jonny
Chciałem tylko dodać, że hitTest: withEvent: może być wywoływany we wszystkich widokach w zależności od ich hierarchii.
Adithya,
47

Uważam, że to testowanie trafień w iOS jest bardzo pomocne

wprowadź opis obrazu tutaj

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

Edytuj Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}
onmyway133
źródło
Więc musisz dodać to do podklasy UIView i czy wszystkie widoki w twojej hierarchii dziedziczą po nim?
Guig
21

Dzięki za odpowiedzi, pomogli mi rozwiązać sytuację z widokami „nakładkowymi”.

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

Załóżmy X- dotyk użytkownika. pointInside:withEvent:na Bzwrotach NO, więc hitTest:withEvent:zwraca A. Napisałem kategorię, UIViewaby poradzić sobie z problemem, gdy potrzebujesz dotknąć najbardziej widocznego widoku z góry.

- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
        return nil;

    // 2
    UIView *hitView = self;
    if (![self pointInside:point withEvent:event]) {
        if (self.clipsToBounds) return nil;
        else hitView = nil;
    }

    // 3
    for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
        CGPoint insideSubview = [self convertPoint:point toView:subview];
        UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
        if (sview) return sview;
    }

    // 4
    return hitView;
}
  1. Nie powinniśmy wysyłać zdarzeń dotykowych dla widoków ukrytych lub przezroczystych lub widoków z userInteractionEnabledustawieniem na NO;
  2. Jeśli dotyk jest w środku self, selfzostanie uznany za potencjalny wynik.
  3. Sprawdź rekurencyjnie wszystkie podglądy pod kątem trafienia. Jeśli tak, zwróć.
  4. W przeciwnym razie zwraca wartość własną lub zero w zależności od wyniku z kroku 2.

Uwaga, [self.subviewsreverseObjectEnumerator]konieczne jest przestrzeganie hierarchii widoków od góry do dołu. I sprawdź, czy clipsToBoundsnie testujesz zamaskowanych podglądów podrzędnych.

Stosowanie:

  1. Importuj kategorię w widoku podklasy.
  2. Zastąp hitTest:withEvent:to
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Oficjalny przewodnik Apple zawiera również dobre ilustracje.

Mam nadzieję, że to komuś pomoże.

Lew
źródło
Niesamowity! Dzięki za jasną logikę i WSPANIAŁY fragment kodu, rozwiązałem mój drapak głowy!
Thompson,
@Lion, dobra odpowiedź. Możesz również sprawdzić równość, aby usunąć kolor w pierwszym kroku.
aquarium_moose
3

Pokazuje jak ten fragment!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
    {
        return nil;
    }

    if (![self pointInside:point withEvent:event])
    {
        return nil;
    }

    __block UIView *hitView = self;

    [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {   

        CGPoint thePoint = [self convertPoint:point toView:obj];

        UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];

        if (theSubHitView != nil)
        {
            hitView = theSubHitView;

            *stop = YES;
        }

    }];

    return hitView;
}
hipopotam
źródło
Uważam, że ta odpowiedź jest najłatwiejsza do zrozumienia i bardzo dobrze pasuje do moich obserwacji rzeczywistego zachowania. Jedyną różnicą jest to, że podglądy są wyliczane w odwrotnej kolejności, więc podglądy bliższe przodu są bardziej dotykane niż rodzeństwo za nimi.
Douglas Hill
@DouglasHill dzięki twojej korekcie. Pozdrawiam
hipopotam
1

Fragment @lion działa jak urok. Przeniosłem go do Swift 2.1 i użyłem jako rozszerzenia do UIView. Wysyłam to tutaj na wypadek, gdyby ktoś tego potrzebował.

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

Aby go użyć, po prostu zastąp hitTest: point: withEvent w swoim uiview w następujący sposób:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}
mortadelo
źródło
0

Diagram klas

Testowanie trafień

Znajdź First Responder

First Responderw tym przypadku jest to najgłębsza UIView point()metoda, która zwróciła prawdę

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

hitTest()Wygląda jak wewnątrz

hitTest() {

    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }

    return nil

}

Wyślij zdarzenie dotykowe do First Responder

//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

Spójrzmy na przykład

Łańcuch odpowiedzi

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

Spójrz na przykład

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

[Android onTouch]

yoAlex5
źródło