web-dev-qa-db-fra.com

Interaction au-delà des limites de UIView

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

84
ThomasM

Oui. Vous pouvez remplacer le hitTest:withEvent: méthode pour renvoyer une vue pour un ensemble de points plus grand que cette vue ne contient. Voir Référence de classe UIView .

Modifier: 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;
}

Édition 2: (Après clarification :) Afin de garantir 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 le 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.oversizeButton en tant qu'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 parcourir votre liste de sous-vues pour ces classes.) J'espère que cela vous aidera.

91
jnic

@jnic, je travaille sur iOS SDK 5.0 et pour que votre code fonctionne correctement, j'ai dû faire ceci:

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

La vue du 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 du parent UIButton.

Meilleur

23
Summon

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

- (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 renvoyez l'appel; sinon, revenez à l'implémentation d'origine.

19
Ja͢ck

Dans mon cas, j'avais une sous-classe UICollectionViewCell qui contenait un UIButton. J'ai désactivé clipsToBounds sur la 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)
}
11
user3344977

Rapide:

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

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)
}
8
Luke Bartolomeo

Pourquoi cela se produit-il?

En effet, lorsque votre sous-vue se trouve en dehors des limites de votre vue d'ensemble, les événements tactiles qui se produisent réellement sur cette sous-vue ne seront pas transmis à cette sous-vue. Cependant, il [~ # ~] sera [~ # ~] livré dans sa vue d'ensemble.

Que les sous-vues soient écrêtées visuellement ou non, les événements tactiles respectent toujours le rectangle des limites de la vue d'ensemble 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 des limites de sa vue d'ensemble ne sont pas transmis à cette vue. Lien

Ce que vous devez faire?

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

Qu'en est-il du code?

Dans votre vue d'ensemble, 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 vous rendre à la "superview trop petite la plus haute"

Vous devez aller "vers le haut" à la vue "la plus élevée" à laquelle la vue du problème est extérieure.

Exemple typique:

Disons que vous avez un écran S, avec une vue de conteneur C. La vue du contrôleur de vue de conteneur est V. (Le rappel V sera assis à l'intérieur de C et sera de 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 hitTest en fait à C, pas à V . Si vous l'appliquez à V - cela ne fait rien.

8
Scott Zhu

Pour moi (comme pour les autres), la réponse était non pas pour remplacer la méthode hitTest, mais plutôt la pointInside méthode. J'ai dû le faire à deux endroits seulement.

Le Superview C'est la vue que si clipsToBounds était réglé sur true, cela ferait disparaître tout ce problème. Voici donc mon simple UIView en 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û écraser la méthode pointInside pour la sous-vue qui avait décidé la 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 aider à clarifier votre compréhension.

3
Dan Rosenstark

Swift 4.1 mis à jour

Pour obtenir des événements tactiles dans la 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 limite

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)
}
2
Ankit Kushwah