Gestion des événements pour iOS - comment hitTest: withEvent: et pointInside: withEvent: sont-ils liés?

145

Alors que la plupart des documents Apple sont très bien écrits, je pense que le « Guide de gestion des événements pour iOS » est une exception. J'ai du mal à comprendre clairement ce qui y est décrit.

Le document dit,

Dans le test de positionnement, une fenêtre appelle hitTest:withEvent:la vue la plus haute de la hiérarchie de vues; cette méthode procède en appelant de manière récursive pointInside:withEvent:sur chaque vue de la hiérarchie de vues qui renvoie OUI, en descendant la hiérarchie jusqu'à ce qu'elle trouve la sous-vue dans les limites de laquelle le contact a eu lieu. Cette vue devient la vue de test de succès.

Est-ce que c'est comme si seule hitTest:withEvent:la vue la plus haute est appelée par le système, qui appelle pointInside:withEvent:toutes les sous-vues, et si le retour d'une sous-vue spécifique est OUI, alors les appels pointInside:withEvent:des sous-classes de cette sous-vue?

realstuff02
la source
3
Un très bon tutoriel qui m'a aidé à créer un lien
anneblue
Le document équivalent plus récent pour cela pourrait maintenant être developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Réponses:

173

Cela semble une question assez fondamentale. Mais je suis d'accord avec vous, le document n'est pas aussi clair que d'autres documents, voici donc ma réponse.

La mise hitTest:withEvent:en œuvre de dans UIResponder effectue les opérations suivantes:

  • Il appelle pointInside:withEvent:deself
  • Si le retour est NON, hitTest:withEvent:retourne nil. la fin de l'histoire.
  • Si le retour est OUI, il envoie des hitTest:withEvent:messages à ses sous-vues. il commence à partir de la sous-vue de niveau supérieur et continue vers d'autres vues jusqu'à ce qu'une sous-vue renvoie un non- nilobjet ou que toutes les sous-vues reçoivent le message.
  • Si une sous-vue renvoie un non- nilobjet pour la première fois, la première hitTest:withEvent:renvoie cet objet. la fin de l'histoire.
  • Si aucune sous-vue ne renvoie un non- nilobjet, la première hitTest:withEvent:renvoieself

Ce processus se répète de manière récursive, de sorte que la vue feuille de la hiérarchie de vues est normalement renvoyée.

Cependant, vous pouvez passer outre hitTest:withEventpour faire quelque chose différemment. Dans de nombreux cas, le remplacement pointInside:withEvent:est plus simple et fournit toujours suffisamment d'options pour modifier la gestion des événements dans votre application.

