Interazione oltre i limiti di UIView


92

È possibile che un UIButton (o qualsiasi altro controllo per quella materia) riceva eventi di tocco quando il frame dell'UIButton si trova al di fuori del frame del genitore? Perché quando provo questo, il mio UIButton sembra non essere in grado di ricevere alcun evento. Come posso aggirare questo problema?


1
Questo funziona perfettamente per me. developer.apple.com/library/ios/qa/qa2013/qa1812.html Best
Frank Li

2
Questo è anche buono e rilevante (o forse una vittima): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Risposte:


93

Sì. È possibile sovrascrivere il hitTest:withEvent:metodo per restituire una vista per un insieme di punti più ampio di quello contenuto nella vista. Vedere il riferimento alla classe UIView .

Modifica: Esempio:

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

Modifica 2: (dopo il chiarimento :) Per garantire che il pulsante venga trattato come all'interno dei limiti del genitore, è necessario sovrascrivere pointInside:withEvent:il genitore per includere la cornice del pulsante.

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

Nota che il codice solo lì per sovrascrivere pointInside non è del tutto corretto. Come Summon spiega di seguito, fai questo:

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

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

Nota che molto probabilmente lo faresti con self.oversizeButtonIBOutlet in questa sottoclasse UIView; quindi puoi semplicemente trascinare il "pulsante oversize" in questione nella vista speciale in questione. (Oppure, se per qualche motivo lo facessi spesso in un progetto, avresti una speciale sottoclasse UIButton e potresti guardare l'elenco delle sottovisioni per quelle classi.) Spero che aiuti.


Potete aiutarmi ulteriormente a implementarlo correttamente con un codice di esempio? Inoltre ho letto il riferimento e, a causa della riga seguente, sono confuso: "I punti che si trovano al di fuori dei limiti del ricevitore non vengono mai riportati come risultati, anche se in realtà si trovano all'interno di una delle sottoview del ricevitore. Le visualizzazioni secondarie possono estendersi visivamente oltre i limiti del loro genitore se la proprietà clipsToBounds della vista principale è impostata su NO. Tuttavia, l'hit testing ignora sempre i punti al di fuori dei limiti della vista principale. "
ThomasM

È soprattutto questa parte che fa sembrare che questa non sia la soluzione di cui ho bisogno: "Tuttavia, l'hit testing ignora sempre i punti al di fuori dei limiti della vista genitore" .. Ma potrei sbagliarmi, naturalmente, trovo i riferimenti Apple a volte davvero confusi ..
ThomasM

Ah, ora capisco. Devi sovrascrivere quello del genitore pointInside:withEvent:per includere la cornice del pulsante. Aggiungerò del codice di esempio alla mia risposta sopra.
jnic

C'è un modo per farlo senza sovrascrivere i metodi di una UIView? Ad esempio, se le tue visualizzazioni sono completamente generiche per UIKit e inizializzate tramite un pennino, puoi sovrascrivere questi metodi dall'interno del tuo UIViewController?
RLH

1
hitTest:withEvent:e l' pointInside:withEvent:area non chiamata per me quando premo un pulsante che si trova al di fuori dei limiti dei genitori. Cosa può esserci di sbagliato?
iosdude

24

@jnic, sto lavorando su iOS SDK 5.0 e per far funzionare correttamente il tuo codice ho dovuto fare questo:

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

La visualizzazione del contenitore nel mio caso è un UIButton e tutti gli elementi figlio sono anche UIButtons che possono spostarsi al di fuori dei limiti del UIButton padre.

Migliore


20

Nella visualizzazione genitore puoi sostituire il metodo di hit test:

- (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 questo caso, se il punto rientra nei limiti del tuo pulsante, inoltri lì la chiamata; in caso contrario, ripristina l'implementazione originale.


17

Nel mio caso, avevo una UICollectionViewCellsottoclasse che conteneva un file UIButton. Ho disabilitato clipsToBoundssulla cella e il pulsante era visibile al di fuori dei limiti della cella. Tuttavia, il pulsante non riceveva eventi di tocco. Sono stato in grado di rilevare gli eventi di tocco sul pulsante utilizzando la risposta di Jack: https://stackoverflow.com/a/30431157/3344977

Ecco una versione 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)
}

Quello era esattamente il mio scenario. L'altro punto è che dovresti mettere questo metodo nella classe CollectionViewCell.
Hamid Reza Ansari

Ma perché iniettare il pulsante in un metodo sovrascritto ???
rommex

10

Perché sta succedendo?

Questo perché quando la tua sottoview si trova al di fuori dei limiti della tua superview, gli eventi di tocco che si verificano effettivamente in quella sottoview non verranno consegnati a quella sottoview. Tuttavia, SARA essere consegnato al suo superview.

Indipendentemente dal fatto che le viste secondarie siano o meno ritagliate visivamente, gli eventi di tocco rispettano sempre il rettangolo dei limiti della superview della vista di destinazione. In altre parole, gli eventi di tocco che si verificano in una parte di una vista che si trova al di fuori del rettangolo dei limiti della sua supervista non vengono consegnati a quella vista. Link

Cosa devi fare?

Quando la tua supervista riceve l'evento di tocco menzionato sopra, dovrai dire esplicitamente a UIKit che la mia sottoview dovrebbe essere quella che riceve questo evento di tocco.

E il codice?

Nella tua superview, implementa 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)
    }

Affascinante gotchya: devi andare al "superview più alto troppo piccolo"

Devi andare "su" alla vista "più alta" che la vista del problema è all'esterno.

Esempio tipico:

Supponiamo che tu abbia uno schermo S, con una vista contenitore C.La vista del controller della vista contenitore è V. (Ricorda che V si troverà all'interno di C e avrà le stesse dimensioni.) V ha una vista secondaria (forse un pulsante) B. B è il problema vista che è effettivamente al di fuori di V.

Ma nota che B è anche al di fuori di C.

In questo esempio, si deve applicare la soluzione override hitTestdi fatto a C, non V . Se lo applichi a V, non fa nulla.


4
Grazie per questo! Ho passato ore e ore su un problema derivante da questo. Apprezzo molto il tuo tempo per pubblicare questo. :)
ClayJ

@ScottZhu: ho aggiunto un gotchya importante alla tua risposta. Ovviamente dovresti sentirti libero di cancellare o modificare la mia aggiunta! Grazie ancora!
Fattie

9

Swift:

Dove targetView è la vista in cui desideri ricevere l'evento (che può essere interamente o parzialmente situata al di fuori del frame della sua vista principale).

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

5

Per me (come per altri), la risposta non è stata sovrascrivere il hitTestmetodo, ma piuttosto il pointInsidemetodo. Ho dovuto farlo solo in due punti.

The Superview Questa è l'opinione che se fosse clipsToBoundsimpostata su true, farebbe scomparire l'intero problema. Quindi ecco il mio semplice UIViewin Swift 3:

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

La sottoview Il tuo chilometraggio può variare, ma nel mio caso ho anche dovuto sovrascrivere il pointInsidemetodo per la sottoview che aveva deciso che il tocco era fuori limite. Il mio è in Objective-C, quindi:

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

Questa risposta (e poche altre sulla stessa domanda) può aiutarti a chiarire la tua comprensione.


2

swift 4.1 aggiornato

Per ottenere eventi di tocco nella stessa UIView devi solo aggiungere il seguente metodo di sovrascrittura, identificherà la vista specifica toccata in UIView che è posizionata fuori 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)
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.