Interaktion über die Grenzen von UIView hinaus

91

Kann ein UIButton (oder ein anderes Steuerelement) Berührungsereignisse empfangen, wenn der Rahmen des UIButton außerhalb des Rahmens des übergeordneten Elements liegt? Wenn ich dies versuche, scheint mein UIButton keine Ereignisse empfangen zu können. Wie kann ich das umgehen?

ThomasM
quelle
1
Das funktioniert perfekt für mich. developer.apple.com/library/ios/qa/qa2013/qa1812.html Best
Frank Li
2
Dies ist auch gut und relevant (oder vielleicht ein Betrüger): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Antworten:

93

Ja. Sie können die hitTest:withEvent:Methode überschreiben , um eine Ansicht für einen größeren Satz von Punkten zurückzugeben, als diese Ansicht enthält. Siehe die UIView-Klassenreferenz .

Bearbeiten: Beispiel:

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

Bearbeiten 2: (Nach Klarstellung :) Um sicherzustellen, dass die Schaltfläche innerhalb der Grenzen des übergeordneten Elements behandelt wird, müssen Sie das übergeordnete Element überschreiben pointInside:withEvent:, um den Rahmen der Schaltfläche einzuschließen.

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

Beachten Sie, dass der Code nur zum Überschreiben von pointInside nicht ganz korrekt ist. Gehen Sie wie folgt vor, wie Summon unten erklärt:

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

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

Beachten Sie, dass Sie dies höchstwahrscheinlich self.oversizeButtonals IBOutlet in dieser UIView-Unterklasse tun würden. Dann können Sie einfach die betreffende Schaltfläche "Übergröße" in die betreffende Sonderansicht ziehen. (Wenn Sie dies aus irgendeinem Grund häufig in einem Projekt tun, haben Sie eine spezielle UIButton-Unterklasse, und Sie können Ihre Unteransichtsliste nach diesen Klassen durchsuchen.) Ich hoffe, es hilft.

jnic
quelle
Können Sie mir bei der korrekten Implementierung mit einem Beispielcode weiterhelfen? Außerdem habe ich die Referenz gelesen und bin aufgrund der folgenden Zeile verwirrt: "Punkte, die außerhalb der Grenzen des Empfängers liegen, werden niemals als Treffer gemeldet, selbst wenn sie tatsächlich innerhalb einer der Unteransichten des Empfängers liegen. Unteransichten können sich visuell über die Grenzen hinaus erstrecken des übergeordneten Elements, wenn die clipsToBounds-Eigenschaft der übergeordneten Ansicht auf NO festgelegt ist. Beim Treffer-Test werden jedoch immer Punkte außerhalb der Grenzen der übergeordneten Ansicht ignoriert. "
ThomasM
Es ist besonders dieser Teil, der den Anschein erweckt, als wäre dies nicht die Lösung, die ich brauche: "Beim Treffer-Testen werden jedoch immer Punkte außerhalb der Grenzen der übergeordneten Ansicht ignoriert." Aber ich könnte mich natürlich irren, ich finde die Apfelreferenzen manchmal wirklich verwirrend ..
ThomasM
Ah, ich verstehe jetzt. Sie müssen die übergeordneten Elemente überschreiben pointInside:withEvent:, um den Rahmen der Schaltfläche einzuschließen. Ich werde meiner obigen Antwort einen Beispielcode hinzufügen.
Jnic
Gibt es eine Möglichkeit, dies zu tun, ohne die Methoden eines UIView zu überschreiben? Können Sie diese Methoden beispielsweise in Ihrem UIViewController überschreiben, wenn Ihre Ansichten für UIKit vollständig generisch sind und über eine Schreibfeder initialisiert werden?
RLH
1
hitTest:withEvent:und pointInside:withEvent:Bereich, der nicht für mich aufgerufen wird, wenn ich auf einen Knopf drücke, der außerhalb der Grenzen der Eltern liegt. Was kann falsch sein?
Iosdude
24

@jnic, ich arbeite an iOS SDK 5.0 und damit Ihr Code richtig funktioniert, musste ich Folgendes tun:

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

Die Containeransicht ist in meinem Fall ein UIButton, und alle untergeordneten Elemente sind auch UIButtons, die sich außerhalb der Grenzen des übergeordneten UIButton bewegen können.

Beste

Beschwörung
quelle
20

In der übergeordneten Ansicht können Sie die Treffer-Testmethode überschreiben:

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

}

In diesem Fall leiten Sie den Anruf dort weiter, wenn der Punkt innerhalb der Grenzen Ihrer Schaltfläche liegt. Wenn nicht, kehren Sie zur ursprünglichen Implementierung zurück.

Jack
quelle
16

