Interacción más allá de los límites de UIView

92

¿Es posible que un UIButton (o cualquier otro control para el caso) reciba eventos táctiles cuando el marco del UIButton se encuentra fuera del marco de su padre? Porque cuando intento esto, parece que mi UIButton no puede recibir ningún evento. ¿Cómo soluciono esto?

ThomasM
fuente
1
Esto funciona perfectamente para mi. developer.apple.com/library/ios/qa/qa2013/qa1812.html Mejor
Frank Li
2
Esto también es bueno y relevante (o quizás un engaño): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Respuestas:

93

Si. Puede anular el hitTest:withEvent:método para devolver una vista para un conjunto de puntos más grande que el que contiene esa vista. Consulte la referencia de clase de UIView .

Editar: Ejemplo:

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

Edición 2: (Después de la aclaración :) Para asegurarse de que el botón se trate como si estuviera dentro de los límites del padre, debe anular pointInside:withEvent:en el padre para incluir el marco del botón.

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

Tenga en cuenta que el código que está ahí para anular pointInside no es del todo correcto. Como Summon explica a continuación, haz esto:

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

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

Tenga en cuenta que es muy probable que lo haga self.oversizeButtoncomo IBOutlet en esta subclase de UIView; luego puede arrastrar el "botón de gran tamaño" en cuestión, a la vista especial en cuestión. (O, si por alguna razón estuvieras haciendo esto mucho en un proyecto, tendrías una subclase especial de UIButton, y podrías buscar esas clases en tu lista de subvista). Espero que te ayude.

jnic
fuente
¿Me pueden ayudar más a implementar esto correctamente con algún código de muestra? También leí la referencia y, debido a la siguiente línea, estoy confundido: "Los puntos que se encuentran fuera de los límites del receptor nunca se informan como aciertos, incluso si realmente se encuentran dentro de una de las subvistas del receptor. Las subvistas pueden extenderse visualmente más allá de los límites de su padre si la propiedad clipsToBounds de la vista padre está establecida en NO. Sin embargo, las pruebas de posicionamiento siempre ignoran los puntos fuera de los límites de la vista padre ".
ThomasM
Es especialmente esta parte la que hace que parezca que esta no es la solución que necesito: "Sin embargo, las pruebas de posicionamiento siempre ignoran los puntos fuera de los límites de la vista principal". Pero podría estar equivocado, por supuesto, encuentro que las referencias de Apple a veces son realmente confusas. ..
ThomasM
Ah, ahora lo entiendo. Debe anular el de los padres pointInside:withEvent:para incluir el marco del botón. Agregaré un código de muestra a mi respuesta anterior.
jnic
¿Hay alguna forma de hacer esto sin anular los métodos de una UIView? Por ejemplo, si sus vistas son completamente genéricas para UIKit y se inicializan a través de un Nib, ¿puede anular estos métodos desde su UIViewController?
RLH
1
hitTest:withEvent:y el pointInside:withEvent:área no se me llama cuando presiono un botón que se encuentra fuera de los límites de los padres. ¿Qué puede estar mal?
iosdude
24

@jnic, estoy trabajando en iOS SDK 5.0 y para que su código funcione correctamente tuve que hacer esto:

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

La vista de contenedor en mi caso es un UIButton y todos los elementos secundarios también son UIButtons que pueden moverse fuera de los límites del UIButton principal.

Mejor

Convocar
fuente
20

En la vista principal, puede anular el método de prueba de posicionamiento:

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

}

En este caso, si el punto cae dentro de los límites de su botón, reenvía la llamada allí; de lo contrario, vuelva a la implementación original.

Jack
fuente
17

En mi caso, tenía una UICollectionViewCellsubclase que contenía un UIButton. Inhabilité clipsToBoundsen la celda y el botón estaba visible fuera de los límites de la celda. Sin embargo, el botón no estaba recibiendo eventos táctiles. Pude detectar los eventos táctiles en el botón usando la respuesta de Jack: https://stackoverflow.com/a/30431157/3344977

Aquí hay una versión 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)
}

Rápido 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)
}
usuario3344977
fuente
Ese era exactamente mi escenario. El otro punto es que debe poner este método dentro de la clase CollectionViewCell.
Hamid Reza Ansari
Pero, ¿por qué inyectarías el botón en un método anulado?
rommex
10

¿Por qué está pasando esto?

Esto se debe a que cuando su subvista se encuentra fuera de los límites de su supervista, los eventos táctiles que realmente suceden en esa subvista no se entregarán a esa subvista. Sin embargo, SERÁ ser entregado a su supervista.

Independientemente de si las subvistas se recortan visualmente o no, los eventos táctiles siempre respetan el rectángulo de límites de la supervista de la vista de destino. En otras palabras, los eventos táctiles que ocurren en una parte de una vista que se encuentra fuera del rectángulo de límites de su supervista no se envían a esa vista. Enlace

¿Qué necesitas hacer?

Cuando su supervista reciba el evento táctil mencionado anteriormente, deberá decirle a UIKit explícitamente que mi subvista debería ser la que reciba este evento táctil.

¿Y el código?

En su supervista, implemente 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)
    }

Gotchya fascinante: debes ir a la "supervista más alta demasiado pequeña"

Tienes que ir "arriba" a la vista "más alta" a la que se encuentra la vista del problema.

Ejemplo típico:

Digamos que tiene una pantalla S, con una vista de contenedor C. La vista del controlador de vista de contenedor es V. (Recuerde que V se ubicará dentro de C y tendrá el tamaño idéntico). V tiene una subvista (tal vez un botón) B. B es el problema vista que en realidad está fuera de V.

Pero tenga en cuenta que B también está fuera de C.

En este ejemplo, usted tiene que aplicar la solución override hitTest, de hecho, a C, no a V . Si lo aplica a V, no hace nada.

Scott Zhu
fuente
4
¡Gracias por esto! He pasado horas y horas en un problema derivado de esto. Realmente aprecio que se haya tomado el tiempo para publicar esto. :)
ClayJ
@ScottZhu: Agregué un gotchya importante a tu respuesta. ¡Por supuesto, no dude en eliminar o cambiar mi adición! ¡Gracias de nuevo!
Fattie
9

Rápido:

Donde targetView es la vista en la que desea recibir el evento (que puede estar total o parcialmente ubicada fuera del marco de su vista principal).

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)
}
Lucas Bartolomeo
fuente
5

Para mí (como para otros), la respuesta no fue anular el hitTestmétodo, sino el pointInsidemétodo. Tuve que hacer esto solo en dos lugares.

La Supervista Esta es la opinión de que si se hubiera clipsToBoundsestablecido como verdadera, haría desaparecer todo este problema. Así que aquí está mi simple UIViewen Swift 3:

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

La subvista Su kilometraje puede variar, pero en mi caso también tuve que sobrescribir el pointInsidemétodo para la subvista que se había decidido que el toque estaba fuera de los límites. El mío está en Objective-C, entonces:

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

Esta respuesta (y algunas otras sobre la misma pregunta) pueden ayudar a aclarar su comprensión.

Dan Rosenstark
fuente
2

swift 4.1 actualizado

Para obtener eventos táctiles en la misma UIView, solo necesita agregar el siguiente método de anulación, identificará la vista específica tocada en UIView que se coloca fuera del límite

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
fuente