Pytanie Przechwytywanie elementów wyś wietlanych poza kadrem swojego superviewu za pomocĘ ... parametru hitTest: withEvent:


Mój problem: Mam superview EditView który zajmuje zasadniczo całą ramkę aplikacji i wywiady MenuView który zajmuje tylko dolny ~ 20%, a następnie MenuView zawiera własny wydział ButtonView który faktycznie znajduje się poza MenuViewGranice (coś takiego: ButtonView.frame.origin.y = -100).

(Uwaga: EditView ma inne subviews, które nie są częścią MenuViewhierarchia widoku, ale może wpłynąć na odpowiedź.)

Prawdopodobnie już znasz problem: kiedy ButtonView mieści się w granicach MenuView (a dokładniej, gdy moje dotknięcia są w środku MenuViewgranice), ButtonView reaguje na zdarzenia dotykowe. Kiedy moje dotknięcia są poza MenuView(ale wciąż w środku ButtonViewgranice), żadne zdarzenie dotykowe nie jest odbierane przez ButtonView.

Przykład:

  • (E) jest EditView, rodzic wszystkich widoków
  • (M) jest MenuView, wyeksportowany widok EditView
  • (B) jest ButtonView, subview of MenuView

Diagram: 

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

Ponieważ (B) jest ramką zewnętrzną (M), uderzenie w obszarze (B) nigdy nie zostanie wysłane do (M) - w rzeczywistości (M) nigdy nie analizuje dotyku w tym przypadku, a dotyk jest wysyłany do następny obiekt w hierarchii.

Cel: Zbieram to, że się nad tym zastanawiam hitTest:withEvent: może rozwiązać ten problem, ale nie rozumiem dokładnie jak. W moim przypadku powinien hitTest:withEvent: być nadpisanym EditView (mój "master" superview)? A może powinien zostać nadpisany? MenuView, bezpośredni podgląd przycisku, który nie otrzymuje dotknięć? Czy też myślę o tym niepoprawnie?

Jeśli wymaga to długich wyjaśnień, przydatny mógłby być dobry zasób online - z wyjątkiem dokumentów UIView firmy Apple, które nie wyjaśniły mi tego.

Dzięki!


76
2017-08-02 03:41


pochodzenie




Odpowiedzi:


Zmodyfikowałem kod zaakceptowanej odpowiedzi, aby był bardziej ogólny - obsługuje przypadki, w których widok klipuje subskrybcje do jego granic, może być ukryty, a co ważniejsze: jeśli podelementy są złożonymi hierarchiami widoków, poprawny podgląd zostanie zwrócony.

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

Mam nadzieję, że pomoże to każdemu, kto spróbuje użyć tego rozwiązania w bardziej złożonych przypadkach użycia.


123
2018-02-14 13:13



To wygląda dobrze i dzięki za to. Miał jednak jedno pytanie: dlaczego robisz return [super hitTest: point withEvent: event]; ? Czy po prostu nie wrócisz do zera, jeśli nie ma subview, który wyzwala dotyk? Apple twierdzi, że hitTest zwraca zero, jeśli żadne wywiady nie zawierają dotyku. - Ser Pounce
Hm ... Tak, to brzmi jak należy. Ponadto, aby zachować prawidłowe zachowanie, obiekty muszą być iterowane w odwrotnej kolejności (ponieważ ostatnia jest najbardziej widoczna). Zmieniono kod do dopasowania. - Noam
Idealnie, to pomogło mi stworzyć niestandardowe menu boczne dla mojego projektu, w którym wprowadzam niestandardowe UIButtons jako menu - Jason Renaldo
Właśnie użyłem twojego rozwiązania, aby UIButton uchwycił dotyk, i jest wewnątrz UIView, który jest wewnątrz UICollectionViewCell wewnątrz (oczywiście) UICollectionView. Musiałem podklasę UICollectionView i UICollectionViewCell zastąpić hitTest: withEvent: na tych trzech klasach. I działa jak urok !! Dzięki !! - Daniel García
W zależności od pożądanego zastosowania powinien on albo zwrócić [super hitTest: point withEvent: event] lub zero. Powracanie samego siebie spowoduje, że otrzyma wszystko. - Noam


Ok, zrobiłem kopanie i testowanie, oto jak hitTest:withEvent działa - przynajmniej na wysokim poziomie. Obraz ten scenariusz:

  • (E) to EditView, rodzic wszystkich widoków
  • (M) to MenuView, wyeksportowany widok EditView
  • (B) to ButtonView, subview of MenuView

Diagram:

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

Ponieważ (B) jest ramką zewnętrzną (M), uderzenie w obszarze (B) nigdy nie zostanie wysłane do (M) - w rzeczywistości (M) nigdy nie analizuje dotyku w tym przypadku, a dotyk jest wysyłany do następny obiekt w hierarchii.

Jeśli jednak wdrożysz hitTest:withEvent: w (M) dotknięcia w dowolnym miejscu w aplikacji będą wysyłane do (M) (lub przynajmniej o nich wie). Możesz napisać kod, aby poradzić sobie z dotykiem w tym przypadku i zwrócić obiekt, który powinien otrzymać dotyk.

Dokładniej: celem hitTest:withEvent: jest zwrócenie obiektu, który powinien otrzymać trafienie. Tak więc w (M) możesz napisać kod w ten sposób:

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

Wciąż jestem nowicjuszem w tej metodzie i tym problemie, więc jeśli istnieją bardziej wydajne lub poprawne sposoby napisania kodu, prosimy o komentarz.

Mam nadzieję, że pomoże to każdemu, kto trafi na to pytanie później. :)


26
2017-08-03 17:39



Dzięki, to działało dla mnie, chociaż musiałem zrobić trochę więcej logiki, aby określić, które z subviews, które powinny faktycznie otrzymywać trafienia. Usunąłem niepotrzebną przerwę ze swojego przykładu btw. - Daniel Saidi
Gdy ramka podglądu była ramką rodzica, kliknięcie zdarzenia nie odpowiadało! Twoje rozwiązanie naprawiło ten problem. Wielkie dzięki :-) - byJeevan


W 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





Co mogę zrobić, to zarówno ButtonView i MenuView istnieją na tym samym poziomie w hierarchii widoku, umieszczając je zarówno w kontenerze, którego ramka całkowicie pasuje do nich obu. W ten sposób region interaktywny obciętego przedmiotu nie zostanie zignorowany z powodu jego granic superwizji.


2
2017-08-02 04:07



myślałem także o tym obejściu - oznacza to, że będę musiała zduplikować logikę miejsc docelowych (lub zmienić jakiś poważny kod!), ale może to być w końcu mój najlepszy wybór. - toblerpwn


Jeśli ktoś tego potrzebuje, oto szybka alternatywa

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





Jeśli masz wiele innych subskrybentów w widoku rodzica, prawdopodobnie większość innych interaktywnych widoków nie zadziała, jeśli użyjesz powyższych rozwiązań, w takim przypadku możesz użyć czegoś podobnego (w 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





Umieść poniższe wiersze kodu w hierarchii widoku:

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

Aby uzyskać więcej wyjaśnień, wyjaśniono na moim blogu: "goaheadwithiphonetech" dotyczące "Niestandardowe objaśnienie: Przycisk nie jest klikalny".

Mam nadzieję, że to pomoże ... !!!


-1
2018-05-19 10:50



Twój blog został usunięty, więc gdzie możemy znaleźć wyjaśnienie? - ishahak