Gestione degli eventi per iOS: come hitTest: withEvent: e pointInside: withEvent: sono correlati?


145

Mentre la maggior parte dei documenti Apple sono scritti molto bene, penso che la " Guida alla gestione degli eventi per iOS " sia un'eccezione. È difficile per me capire chiaramente cosa è stato descritto lì.

Il documento dice:

Nel hit test, una finestra richiama hitTest:withEvent:la vista più in alto della gerarchia della vista; questo metodo procede invocando ricorsivamente pointInside:withEvent:ogni vista nella gerarchia della vista che restituisce SÌ, procedendo verso il basso nella gerarchia fino a trovare la sottoview all'interno dei cui limiti ha avuto luogo il tocco. Quella vista diventa la vista hit-test.

Quindi è come se solo hitTest:withEvent:la vista più in alto sia chiamata dal sistema, che chiama pointInside:withEvent:tutte le viste secondarie, e se il ritorno da una vista secondaria specifica è SÌ, allora le chiamate pointInside:withEvent:delle sottoclassi di quella sottoview?


3
Un ottimo tutorial che mi ha aiutato a trovare il link
— anneblue,

Il documento equivalente più recente potrebbe ora essere developer.apple.com/documentation/uikit/uiview/1622469-hittest
— Cœur

Risposte:


173

Sembra una domanda abbastanza semplice. Ma sono d'accordo con te che il documento non è chiaro come altri documenti, quindi ecco la mia risposta.

L'implementazione di hitTest:withEvent:in UIResponder procede come segue:

  • Si chiama pointInside:withEvent:diself
  • Se il reso è NO, hitTest:withEvent:restituisce nil. la fine della storia.
  • Se il ritorno è SÌ, invia i hitTest:withEvent:messaggi alle sue visualizzazioni secondarie. inizia dalla sottoview di livello superiore e continua con altre visualizzazioni fino a quando una subview restituisce un non niloggetto o tutte le visualizzazioni secondarie ricevono il messaggio.
  • Se una sottoview restituisce un non niloggetto per la prima volta, la prima hitTest:withEvent:restituisce quell'oggetto. la fine della storia.
  • Se nessuna sottoview restituisce un non niloggetto, il primo hitTest:withEvent:ritornaself

Questo processo si ripete in modo ricorsivo, quindi alla fine viene normalmente restituita la vista foglia della gerarchia della vista.

Tuttavia, potresti scavalcare hitTest:withEventper fare qualcosa di diverso. In molti casi, l'override pointInside:withEvent:è più semplice e offre comunque opzioni sufficienti per modificare la gestione degli eventi nell'applicazione.


Vuoi dire che hitTest:withEvent:tutte le subview vengono eseguite alla fine?
— realstuff02

