Frage Berührungen einer Unteransicht außerhalb des Rahmens ihrer Superview mit hitTest: withEvent:


Mein Problem: Ich habe eine super Aussicht EditView das nimmt im Grunde den gesamten Anwendungsrahmen und eine Unteransicht auf MenuView die nur die unteren ~ 20% aufnimmt, und dann MenuView enthält eine eigene Unteransicht ButtonView die sich tatsächlich außerhalb von befindet MenuViewGrenzen (etwa so: ButtonView.frame.origin.y = -100).

(Hinweis: EditView hat andere Teilansichten, die nicht Teil von sind MenuViewAnsichtshierarchie, kann aber die Antwort beeinflussen.)

Sie kennen das Problem wahrscheinlich schon: Wann? ButtonView liegt innerhalb der Grenzen von MenuView (oder genauer gesagt, wenn meine Berührungen innerhalb sind MenuView's Grenzen), ButtonView reagiert auf Berührungsereignisse. Wenn meine Berührungen außerhalb sind MenuViewGrenzen (aber immer noch innerhalb ButtonView's Grenzen), wird kein Berührungsereignis empfangen ButtonView.

Beispiel:

  • (E) ist EditView, das Elternteil aller Ansichten
  • (M) ist MenuView, eine Unteransicht von EditView
  • (B) ist ButtonView, eine Unteransicht von MenuView

Diagramm: 

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

Da (B) außerhalb des Rahmens von (M) liegt, wird ein Antippen in der Region (B) niemals zu (M) gesendet - tatsächlich analysiert (M) in diesem Fall niemals die Berührung, und die Berührung wird an gesendet das nächste Objekt in der Hierarchie.

Tor: Ich verstehe das übertreiben hitTest:withEvent: kann dieses Problem lösen, aber ich verstehe nicht genau wie. In meinem Fall sollte hitTest:withEvent: überschrieben werden EditView (mein "Meister" Superview)? Oder sollte es in überschrieben werden MenuView, die direkte Superansicht des Knopfes, der keine Berührungen erhält? Oder denke ich darüber falsch?

Wenn dies eine langwierige Erklärung erfordert, wäre eine gute Online-Ressource hilfreich - außer Apples UIView-Dokumenten, die mir nicht klar sind.

Vielen Dank!


76
2017-08-02 03:41


Ursprung


Antworten:


Ich habe den Code der akzeptierten Antwort so modifiziert, dass er generischer ist - er behandelt die Fälle, in denen die Ansicht Subviews an ihre Grenzen klammert, möglicherweise ausgeblendet ist und, was noch wichtiger ist: Wenn die Subviews komplexe View-Hierarchien sind, wird die richtige Subview zurückgegeben.

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

Ich hoffe, dies hilft jedem, der versucht, diese Lösung für komplexere Anwendungsfälle zu verwenden.


123
2018-02-14 13:13



Ok, ich habe etwas gegraben und getestet, hier ist wie hitTest:withEvent funktioniert - zumindest auf hohem Niveau. Bild dieses Szenario:

  • (E) ist EditView, das Elternteil aller Ansichten
  • (M) ist MenuView, eine Unteransicht von EditView
  • (B) ist ButtonView, eine Unteransicht von MenuView

Diagramm:

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

Da (B) außerhalb des Rahmens von (M) liegt, wird ein Antippen in der Region (B) niemals zu (M) gesendet - tatsächlich analysiert (M) in diesem Fall niemals die Berührung, und die Berührung wird an gesendet das nächste Objekt in der Hierarchie.

Wenn Sie jedoch implementieren hitTest:withEvent: In (M) werden Taps irgendwo in der Anwendung an (M) gesendet (oder am wenigsten weiß es über sie). Sie können in diesem Fall Code schreiben, der die Berührung behandelt, und das Objekt zurückgeben, das die Berührung erhalten soll.

Genauer gesagt: das Ziel von hitTest:withEvent: ist, das Objekt zurückzugeben, das den Treffer erhalten soll. In (M) könnte man also Code schreiben:

// 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];
}

Ich bin immer noch sehr neu in dieser Methode und dieses Problem, also, wenn es effizienter oder korrekte Möglichkeiten gibt, den Code zu schreiben, kommentieren Sie bitte.

Ich hoffe, das hilft jedem anderen, der diese Frage später stellt. :)


26
2017-08-03 17:39



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

18
2018-03-28 14:24



Ich würde sowohl ButtonView als auch MenuView auf derselben Ebene in der Ansichtshierarchie verwenden, indem Sie beide in einem Container platzieren, dessen Rahmen beide vollständig enthält. Auf diese Weise wird die interaktive Region des ausgeschnittenen Objekts nicht ignoriert, da es die Grenzen der Superviews darstellt.


2
2017-08-02 04:07



Wenn jemand es braucht, hier ist die schnelle Alternative

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
2017-08-26 17:16



Wenn Sie viele andere Unteransichten in Ihrer Elternansicht haben, funktionieren wahrscheinlich die meisten anderen interaktiven Ansichten nicht, wenn Sie obige Lösungen verwenden. In diesem Fall können Sie so etwas verwenden (In 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
2017-11-03 06:51



Platzieren Sie die folgenden Codezeilen in Ihrer Ansichtshierarchie:

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
    UIView* hitView = [super hitTest:point withEvent:event];
    if (hitView != nil)
    {
        [self.superview bringSubviewToFront:self];
    }
    return hitView;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

Zur Verdeutlichung wurde in meinem Blog erklärt: "goaheadwithiphonetech" bezüglich "Custom Callout: Button ist nicht anklickbar".

Ich hoffe das hilft dir ... !!!


-1
2018-05-19 10:50