Interaction au-delà des limites de UIView

92

Est-il possible pour un UIButton (ou tout autre contrôle d'ailleurs) de recevoir des événements tactiles lorsque le cadre de l'UIButton se trouve en dehors du cadre de son parent? Parce que lorsque j'essaye cela, mon UIButton ne semble pas pouvoir recevoir d'événements. Comment contourner ce problème?

ThomasM
la source
1
Cela fonctionne parfaitement pour moi. developer.apple.com/library/ios/qa/qa2013/qa1812.html Best
Frank Li
2
C'est aussi bon et pertinent (ou peut-être une dupe): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Réponses:

93

Oui. Vous pouvez remplacer la hitTest:withEvent:méthode pour renvoyer une vue pour un ensemble de points plus grand que ce que contient cette vue. Consultez la référence de classe UIView .

Edit: Exemple:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(-radius, -radius,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

Edit 2: (Après clarification :) Afin de vous assurer que le bouton est traité comme étant dans les limites du parent, vous devez remplacer pointInside:withEvent:dans le parent pour inclure le cadre du bouton.

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(self.view.bounds, point) ||
        CGRectContainsPoint(button.view.frame, point))
    {
        return YES;
    }
    return NO;
}

Remarque le code juste là pour remplacer pointInside n'est pas tout à fait correct. Comme Summon l'explique ci-dessous, procédez comme suit:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
    if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
        return YES;

    return [super pointInside:point withEvent:event];
    }

Notez que vous le feriez très probablement avec self.oversizeButtonun IBOutlet dans cette sous-classe UIView; alors vous pouvez simplement faire glisser le "bouton surdimensionné" en question, vers la vue spéciale en question. (Ou, si pour une raison quelconque vous faisiez beaucoup cela dans un projet, vous auriez une sous-classe UIButton spéciale, et vous pourriez regarder dans votre liste de sous-vues pour ces classes.) J'espère que cela vous aidera.

jnic
la source
Pouvez-vous m'aider davantage à implémenter cela correctement avec un exemple de code? J'ai également lu la référence et à cause de la ligne suivante, je suis confus: "Les points qui se trouvent en dehors des limites du receveur ne sont jamais signalés comme des hits, même s'ils se trouvent réellement dans l'une des sous-vues du récepteur. Les sous-vues peuvent s'étendre visuellement au-delà des limites. de leur parent si la propriété clipsToBounds de la vue parent est définie sur NON. Cependant, le test de positionnement ignore toujours les points en dehors des limites de la vue parent. "
ThomasM
C'est surtout cette partie qui donne l'impression que ce n'est pas la solution dont j'ai besoin: "Cependant, les tests de résultats ignorent toujours les points en dehors des limites de la vue parente." ..
ThomasM
Ah, je comprends maintenant. Vous devez remplacer le parent pointInside:withEvent:pour inclure le cadre du bouton. Je vais ajouter un exemple de code à ma réponse ci-dessus.
jnic
Existe-t-il un moyen de faire cela sans remplacer les méthodes d'un UIView? Par exemple, si vos vues sont entièrement génériques pour UIKit et initialisées via un Nib, pouvez-vous remplacer ces méthodes depuis votre UIViewController?
RLH
1
hitTest:withEvent:et la pointInside:withEvent:zone ne m'a pas appelé lorsque j'appuie sur un bouton qui se trouve en dehors des limites des parents. Qu'est-ce qui ne va pas?
iosdude
24

@jnic, je travaille sur iOS SDK 5.0 et pour que votre code fonctionne correctement, je devais faire ceci:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
    return YES;
}
return [super pointInside:point withEvent:event]; }

La vue conteneur dans mon cas est un UIButton et tous les éléments enfants sont également des UIButtons qui peuvent se déplacer en dehors des limites de l'UIButton parent.

Meilleur

Convoquer
la source
20

Dans la vue parent, vous pouvez remplacer la méthode de test de positionnement:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];

    if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
        return [_myButton hitTest:translatedPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];

}

Dans ce cas, si le point se situe dans les limites de votre bouton, vous y transférez l'appel; sinon, revenez à l'implémentation d'origine.

Jack
la source
17

Dans mon cas, j'avais une UICollectionViewCellsous-classe qui contenait un fichier UIButton. J'ai désactivé clipsToBoundsla cellule et le bouton était visible en dehors des limites de la cellule. Cependant, le bouton ne recevait pas d'événements tactiles. J'ai pu détecter les événements tactiles sur le bouton en utilisant la réponse de Jack: https://stackoverflow.com/a/30431157/3344977

Voici une version Swift:

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

    let translatedPoint = button.convertPoint(point, fromView: self)

    if (CGRectContainsPoint(button.bounds, translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, withEvent: event)
    }
    return super.hitTest(point, withEvent: event)
}