In meinem Fall hatte ich eine UICollectionViewCellUnterklasse, die a enthielt UIButton. Ich habe clipsToBoundsdie Zelle deaktiviert und die Schaltfläche war außerhalb der Zellengrenzen sichtbar. Die Schaltfläche empfing jedoch keine Berührungsereignisse. Ich konnte die Berührungsereignisse auf der Schaltfläche mithilfe von Jacks Antwort erkennen: https://stackoverflow.com/a/30431157/3344977

Hier ist eine Swift-Version:

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
quelle
Das war genau mein Szenario. Der andere Punkt ist, dass Sie diese Methode in die CollectionViewCell-Klasse einfügen sollten.
Hamid Reza Ansari
Aber warum sollten Sie den Button einer überschriebenen Methode hinzufügen?
Rommex
10

Warum passiert das?

Dies liegt daran, dass Berührungsereignisse, die tatsächlich in dieser Unteransicht auftreten, nicht an diese Unteransicht gesendet werden, wenn Ihre Unteransicht außerhalb der Grenzen Ihrer Übersicht liegt. Es ist jedoch WILL seine Superview geliefert werden.

Unabhängig davon, ob Unteransichten visuell abgeschnitten sind oder nicht, berücksichtigen Berührungsereignisse immer das Begrenzungsrechteck der Übersichtsansicht der Zielansicht. Mit anderen Worten, Berührungsereignisse, die in einem Teil einer Ansicht auftreten, der außerhalb des Rechtecks ​​der Begrenzungsansicht liegt, werden nicht an diese Ansicht übergeben. Verknüpfung

Was musst du machen?

Wenn Ihre Übersicht das oben erwähnte Berührungsereignis empfängt, müssen Sie UIKit ausdrücklich mitteilen, dass meine Unteransicht diejenige sein sollte, die dieses Berührungsereignis empfängt.

Was ist mit dem Code?

Implementieren Sie in Ihrer Übersicht 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)
    }

Faszinierende Gotchya: Sie müssen zur "höchsten zu kleinen Übersicht" gehen

Sie müssen "nach oben" zur "höchsten" Ansicht gehen, in der sich die Problemansicht befindet.

Typisches Beispiel:

Angenommen, Sie haben einen Bildschirm S mit einer Containeransicht C. Die Ansicht des Container Viewcontrollers ist V. (Rückruf V befindet sich innerhalb von C und hat die gleiche Größe.) V hat eine Unteransicht (möglicherweise eine Schaltfläche) B. B ist das Problem Ansicht, die tatsächlich außerhalb von V liegt.

Beachten Sie jedoch, dass B auch außerhalb von C liegt.

In diesem Beispiel haben Sie die Lösung anzuwenden override hitTestund zwar bis C, nicht zu V . Wenn Sie es auf V anwenden, tut es nichts.

Scott Zhu
quelle
4
Danke dafür! Ich habe Stunden und Stunden mit einem Problem verbracht, das sich daraus ergibt. Ich freue mich sehr, dass Sie sich die Zeit genommen haben, dies zu posten. :)
ClayJ
@ScottZhu: Ich habe Ihrer Antwort ein wichtiges Gotchya hinzugefügt. Natürlich können Sie meinen Zusatz löschen oder ändern! Danke noch einmal!
Fattie
9

Schnell:

Wobei targetView die Ansicht ist, für die Sie das Ereignis empfangen möchten (die sich ganz oder teilweise außerhalb des Rahmens der übergeordneten Ansicht befinden kann).

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
quelle
5

Für mich (wie für andere) bestand die Antwort nicht darin , die hitTestMethode zu überschreiben , sondern die pointInsideMethode. Ich musste das nur an zwei Stellen tun.

Die Übersicht Dies ist die Ansicht, dass clipsToBoundsdas gesamte Problem verschwinden würde , wenn es auf true gesetzt wäre. Also hier ist meine einfache UIViewin Swift 3:

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

Die Unteransicht Ihr Kilometerstand kann variieren, aber in meinem Fall musste ich auch die pointInsideMethode für die Unteransicht überschreiben , bei der entschieden wurde, dass die Berührung außerhalb der Grenzen liegt. Meins ist in Ziel-C, also:

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

Diese Antwort (und einige andere auf dieselbe Frage) kann Ihnen helfen, Ihr Verständnis zu verbessern.

Dan Rosenstark
quelle
2

Swift 4.1 aktualisiert

Um Berührungsereignisse in derselben UIView zu erhalten, müssen Sie nur die folgende Überschreibungsmethode hinzufügen. Dadurch wird eine bestimmte Ansicht identifiziert, die in UIView getippt wurde und außerhalb der Grenzen liegt

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
quelle