Interakcja poza granicami UIView

94

Czy możliwe jest, aby UIButton (lub jakakolwiek inna kontrolka w tym zakresie) odbierała zdarzenia dotyku, gdy ramka UIButton znajduje się poza ramką swojego rodzica? Ponieważ kiedy próbuję tego, mój UIButton nie wydaje się być w stanie odbierać żadnych zdarzeń. Jak mam to obejść?

ThomasM
źródło
1
U mnie to działa idealnie. developer.apple.com/library/ios/qa/qa2013/qa1812.html Najlepsze
Frank Li
2
Jest to również dobre i istotne (a może fałszywe): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Odpowiedzi:

94

Tak. Możesz nadpisać hitTest:withEvent:metodę, aby zwrócić widok dla większego zestawu punktów niż zawiera ten widok. Zobacz odwołanie do klasy UIView .

Edycja: Przykład:

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

Edycja 2: (Po wyjaśnieniu :) Aby upewnić się, że przycisk jest traktowany jako znajdujący się w granicach rodzica, musisz nadpisać pointInside:withEvent:w rodzicu, aby uwzględnić ramkę przycisku.

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

Zwróć uwagę, że kod właśnie tam zastępujący pointInside nie jest całkiem poprawny. Jak Summon wyjaśnia poniżej, zrób to:

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

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

Zwróć uwagę, że najprawdopodobniej zrobiłbyś to self.oversizeButtonjako IBOutlet w tej podklasie UIView; następnie możesz po prostu przeciągnąć dany „przycisk oversize” do odpowiedniego widoku specjalnego. (Lub, jeśli z jakiegoś powodu robiłeś to często w projekcie, miałbyś specjalną podklasę UIButton i mógłbyś przejrzeć listę podrzędnych tych klas.) Mam nadzieję, że to pomoże.

jnic
źródło
Czy możesz mi pomóc w poprawnym wdrożeniu tego za pomocą przykładowego kodu? Przeczytałem również odniesienie i z powodu następującego wiersza jestem zdezorientowany: „Punkty leżące poza granicami odbiorcy nigdy nie są zgłaszane jako trafienia, nawet jeśli w rzeczywistości znajdują się w jednym z podwidoków odbiorcy. Podglądy mogą wizualnie rozciągać się poza granice ich rodzica, jeśli właściwość clipsToBounds widoku nadrzędnego ma wartość NIE. Jednak testowanie trafień zawsze ignoruje punkty poza granicami widoku nadrzędnego. "
ThomasM
Szczególnie ta część sprawia, że ​​wydaje mi się, że nie jest to rozwiązanie, którego potrzebuję: „Jednak test trafień zawsze ignoruje punkty poza granicami widoku rodzica” .. Ale mogę się oczywiście mylić, odniesienia do jabłek są czasami bardzo mylące ..
ThomasM
Ach, teraz rozumiem. Musisz zastąpić rodzica, pointInside:withEvent:aby uwzględnić ramkę przycisku. Dodam przykładowy kod do mojej odpowiedzi powyżej.
jnic
Czy istnieje sposób na zrobienie tego bez zastępowania metod UIView? Na przykład, jeśli Twoje widoki są całkowicie ogólne dla UIKit i zainicjowane przez Nib, czy możesz zastąpić te metody z poziomu kontrolera UIViewController?
RLH
1
hitTest:withEvent:i pointInside:withEvent:obszar, który mnie nie wzywał, kiedy naciskam przycisk znajdujący się poza granicami rodziców. Co może być nie tak?
iosdude
24

@jnic, pracuję na iOS SDK 5.0 i aby twój kod działał poprawnie, musiałem zrobić to:

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

Widok kontenera w moim przypadku to UIButton, a wszystkie elementy podrzędne są również UIButtonami, które mogą poruszać się poza granice nadrzędnego UIButton.

Najlepsza

Wezwać
źródło
20

W widoku nadrzędnym możesz zmienić metodę testu trafień:

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

}

W takim przypadku, jeśli punkt mieści się w granicach twojego przycisku, przekierowujesz tam połączenie; jeśli nie, wróć do pierwotnej implementacji.

Jacek
źródło
18

