Ereignisbehandlung für iOS - wie hängen hitTest: withEvent: und pointInside: withEvent: zusammen?


145

Während die meisten Apple-Dokumente sehr gut geschrieben sind, halte ich ' Event Handling Guide für iOS ' für eine Ausnahme. Es fällt mir schwer, klar zu verstehen, was dort beschrieben wurde.

Das Dokument sagt:

Beim Testen von Treffern ruft ein Fenster hitTest:withEvent:die oberste Ansicht der Ansichtshierarchie auf. Bei dieser Methode wird pointInside:withEvent:jede Ansicht in der Ansichtshierarchie, die YES zurückgibt, rekursiv aufgerufen und die Hierarchie nach unten verschoben, bis die Unteransicht gefunden wird, innerhalb deren Grenzen die Berührung stattgefunden hat. Diese Ansicht wird zur Hit-Test-Ansicht.

Ist es also so, dass nur hitTest:withEvent:die oberste Ansicht vom System aufgerufen wird, das pointInside:withEvent:alle Unteransichten aufruft, und wenn die Rückkehr von einer bestimmten Unteransicht JA lautet, dann Aufrufe pointInside:withEvent:der Unterklassen dieser Unteransicht?


3
Ein sehr gutes Tutorial, das mir beim Link
anneblue

Das entsprechende neuere Dokument hierfür könnte jetzt developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Antworten:


173

Es scheint eine ziemlich grundlegende Frage zu sein. Aber ich stimme Ihnen zu, dass das Dokument nicht so klar ist wie andere Dokumente. Hier ist meine Antwort.

Die Implementierung von hitTest:withEvent:in UIResponder führt Folgendes aus:

  • Es ruft pointInside:withEvent:vonself
  • Wenn die Rückgabe NEIN ist, wird hitTest:withEvent:zurückgegeben nil. Das Ende der Geschichte.
  • Wenn die Rückgabe JA lautet, werden hitTest:withEvent:Nachrichten an die Unteransichten gesendet. Es beginnt in der Unteransicht der obersten Ebene und wird in anderen Ansichten fortgesetzt, bis eine Unteransicht ein Nichtobjekt zurückgibt niloder alle Unteransichten die Nachricht empfangen.
  • Wenn eine Unteransicht nilzum ersten Mal ein Nichtobjekt zurückgibt , gibt die erste hitTest:withEvent:dieses Objekt zurück. Das Ende der Geschichte.
  • Wenn keine Unteransicht ein Nichtobjekt zurückgibt nil, wird die erste hitTest:withEvent:zurückgegebenself

Dieser Vorgang wird rekursiv wiederholt, sodass normalerweise die Blattansicht der Ansichtshierarchie schließlich zurückgegeben wird.

Sie können jedoch überschreiben hitTest:withEvent, um etwas anderes zu tun. In vielen Fällen ist das Überschreiben pointInside:withEvent:einfacher und bietet dennoch genügend Optionen, um die Ereignisbehandlung in Ihrer Anwendung zu optimieren.


hitTest:withEvent:Meinst du, dass alle Unteransichten irgendwann ausgeführt werden?
Realstuff02

