web-dev-qa-db-fra.com

Capturer des contacts sur une sous-vue en dehors du cadre de son aperçu en utilisant hitTest: withEvent:

Mon problème: j'ai un superview EditView qui occupe essentiellement le cadre entier de l'application et une sous-vue MenuView qui n'occupe que le bas ~ 20%, puis MenuView contient sa propre sous-vue ButtonView qui réside en dehors des limites de MenuView (quelque chose comme ceci: ButtonView.frame.Origin.y = -100).

(Remarque: EditView contient d'autres sous-vues qui ne font pas partie de la hiérarchie des vues de MenuView, mais qui pourraient affecter la réponse.)

Vous connaissez probablement déjà le problème: lorsque ButtonView est dans les limites de MenuView (ou, plus précisément, lorsque mes touches sont dans les limites de MenuView), ButtonView répond aux événements tactiles. Lorsque mes contacts sont en dehors des limites de MenuView (mais toujours dans les limites de ButtonView), aucun événement tactile n'est reçu par ButtonView.

Exemple:

  • (E) est EditView, le parent de toutes les vues
  • (M) est MenuView, une sous-vue de EditView
  • (B) est ButtonView, une sous-vue de MenuView

Diagramme:  

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Parce que (B) est en dehors du cadre de (M), un tap dans la région (B) ne sera jamais envoyé à (M). En fait, (M) n’analysera jamais le toucher dans ce cas et le contact sera envoyé à l'objet suivant dans la hiérarchie.

Objectif: Je suppose que surcharger hitTest:withEvent: peut résoudre ce problème, mais je ne comprends pas exactement comment. Dans mon cas, hitTest:withEvent: devrait-il être remplacé dans EditView (ma vue d'ensemble 'maître')? Ou doit-il être remplacé dans MenuView, la vue directe du bouton qui ne reçoit pas de touches? Ou est-ce que j'y pense de manière incorrecte?

Si cela nécessite une longue explication, une bonne ressource en ligne serait utile - à l'exception de la documentation UIView d'Apple, qui ne m'a pas été clairement expliquée.

Merci!

80
toblerpwn

J'ai modifié le code de la réponse acceptée pour qu'il soit plus générique. Elle gère les cas où la vue découpe les sous-vues à ses limites, peut être masquée et plus important encore: si les sous-vues sont des hiérarchies de vues complexes, la sous-vue correcte sera renvoyée.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

Swift 3

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

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

J'espère que cela aidera tous ceux qui essaient d'utiliser cette solution pour des cas d'utilisation plus complexes.

128
Noam

Ok, j’ai fait des recherches et des tests, voici comment fonctionne hitTest:withEvent - du moins à un niveau élevé. Image ce scénario:

  • (E) est EditView , le parent de toutes les vues
  • (M) est MenuView , une sous-vue de EditView
  • (B) est ButtonView , une sous-vue de MenuView

Diagramme:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

Parce que (B) est en dehors du cadre de (M), un tap dans la région (B) ne sera jamais envoyé à (M). En fait, (M) n’analysera jamais le toucher dans ce cas et le contact sera envoyé à l'objet suivant dans la hiérarchie.

Cependant, si vous implémentez hitTest:withEvent: dans (M), des taps n'importe où dans l'application seront envoyés à (M) (ou au moins, il les connaît). Vous pouvez écrire du code pour gérer le contact dans ce cas et renvoyer l'objet qui doit recevoir le contact.

Plus précisément: l'objectif de hitTest:withEvent: est de renvoyer l'objet qui doit recevoir le hit. Donc, en (M), vous pourriez écrire un code comme celui-ci:

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

Je suis encore très novice en ce qui concerne cette méthode et ce problème. Par conséquent, s'il existe des méthodes plus efficaces ou correctes pour écrire le code, veuillez commenter.

J'espère que cela aidera tout le monde qui pose cette question plus tard. :)

29
toblerpwn

Dans Swift 4

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}
19
duan

Ce que je ferais, c’est que ButtonView et MenuView existent au même niveau dans la hiérarchie des vues en les plaçant tous les deux dans un conteneur dont le cadre s’adapte parfaitement aux deux. De cette façon, la région interactive de l'élément découpé ne sera pas ignorée à cause de ses limites.

2
michael_mackenzie

Si vous avez beaucoup d'autres sous-vues dans votre vue parent, la plupart des autres vues interactives ne fonctionneraient probablement pas si vous utilisiez les solutions ci-dessus. Dans ce cas, vous pouvez utiliser quelque chose comme ceci (Dans Swift 3.2): 

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}
0
paras gupta

Si quelqu'un en a besoin, voici l'alternative Swift

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}
0
Sonny