Pregunta Captura de toques en una subvista fuera del marco de su supervista usando hitTest: withEvent:


Mi problema: Tengo una supervista EditView eso ocupa básicamente todo el marco de la aplicación y una subvista MenuView que ocupa solo el fondo ~ 20%, y luego MenuView contiene su propia subvista ButtonView que en realidad reside fuera de MenuView's de los límites (algo como esto: ButtonView.frame.origin.y = -100)

(Nota: EditView tiene otras subvistas que no son parte de MenuViewla jerarquía de vista, pero puede afectar la respuesta).

Probablemente ya conozcas el problema: cuando ButtonView está dentro de los límites de MenuView (o, más específicamente, cuando mis toques están dentro MenuViewlímites de) ButtonView responde a eventos táctiles. Cuando mis toques están fuera de MenuView's límites (pero aún dentro ButtonView's bounds), ningún evento táctil es recibido por ButtonView.

Ejemplo:

  • (E) es EditView, el padre de todas las vistas
  • (M) es MenuView, una subvista de EditView
  • (B) es ButtonView, una subvista de MenuView

Diagrama: 

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

Como (B) está fuera del marco (M), un toque en la región (B) nunca se enviará a (M); de hecho, (M) nunca analiza el toque en este caso, y el toque se envía a el siguiente objeto en la jerarquía.

Gol: Me doy cuenta de que prevalece hitTest:withEvent: puede resolver este problema, pero no entiendo exactamente cómo. En mi caso, debería hitTest:withEvent: ser anulado en EditView (mi supervista 'maestra')? O debería ser anulado en MenuView, la supervista directa del botón que no está recibiendo toques? ¿O estoy pensando en esto incorrectamente?

Si esto requiere una explicación larga, un buen recurso en línea sería útil, excepto los documentos de UIView de Apple, que no me han dejado claro.

¡Gracias!


76
2017-08-02 03:41


origen


Respuestas:


He modificado el código de la respuesta aceptada para que sea más genérico: maneja los casos en que la vista recorta las subvistas a sus límites, puede estar oculta y, lo que es más importante, si las subvistas son jerarquías de vistas complejas, se devolverá la subvista correcta.

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

Espero que esto ayude a cualquiera que trate de usar esta solución para casos de uso más complejos.


123
2018-02-14 13:13



Ok, hice algunas excavaciones y pruebas, así es cómo hitTest:withEvent funciona, al menos en un nivel alto. Imagen de este escenario:

  • (E) es EditView, el padre de todas las vistas
  • (M) es MenuView, una subvista de EditView
  • (B) es ButtonView, una subvista de MenuView

Diagrama:

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

Como (B) está fuera del marco (M), un toque en la región (B) nunca se enviará a (M); de hecho, (M) nunca analiza el toque en este caso, y el toque se envía a el siguiente objeto en la jerarquía.

Sin embargo, si implementa hitTest:withEvent: en (M), los grifos en cualquier parte de la aplicación se enviarán a (M) (o al menos se sabe sobre ellos). Puede escribir código para manejar el toque en ese caso y devolver el objeto que debería recibir el toque.

Más específicamente: el objetivo de hitTest:withEvent: es devolver el objeto que debería recibir el golpe. Entonces, en (M) podrías escribir código como este:

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

Todavía soy muy nuevo en este método y este problema, por lo que si hay formas más eficientes o correctas de escribir el código, por favor comente.

Espero que eso ayude a cualquiera que responda esta pregunta más tarde. :)


26
2017-08-03 17:39



En 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



Lo que haría es tener tanto ButtonView como MenuView en el mismo nivel en la jerarquía de vistas, colocándolos a ambos en un contenedor cuyo marco se ajuste completamente a ambos. De esta manera, la región interactiva del elemento recortado no se ignorará debido a sus límites.


2
2017-08-02 04:07



Si alguien lo necesita, aquí está la alternativa rápida

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



Si tiene muchas otras subvistas dentro de su vista principal, probablemente la mayoría de las otras vistas interactivas no funcionarían si utiliza las soluciones anteriores, en ese caso puede usar algo como esto (en 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



Coloque debajo de las líneas de código en su jerarquía de vista:

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

Para una mayor aclaración, se explicó en mi blog: "goaheadwithiphonetech" con respecto a "Llamada personalizada: el botón no se puede hacer clic en el problema".

Espero que eso te ayude...!!!


-1
2018-05-19 10:50