2
Sì. Basta ignorare hitTest:withEvent:le visualizzazioni (e, pointInsidese lo si desidera), stampare un registro e chiamare [super hitTest...per scoprire chi hitTest:withEvent:è chiamato in quale ordine.
— MHC,

non dovresti passare al punto 3 in cui dici "Se il ritorno è SÌ, invia hitTest: withEvent: ... non dovrebbe essere pointInside: withEvent? Pensavo che inviasse pointInside a tutte le subview?
— prostock

A febbraio ha inviato hitTest: withEvent :, in cui un puntoInside: withEvent: è stato inviato a se stesso. Non ho ricontrollato questo comportamento con le seguenti versioni dell'SDK, ma penso che l'invio di hitTest: withEvent: abbia più senso perché fornisce un controllo di livello superiore se un evento appartiene o meno a una vista; pointInside: withEvent: indica se la posizione dell'evento è nella vista o meno, non se l'evento appartiene alla vista. Ad esempio, una sottoview potrebbe non voler gestire un evento anche se la sua posizione si trova sulla sottoview.
— MHC,

1
WWDC2014 Session 235 - Advanced Scrollviews and Touch Handling Techniques offre una grande spiegazione ed esempio per questo problema.
— antonio081014,

297

Penso che tu stia confondendo la sottoclasse con la gerarchia della vista. Quello che dice il documento è il seguente. Supponi di avere questa gerarchia di visualizzazioni. Per gerarchia non sto parlando della gerarchia di classi, ma delle viste all'interno della gerarchia delle viste, come segue:

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

Di 'che hai messo il dito dentro D. Ecco cosa accadrà:

  1. hitTest:withEvent:viene chiamato A, la vista più in alto della gerarchia della vista.
  2. pointInside:withEvent: viene chiamato in modo ricorsivo su ciascuna vista.
    1. pointInside:withEvent:viene chiamato Ae ritornaYES
    2. pointInside:withEvent:viene chiamato Be ritornaNO
    3. pointInside:withEvent:viene chiamato Ce ritornaYES
    4. pointInside:withEvent:viene chiamato De ritornaYES
  3. Sulle viste che sono tornate YES, guarderà in basso nella gerarchia per vedere la sottoview in cui si è verificato il tocco. In questo caso, da A, Ce D, sarà D.
  4. D sarà la vista hit-test

Grazie per la risposta. Quello che hai descritto è anche ciò che avevo in mente, ma anche @MHC dice hitTest:withEvent:di B, C e D. Cosa succede se D è una sottoview di C, non A? Penso di essermi confuso ...
— realstuff02

2
Nel mio disegno, D è una sottoview di C.
— pag

1
Non Aritornerebbe YESaltrettanto bene, proprio come Ce Dfa?
— Martin Wickman,

2
Non dimenticare che le viste che sono invisibili (o con .hidden o l'opacità inferiore a 0,1) o che hanno l'interazione dell'utente disattivata non risponderanno mai a hitTest. Non penso che hitTest sia stato chiamato su questi oggetti in primo luogo.
— Jonny

Volevo solo aggiungere quel hitTest: withEvent: può essere chiamato su tutte le viste a seconda della loro gerarchia.
— Adithya,

47

Trovo questo Hit Test in iOS molto utile

inserisci qui la descrizione dell'immagine

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

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

Quindi è necessario aggiungerlo a una sottoclasse di UIView e far sì che tutte le viste nella gerarchia ereditino da essa?
— Guig

21

Grazie per le risposte, mi hanno aiutato a risolvere la situazione con viste "sovrapposte".

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

Supponiamo X- tocco dell'utente. pointInside:withEvent:sui Britorni NO, quindi hitTest:withEvent:ritorni A. Ho scritto la categoria UIViewper gestire il problema quando è necessario ricevere il tocco nella vista più visibile in alto .

- (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. Non dovremmo inviare eventi touch per viste nascoste o trasparenti o viste con userInteractionEnabledset su NO;
  2. Se il tocco è dentro self, selfsarà considerato come un potenziale risultato.
  3. Controlla ricorsivamente tutte le sottoview per hit. Se presente, restituiscilo.
  4. Altrimenti restituisce se stesso o zero in base al risultato del passaggio 2.

Nota, è [self.subviewsreverseObjectEnumerator]necessario seguire la gerarchia di visualizzazione dall'alto verso il basso. E controlla per clipsToBoundsassicurarti di non testare le subview in maschera.

Uso:

  1. Importa la categoria nella vista in sottoclasse.
  2. Sostituisci hitTest:withEvent:con questo
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

La guida ufficiale di Apple fornisce anche alcune buone illustrazioni.

Spero che questo aiuti qualcuno.


Sorprendente! Grazie per la chiara logica e l'ottimo frammento di codice, risolto il mio grattacapo!
— Thompson,

@Lion, bella risposta. Inoltre puoi verificare l'uguaglianza per cancellare il colore nel primo passaggio.
— aquarium_moose,

3

Si presenta come questo frammento!

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

Trovo che questa sia la risposta più semplice da comprendere, e corrisponde molto attentamente alle mie osservazioni sul comportamento reale. L'unica differenza è che le viste secondarie sono elencate in ordine inverso, quindi le viste secondarie più vicine al fronte ricevono tocchi rispetto ai fratelli dietro di loro.
— Douglas Hill,

@DouglasHill grazie alla tua correzione. Cordiali saluti
— ippopotamo,

1

Lo snippet di @lion funziona come un fascino. L'ho portato su swift 2.1 e l'ho usato come estensione di UIView. Lo pubblicherò qui nel caso qualcuno ne avesse bisogno.

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

Per usarlo, basta sostituire hitTest: point: withEvent nella tua uiview come segue:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}

0

Diagramma di classe

Hit Test

Trova un First Responder

First Responderin questo caso è il UIView point()metodo più profondo del quale è tornato vero

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

Internamente hitTest()sembra

hitTest() {

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

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

    return nil

}

Invia Evento tocco al 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?)

Diamo un'occhiata ad esempio

Catena di risposta

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

Dai un'occhiata all'esempio

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]

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.