Swift 4:

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

    let translatedPoint = button.convert(point, from: self)

    if (button.bounds.contains(translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, with: event)
    }
    return super.hitTest(point, with: event)
}
user3344977
la source
C'était exactement mon scénario. L'autre point est que vous devez placer cette méthode dans la classe CollectionViewCell.
Hamid Reza Ansari
Mais pourquoi injecteriez-vous le bouton dans une méthode remplacée ???
rommex
10

Pourquoi cela se produit-il?

En effet, lorsque votre sous-vue se trouve en dehors des limites de votre supervision, les événements tactiles qui se produisent réellement sur cette sous-vue ne seront pas livrés à cette sous-vue. Cependant, il FERA être livré à son superview.

Indépendamment du fait que les sous-vues soient coupées visuellement ou non, les événements tactiles respectent toujours le rectangle de limites de la vue de surveillance de la vue cible. En d'autres termes, les événements tactiles se produisant dans une partie d'une vue située en dehors du rectangle de limites de sa vue supervisée ne sont pas transmis à cette vue. Lien

Qu'as tu besoin de faire?

Lorsque votre superview reçoit l'événement tactile mentionné ci-dessus, vous devrez indiquer explicitement à UIKit que ma sous-vue doit être celle qui recevra cet événement tactile.

Et le code?

Dans votre supervision, implémentez func hitTest(_ point: CGPoint, with event: UIEvent?)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
        // convert the point into subview's coordinate system
        let subviewPoint = self.convert(point, to: subview)
        // if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
        if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
        return super.hitTest(point, with: event)
    }

Gotchya fascinant: vous devez aller à la "vue la plus haute trop petite"

Vous devez aller "vers le haut" jusqu'à la vue "la plus élevée" à laquelle la vue du problème se trouve à l'extérieur.

Exemple typique:

Supposons que vous ayez un écran S, avec une vue conteneur C.La vue du contrôleur de vue conteneur est V. (Rappelez-vous que V sera placé à l'intérieur de C et aura la même taille.) V a une sous-vue (peut-être un bouton) B.B est le problème vue qui est en fait en dehors de V.

Mais notez que B est également en dehors de C.

Dans cet exemple , vous devez appliquer la solution override hitTesten fait à C, et non à V . Si vous l'appliquez à V - cela ne fait rien.

Scott Zhu
la source
4
Merci pour ça! J'ai passé des heures et des heures sur un problème qui en découle. J'apprécie vraiment que vous preniez le temps de publier ceci. :)
ClayJ
@ScottZhu: J'ai ajouté un gotchya important à votre réponse. Bien sûr, vous devriez vous sentir libre de supprimer ou de modifier mon ajout! Merci encore!
Fattie
9

Rapide:

Où targetView est la vue que vous souhaitez recevoir l'événement (qui peut être entièrement ou partiellement situé en dehors du cadre de sa vue parente).

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        let pointForTargetView: CGPoint = targetView.convertPoint(point, fromView: self)
        if CGRectContainsPoint(targetView.bounds, pointForTargetView) {
            return closeButton
        }
        return super.hitTest(point, withEvent: event)
}
Luke Bartolomeo
la source
5

Pour moi (comme pour les autres), la réponse n'était pas de remplacer la hitTestméthode, mais plutôt lapointInside méthode. Je devais le faire à seulement deux endroits.

Le Superview C'est l'opinion que si elle avait été clipsToBoundsmise à vrai, cela ferait disparaître tout ce problème. Alors, voici mon simple UIViewdans Swift 3:

class PromiscuousView : UIView {
    override func point(inside point: CGPoint,
               with event: UIEvent?) -> Bool {
        return true
    }
} 

La sous-vue Votre kilométrage peut varier, mais dans mon cas, j'ai également dû remplacer la pointInsideméthode de la sous-vue qui avait décidé que le toucher était hors limites. Le mien est en Objective-C, donc:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    WEPopoverController *controller = (id) self.delegate;
    if (self.takeHitsOutside) {
        return YES;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

Cette réponse (et quelques autres sur la même question) peut vous aider à éclaircir votre compréhension.

Dan Rosenstark
la source
2

swift 4.1 mis à jour

Pour obtenir des événements tactiles dans le même UIView, il vous suffit d'ajouter la méthode de remplacement suivante, elle identifiera la vue spécifique tapée dans UIView qui est placée hors des limites

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let pointforTargetview:CGPoint = self.convert(point, to: btnAngle)
    if  btnAngle.bounds.contains(pointforTargetview) {
        return  self
    }
    return super.hitTest(point, with: event)
}
Ankit Kushwah
la source