W moim przypadku miałem UICollectionViewCellpodklasę zawierającą plik UIButton. Wyłączyłem clipsToBoundsna komórce i przycisk był widoczny poza granicami komórki. Jednak przycisk nie odbierał zdarzeń dotykowych. Udało mi się wykryć zdarzenia dotyku na przycisku, korzystając z odpowiedzi Jacka: https://stackoverflow.com/a/30431157/3344977

Oto wersja 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
źródło
To był dokładnie mój scenariusz. Inną kwestią jest to, że należy umieścić tę metodę wewnątrz klasy CollectionViewCell.
Hamid Reza Ansari
Ale dlaczego miałbyś wstrzyknąć przycisk do metody zastąpionej ???
rommex
10

Dlaczego tak się dzieje?

Dzieje się tak, ponieważ gdy widok podrzędny znajduje się poza granicami podglądu, zdarzenia dotykowe, które faktycznie mają miejsce w tym podglądzie, nie zostaną do niego dostarczone. Jednakże BĘDZIE być dostarczony do SuperView.

Bez względu na to, czy podglądy są przycinane wizualnie, czy nie, zdarzenia dotykowe zawsze uwzględniają prostokąt z granicami superwidoku docelowego widoku. Innymi słowy, zdarzenia dotykowe występujące w części widoku, która znajduje się poza prostokątem granic superwizji, nie są dostarczane do tego widoku. Połączyć

Co musisz zrobić?

Kiedy Twój nadzór otrzyma wspomniane powyżej zdarzenie dotyku, musisz wyraźnie powiedzieć UIKit, że moja subview powinna otrzymywać to zdarzenie dotykowe.

A co z kodem?

W swoim nadzorze, zaimplementuj 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)
    }

Fascynująca gotchya: musisz przejść do „najwyższego zbyt małego superwizji”

Musisz wejść „w górę” do „najwyższego” widoku, którego widok problemu znajduje się na zewnątrz.

Typowy przykład:

Powiedzmy, że masz ekran S z widokiem kontenera C. Widok kontrolera widoku kontenera to V. (Przypomnij sobie, że V będzie znajdować się w C i będzie identycznego rozmiaru). V ma widok podrzędny (może przycisk) B. B to problem widok, który faktycznie znajduje się poza V.

Ale pamiętaj, że B jest również poza C.

W tym przykładzie trzeba zastosować rozwiązanie override hitTestw rzeczywistości do C, a nie V . Jeśli zastosujesz to do V - to nic nie robi.

Scott Zhu
źródło
4
Dziękuję Ci za to! Spędziłem wiele godzin nad kwestią wynikającą z tego. Naprawdę doceniam, że poświęciłeś czas na opublikowanie tego. :)
ClayJ
@ScottZhu: Dodałem ważną gotchya do twojej odpowiedzi. Oczywiście możesz swobodnie usunąć lub zmienić mój dodatek! Dzięki jeszcze raz!
Fattie
9

Szybki:

Gdzie targetView jest widokiem, w którym chcesz odebrać zdarzenie (które może być całkowicie lub częściowo umieszczone poza ramką jego widoku nadrzędnego).

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
źródło
5

Dla mnie (podobnie jak dla innych) odpowiedzią nie było przesłonięcie hitTestmetody, ale raczej pointInsidemetodę. Musiałem to zrobić tylko w dwóch miejscach.

Nadzór To pogląd, że gdyby był clipsToBoundsustawiony na prawdę, zniknąłby cały ten problem. Oto moje proste UIVieww Swift 3:

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

Podgląd podrzędny Twój przebieg może się różnić, ale w moim przypadku musiałem również nadpisać pointInsidemetodę dla podglądu, który zdecydował, że dotyk jest poza zakresem. Mój jest w Objective-C, więc:

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

Ta odpowiedź (i kilka innych na to samo pytanie) może pomóc w zrozumieniu.

Dan Rosenstark
źródło
2

aktualizacja swift 4.1

Aby uzyskać zdarzenia dotykowe w tym samym UIView, wystarczy dodać następującą metodę nadpisania, która zidentyfikuje określony widok dotknięty w UIView, który jest umieszczony poza granicami

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
źródło