151 votes

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

Bien que la plupart des documents relatifs à la pomme soient très bien rédigés, je pense que ' Guide de gestion des événements pour iOS est une exception. J'ai du mal à comprendre ce qui y est décrit.

Le document dit,

Dans le hit-test, une fenêtre appelle hitTest:withEvent: sur la vue la plus élevée de la hiérarchie des vues ; cette méthode procède en appelant récursivement pointInside:withEvent: sur chaque vue de la hiérarchie des vues qui renvoie un OUI, en descendant dans la hiérarchie jusqu'à ce qu'il trouve la sous-vue dans les limites de laquelle le contact a eu lieu. Cette vue devient la vue du hit-test.

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

3 votes

Un très bon tutoriel qui m'a aidé lien

0 votes

Le document équivalent le plus récent pourrait être le suivant developer.apple.com/documentation/uikit/uiview/1622469-hittest

302voto

pgb Points 17316

Je pense que vous confondez la sous-classe avec la hiérarchie des vues. Ce que dit la documentation est le suivant. Supposons que vous ayez cette hiérarchie de vues. Par hiérarchie, je ne parle pas de la hiérarchie des classes, mais des vues à l'intérieur de la hiérarchie des vues, comme suit :

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

Disons que vous mettez votre doigt à l'intérieur D . Voici ce qui se passera :

  1. hitTest:withEvent: est appelé sur A la vue la plus élevée de la hiérarchie des vues.
  2. pointInside:withEvent: est appelé récursivement pour chaque vue.
    1. pointInside:withEvent: est appelé sur A et renvoie YES
    2. pointInside:withEvent: est appelé sur B et renvoie NO
    3. pointInside:withEvent: est appelé sur C et renvoie YES
    4. pointInside:withEvent: est appelé sur D et renvoie YES
  3. Sur les vues qui ont été renvoyées YES Dans le cas d'un toucher, il regardera vers le bas de la hiérarchie pour voir la sous-vue où le toucher a eu lieu. Dans ce cas, de A , C y D , il sera D .
  4. D sera la vue du hit-test

0 votes

Merci pour la réponse. Ce que vous avez décrit est également ce que j'avais en tête, mais @MHC dit hitTest:withEvent: de B, C et D sont également invoquées. Que se passe-t-il si D est une sous-vue de C et non de A ? Je crois que je me suis embrouillé...

2 votes

Dans mon dessin, D est une vue secondaire de C.

1 votes

Ne serait-ce pas A retour YES également, tout comme C y D fait ?

178voto

MHC Points 4483

Cette question semble assez élémentaire. Mais je suis d'accord avec vous pour dire que le document n'est pas aussi clair que d'autres, alors voici ma réponse.

La mise en œuvre de la hitTest:withEvent: dans UIResponder fait ce qui suit :

  • Il appelle pointInside:withEvent: de self
  • Si le retour est NO, hitTest:withEvent: retours nil . la fin de l'histoire.
  • Si le retour est OUI, il envoie hitTest:withEvent: à ses vues secondaires. elle commence par la sous-vue de premier niveau et se poursuit vers les autres vues jusqu'à ce qu'une sous-vue renvoie un message non nil ou toutes les vues secondaires reçoivent le message.
  • Si une sous-vue renvoie une valeur non nil la première fois, le premier hitTest:withEvent: renvoie cet objet. la fin de l'histoire.
  • Si aucune sous-vue ne renvoie une valeur non nil le premier objet hitTest:withEvent: retours self

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

Toutefois, vous pouvez remplacer hitTest:withEvent de faire quelque chose de différent. Dans de nombreux cas, le fait d'ignorer les pointInside:withEvent: est plus simple et offre suffisamment d'options pour modifier la gestion des événements dans votre application.

0 votes

Voulez-vous dire hitTest:withEvent: de toutes les sous-vues sont finalement exécutées ?

3 votes

Oui. Il suffit de passer outre hitTest:withEvent: dans vos vues (et pointInside si vous le souhaitez), imprimez un journal et appelez [super hitTest... pour savoir à qui appartient la hitTest:withEvent: est appelé dans quel ordre.

0 votes

L'étape 3 où vous mentionnez "Si le retour est OUI, il envoie hitTest:withEvent : ... ne devrait-elle pas être pointInside:withEvent ? Je pensais qu'il envoyait pointInside à toutes les sous-vues ?

50voto

onmyway133 Points 2196

Je trouve cela Hit-Testing dans iOS d'être très utile

enter image description here

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

21voto

Lion Points 130

Merci pour les réponses, elles m'ont aidé à résoudre le problème des vues "superposées".

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

Supposons X - l'utilisateur. pointInside:withEvent: sur B retours NO Ainsi hitTest:withEvent: retours A . J'ai écrit la catégorie sur UIView pour gérer les problèmes lorsque vous avez besoin de recevoir des informations sur la plus grande partie de la population. visible vue.

- (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 de contact pour les vues cachées ou transparentes, ou pour les vues avec userInteractionEnabled fixé à NO ;
  2. Si la touche est à l'intérieur self , self sera considéré comme un résultat potentiel.
  3. Vérifier récursivement toutes les sous-vues pour vérifier la présence d'un hit. S'il y en a une, elle est renvoyée.
  4. Sinon, renvoyer self ou nil en fonction du résultat de l'étape 2.

Remarque, [self.subviewsreverseObjectEnumerator] a dû suivre la hiérarchie des vues du haut vers le bas. Et vérifier la présence de clipsToBounds pour s'assurer de ne pas tester les vues secondaires masquées.

Utilisation :

  1. Importer la catégorie dans votre vue sous-classée.
  2. Remplacer hitTest:withEvent: avec cette

    • (UIView )hitTest:(CGPoint)point withEvent:(UIEvent )event { return [self overlapHitTest:point withEvent:event]; }

Guide officiel d'Apple fournit également de bonnes illustrations.

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

0 votes

Incroyable ! Merci pour la logique claire et l'excellent extrait de code, qui ont résolu mon problème de tête !

0 votes

@Lion, Bonne réponse. Vous pouvez également vérifier l'égalité pour effacer la couleur dans la première étape.

4voto

yoAlex5 Points 2350

iOS touch

1. User touch
2. event is created
3. hit testing by coordinates - find first responder - UIView and successors (UIWindow) 
 3.1 hit testing - recursive find the most deep view
  3.1.1 point inside - check coordinates
4. Send Touch Event to the First Responder

Diagramme de classes

Test des 3 coups

Trouver un First Responder

First Responder dans ce cas est le plus profond UIView point() ( hitTest() utilise point() en interne) dont la méthode renvoie un résultat positif. Il passe toujours par UIApplication -> UIWindow -> First Responder

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

En interne hitTest() ressemble à

func hitTest() -> View? {
    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

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

4 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?)

Prenons un exemple

Chaîne du répondant

Il s'agit d'une sorte de chain of responsibility modèle. Il se compose de UIResponser qui peut gérer UIEvent . Dans ce cas, elle commence par le premier intervenant qui passe outre touch... . super.touch... appelle le maillon suivant de la chaîne de réponse

Responder chain est également utilisé par addTarget o sendAction des approches comme le bus événementiel

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

Voici un 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)
    }
}

* isExclusiveTouch est pris en compte lors de la manipulation de multitouch

[Android onTouch]

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X