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.