2
Ja. Überschreiben Sie einfach hitTest:withEvent:Ihre Ansichten (und pointInsidewenn Sie möchten), drucken Sie ein Protokoll und rufen Sie [super hitTest...an, um herauszufinden, wer hitTest:withEvent:in welcher Reihenfolge aufgerufen wird.
MHC

sollte nicht Schritt 3 sein, in dem Sie erwähnen "Wenn die Rückgabe JA ist, sendet es hitTest: withEvent: ... sollte es nicht pointInside: withEvent sein? Ich dachte, es sendet pointInside an alle Unteransichten?
prostock

Bereits im Februar wurde der erste hitTest: withEvent: gesendet, in dem ein pointInside: withEvent: an sich selbst gesendet wurde. Ich habe dieses Verhalten mit den folgenden SDK-Versionen nicht erneut überprüft, aber ich denke, das Senden von hitTest: withEvent: ist sinnvoller, da es eine übergeordnete Kontrolle darüber bietet, ob ein Ereignis zu einer Ansicht gehört oder nicht. pointInside: withEvent: Gibt an, ob sich der Ereignisort in der Ansicht befindet oder nicht, und nicht, ob das Ereignis zur Ansicht gehört. Beispielsweise möchte eine Unteransicht ein Ereignis möglicherweise nicht behandeln, selbst wenn sich seine Position in der Unteransicht befindet.
MHC

1
WWDC2014-Sitzung 235 - Erweiterte Bildlaufansichten und Techniken zur Handhabung von Berührungen bieten eine gute Erklärung und ein Beispiel für dieses Problem.
Antonio081014

297

Ich denke, Sie verwechseln Unterklassen mit der Ansichtshierarchie. Was der Arzt sagt, ist wie folgt. Angenommen, Sie haben diese Ansichtshierarchie. Nach Hierarchie spreche ich nicht von Klassenhierarchie, sondern von Ansichten innerhalb der Ansichtshierarchie wie folgt:

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

Angenommen, Sie legen Ihren Finger hinein D. Folgendes wird passieren:

  1. hitTest:withEvent:wird aufgerufen A, die oberste Ansicht der Ansichtshierarchie.
  2. pointInside:withEvent: wird in jeder Ansicht rekursiv aufgerufen.
    1. pointInside:withEvent:wird aufgerufen Aund kehrt zurückYES
    2. pointInside:withEvent:wird aufgerufen Bund kehrt zurückNO
    3. pointInside:withEvent:wird aufgerufen Cund kehrt zurückYES
    4. pointInside:withEvent:wird aufgerufen Dund kehrt zurückYES
  3. In den zurückgegebenen Ansichten YESwird auf die Hierarchie geschaut, um die Unteransicht zu sehen, in der die Berührung stattgefunden hat. In diesem Fall aus A, Cund Dwird es sein D.
  4. D wird die Hit-Test-Ansicht sein

Danke für die Antwort. Was Sie beschrieben haben, war auch das, was ich dachte, aber @MHC sagt, dass hitTest:withEvent:B, C und D ebenfalls aufgerufen werden. Was passiert, wenn D eine Unteransicht von C ist, nicht A? Ich glaube, ich war verwirrt ...
realstuff02

2
In meiner Zeichnung ist D eine Unteransicht von C.
pgb

1
Würde nicht Azurückkehren YESals auch, wie Cund Dtut?
Martin Wickman

2
Vergessen Sie nicht, dass Ansichten, die unsichtbar sind (entweder durch .hidden oder eine Deckkraft unter 0,1) oder deren Benutzerinteraktion deaktiviert ist, niemals auf hitTest reagieren. Ich glaube nicht, dass hitTest überhaupt für diese Objekte aufgerufen wird.
Jonny

Ich wollte nur diesen hitTest hinzufügen: withEvent: kann abhängig von ihrer Hierarchie für alle Ansichten aufgerufen werden.
Adithya

47

Ich finde dieses Hit-Testing in iOS sehr hilfreich

Geben Sie hier die Bildbeschreibung ein

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

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

Sie müssen dies also zu einer Unterklasse von UIView hinzufügen und alle Ansichten in Ihrer Hierarchie davon erben lassen?
Guig

21

Vielen Dank für die Antworten. Sie haben mir geholfen, die Situation mit "Overlay" -Ansichten zu lösen.

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

Angenommen X- die Berührung des Benutzers. pointInside:withEvent:bei BRückgabe NO, also hitTest:withEvent:Rückgabe A. Ich habe eine Kategorie geschrieben UIView, um Probleme zu lösen, wenn Sie die oberste sichtbare Ansicht berühren müssen .

- (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. Wir sollten keine Berührungsereignisse für versteckte oder transparente Ansichten oder Ansichten mit der userInteractionEnabledEinstellung auf senden NO.
  2. Wenn sich eine Berührung im Inneren befindet self, selfwird dies als potenzielles Ergebnis betrachtet.
  3. Überprüfen Sie rekursiv alle Unteransichten auf Treffer. Falls vorhanden, geben Sie es zurück.
  4. Andernfalls kehren Sie je nach Ergebnis aus Schritt 2 selbst oder null zurück.

Beachten Sie, dass [self.subviewsreverseObjectEnumerator]die Ansichtshierarchie von oben nach unten eingehalten werden muss. Überprüfen Sie clipsToBounds, ob maskierte Unteransichten nicht getestet werden sollen.

Verwendung:

  1. Importieren Sie die Kategorie in Ihre Unterklassenansicht.
  2. Ersetzen Sie hitTest:withEvent:dies durch
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Der offizielle Apple-Leitfaden bietet auch einige gute Illustrationen.

Hoffe das hilft jemandem.


Tolle! Vielen Dank für die klare Logik und das großartige Code-Snippet, das meinen Head-Scratcher gelöst hat!
Thompson

@ Löwe, nette Antwort. Außerdem können Sie im ersten Schritt die Gleichheit überprüfen, um die Farbe zu löschen.
aquarium_moose

3

Es zeigt sich wie dieser Ausschnitt!

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

Ich finde diese Antwort am einfachsten zu verstehen und sie passt sehr gut zu meinen Beobachtungen des tatsächlichen Verhaltens. Der einzige Unterschied besteht darin, dass die Unteransichten in umgekehrter Reihenfolge aufgelistet werden, sodass Unteransichten, die näher an der Vorderseite liegen, Berührungen gegenüber Geschwistern dahinter erhalten.
Douglas Hill

@DouglasHill dank deiner Korrektur. Viele Grüße
Nilpferd

1

Das Snippet von @lion wirkt wie ein Zauber. Ich habe es auf Swift 2.1 portiert und als Erweiterung für UIView verwendet. Ich poste es hier, falls jemand es braucht.

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

Um es zu verwenden, überschreiben Sie einfach hitTest: point: withEvent in Ihrer Ansicht wie folgt:

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

Klassen Diagramm

Hit Testing

Finde einen First Responder

First Responderin diesem Fall ist die tiefste UIView point()Methode, von der true zurückgegeben wurde

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

Intern hitTest()sieht aus wie

hitTest() {

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

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

    return nil

}

Touch-Ereignis an senden 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?)

Schauen wir uns ein Beispiel an

Antwortkette

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

Schauen Sie sich das Beispiel an

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]

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.