MHC
la source
Voulez-vous dire que hitTest:withEvent:toutes les sous-vues sont finalement exécutées?
realstuff02
2
Oui. Remplacez simplement hitTest:withEvent:vos vues (et pointInsidesi vous le souhaitez), imprimez un journal et appelez [super hitTest...pour savoir qui hitTest:withEvent:est appelé dans quel ordre.
MHC
ne devrait pas l'étape 3 où vous mentionnez "Si le retour est OUI, il envoie hitTest: withEvent: ... ne devrait-il pas être pointInside: withEvent? Je pensais qu'il envoie pointInside à toutes les sous-vues?
prostock
En février, il a d'abord envoyé hitTest: withEvent:, dans lequel un pointInside: withEvent: a été envoyé à lui-même. Je n'ai pas revérifié ce comportement avec les versions suivantes du SDK, mais je pense que l'envoi de hitTest: withEvent: a plus de sens car il fournit un contrôle de niveau supérieur pour savoir si un événement appartient à une vue ou non; pointInside: withEvent: indique si l'emplacement de l'événement est sur la vue ou non, pas si l'événement appartient à la vue. Par exemple, une sous-vue peut ne pas souhaiter gérer un événement même si son emplacement est sur la sous-vue.
MHC
1
WWDC2014 Session 235 - Advanced Scrollviews and Touch Handling Techniques donne une excellente explication et un exemple pour ce problème.
antonio081014
297

Je pense que vous confondez les sous-classes avec la hiérarchie des vues. Ce que dit le doc est le suivant. Supposons que vous ayez cette hiérarchie de vues. Par hiérarchie, je ne parle pas de hiérarchie de classes, mais de vues dans la hiérarchie de vues, comme suit:

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

Dites que vous mettez votre doigt à l'intérieur D. Voici ce qui va se passer:

  1. hitTest:withEvent:est appelée A, la vue la plus haute de la hiérarchie des vues.
  2. pointInside:withEvent: est appelé récursivement sur chaque vue.
    1. pointInside:withEvent:est appelé A, et retourneYES
    2. pointInside:withEvent:est appelé B, et retourneNO
    3. pointInside:withEvent:est appelé C, et retourneYES
    4. pointInside:withEvent:est appelé D, et retourneYES
  3. Sur les vues qui sont retournées YES, il regardera la hiérarchie pour voir la sous-vue où le contact a eu lieu. Dans ce cas, à partir A, Cet D, il sera D.
  4. D sera la vue hit-test
pgb
la source
Merci pour la réponse. Ce que vous avez décrit est également ce que je pensais, mais @MHC dit que hitTest:withEvent:B, C et D sont également invoqués. Que se passe-t-il si D est une sous-vue de C et non de A? Je pense que je suis confus ...
realstuff02
2
Dans mon dessin, D est une sous-vue de C.
pgb
1
Ne Areviendrait -il pas YESaussi bien, tout comme Cet Dfait?
Martin Wickman le
2
N'oubliez pas que les vues invisibles (soit par .hidden ou opacité inférieure à 0,1), ou dont l'interaction de l'utilisateur est désactivée ne répondront jamais à hitTest. Je ne pense pas que hitTest soit appelé sur ces objets en premier lieu.
Jonny
Je voulais juste ajouter que hitTest: withEvent: peut être appelé sur toutes les vues en fonction de leur hiérarchie.
Adithya
47

Je trouve ce Hit-Testing dans iOS très utile

entrez la description de l'image ici

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

Modifier 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
la source
Vous devez donc l'ajouter à une sous-classe d'UIView et faire en sorte que toutes les vues de votre hiérarchie en héritent?
Guig
21

Merci pour les réponses, ils m'ont aidé à résoudre la situation avec des vues "superposées".

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

Supposons X- le toucher de l'utilisateur. pointInside:withEvent:sur les Bretours NO, donc les hitTest:withEvent:retours A. J'ai écrit la catégorie UIViewpour gérer le problème lorsque vous avez besoin de recevoir le toucher sur la vue la plus visible .

- (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. Nous ne devons pas envoyer d'événements tactiles pour des vues masquées ou transparentes, ou des vues avec userInteractionEnabledune valeur NO;
  2. Si le toucher est à l'intérieur self, selfsera considéré comme un résultat potentiel.
  3. Vérifiez récursivement toutes les sous-vues pour le hit. Le cas échéant, retournez-le.
  4. Sinon, retourne soi-même ou nul selon le résultat de l'étape 2.

Remarque, [self.subviewsreverseObjectEnumerator]nécessaire pour suivre la hiérarchie des vues de haut en bas. Et vérifiez pour clipsToBoundsvous assurer de ne pas tester les sous-vues masquées.

Usage:

  1. Importez la catégorie dans votre vue sous-classée.
  2. Remplacez hitTest:withEvent:par ceci
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Le guide officiel d'Apple fournit également de bonnes illustrations.

J'espère que cela aide quelqu'un.

Lion
la source
Incroyable! Merci pour la logique claire et le GRAND extrait de code, résolu mon casse-tête!
Thompson
@Lion, belle réponse. Vous pouvez également vérifier l'égalité pour effacer la couleur dans la première étape.
aquarium_moose
3

Cela montre comme cet extrait!

- (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;
}
hippopotame
la source
Je trouve que c'est la réponse la plus facile à comprendre, et elle correspond de très près à mes observations du comportement réel. La seule différence est que les sous-vues sont énumérées dans l'ordre inverse, de sorte que les sous-vues plus proches de l'avant reçoivent des touches de préférence aux frères et sœurs derrière elles.
Douglas Hill
@DouglasHill grâce à votre correction. Best regards
hippo
1

L'extrait de @lion fonctionne comme un charme. Je l'ai porté sur swift 2.1 et je l'ai utilisé comme extension d'UIView. Je le poste ici au cas où quelqu'un en aurait besoin.

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

Pour l'utiliser, remplacez simplement hitTest: point: withEvent dans votre uiview comme suit:

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
la source
0

Diagramme de classe

Test de succès

Trouver un First Responder

First Responderdans ce cas est la UIView point()méthode la plus profonde qui a renvoyé true

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

hitTest()Ressemble en interne

hitTest() {

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

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

    return nil

}

Envoyer l'événement tactile au 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?)

Jetons un coup d'œil à l'exemple

Chaîne de répondeurs

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

Jetez un œil à l'exemple

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
la source