Manejo de eventos para iOS: ¿cómo se relacionan hitTest: withEvent: y pointInside: withEvent:?

145

Si bien la mayoría de los documentos de Apple están muy bien escritos, creo que la ' Guía de manejo de eventos para iOS ' es una excepción. Es difícil para mí entender claramente lo que se ha descrito allí.

El documento dice:

En las pruebas de impacto, una ventana llama hitTest:withEvent:a la vista superior de la jerarquía de vistas; Este método se realiza recurriendo recursivamente pointInside:withEvent:a cada vista en la jerarquía de vistas que devuelve SÍ, avanzando hacia abajo hasta encontrar la subvista dentro de cuyos límites se produjo el toque. Esa vista se convierte en la vista de prueba de impacto.

Entonces, ¿es así que solo hitTest:withEvent:el sistema llama a la vista superior, que llama pointInside:withEvent:a todas las subvistas, y si el retorno de una subvista específica es SÍ, entonces llama a pointInside:withEvent:las subclases de esa subvista?

realstuff02
fuente
3
Un muy buen tutorial que me ayudó a salir de enlace
AnneBlue
El documento más nuevo equivalente para esto ahora podría ser developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Respuestas:

173

Parece una pregunta bastante básica. Pero estoy de acuerdo con usted, el documento no es tan claro como otros documentos, así que aquí está mi respuesta.

La implementación de hitTest:withEvent:en UIResponder hace lo siguiente:

  • Llama pointInside:withEvent:deself
  • Si el retorno es NO, hitTest:withEvent:regresa nil. El fin de la historia.
  • Si la hitTest:withEvent:respuesta es SÍ, envía mensajes a sus subvistas. comienza desde la subvista de nivel superior y continúa a otras vistas hasta que una subvista devuelve un no nilobjeto, o todas las subvistas reciben el mensaje.
  • Si una subvista devuelve un no nilobjeto la primera vez, la primera hitTest:withEvent:devuelve ese objeto. El fin de la historia.
  • Si ninguna subvista devuelve un no nilobjeto, la primera hitTest:withEvent:devuelveself

Este proceso se repite de forma recursiva, por lo que normalmente se devuelve la vista de hoja de la jerarquía de vistas.

Sin embargo, puede anular hitTest:withEventhacer algo diferente. En muchos casos, la anulación pointInside:withEvent:es más simple y aún ofrece suficientes opciones para modificar el manejo de eventos en su aplicación.

MHC
fuente
¿Quieres decir que hitTest:withEvent:todas las subvistas se ejecutan eventualmente?
realstuff02
2
Si. Simplemente anule hitTest:withEvent:sus vistas (y pointInsidesi lo desea), imprima un registro y llame [super hitTest...para averiguar quién hitTest:withEvent:se llama en qué orden.
MHC
no debería el paso 3 donde menciona "Si el retorno es SÍ, envía hitTest: withEvent: ... ¿no debería ser pointInside: withEvent? Pensé que enviaba pointInside a todas las subvistas?
prostock
En febrero, envió por primera vez hitTest: withEvent :, en el que se envió un puntoInside: withEvent: a sí mismo. No he vuelto a comprobar este comportamiento con las siguientes versiones de SDK, pero creo que enviar hitTest: withEvent: tiene más sentido porque proporciona un control de nivel superior de si un evento pertenece a una vista o no; pointInside: withEvent: indica si la ubicación del evento está en la vista o no, no si el evento pertenece a la vista. Por ejemplo, una subvista puede no querer manejar un evento incluso si su ubicación está en la subvista.
MHC
1
La Sesión 235 de WWDC2014: Vistas de desplazamiento avanzadas y técnicas de manejo táctil ofrece una gran explicación y ejemplo para este problema.
antonio081014
297

Creo que estás confundiendo las subclases con la jerarquía de vistas. Lo que dice el documento es lo siguiente. Digamos que tiene esta jerarquía de vistas. Por jerarquía no estoy hablando de jerarquía de clases, sino de vistas dentro de la jerarquía de vistas, de la siguiente manera:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

Digamos que pones tu dedo adentro D. Esto es lo que sucederá:

  1. hitTest:withEvent:se llama A, la vista superior de la jerarquía de vistas.
  2. pointInside:withEvent: se llama recursivamente en cada vista.
    1. pointInside:withEvent:se llama Ay devuelveYES
    2. pointInside:withEvent:se llama By devuelveNO
    3. pointInside:withEvent:se llama Cy devuelveYES
    4. pointInside:withEvent:se llama Dy devuelveYES
  3. En las vistas que regresaron YES, mirará hacia abajo en la jerarquía para ver la subvista donde tuvo lugar el contacto. En este caso, desde A, Cy D, será D.
  4. D será la vista de prueba de éxito
pgb
fuente
Gracias por la respuesta. Lo que describiste es también lo que estaba en mi mente, pero @MHC dice que hitTest:withEvent:B, C y D también se invocan. ¿Qué sucede si D es una subvista de C, no A? Creo que se confundió ...
realstuff02
2
En mi dibujo, D es una subvista de C.
pgb
1
No Avolver YES, así, del mismo modo C, y Dlo hace?
Martin Wickman
2
No olvide que las vistas que son invisibles (ya sea por .hidden u opacidad por debajo de 0.1), o que tienen la interacción del usuario desactivada, nunca responderán a hitTest. No creo que hitTest se llame a estos objetos en primer lugar.
Jonny
Solo quería agregar ese hitTest: withEvent: se puede invocar en todas las vistas según su jerarquía.
Adithya
47

Encuentro que este Hit-Testing en iOS es muy útil

ingrese la descripción de la imagen aquí

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

Editar Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}
onmyway133
fuente
¿Entonces necesita agregar esto a una subclase de UIView y heredar de él todas las vistas en su jerarquía?
Guig
21

Gracias por las respuestas, me ayudaron a resolver situaciones con vistas de "superposición".

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

Asumir X: toque del usuario. pointInside:withEvent:en Bdevoluciones NO, por lo que hitTest:withEvent:vuelve A. Escribí la categoría UIViewpara manejar el problema cuando necesita recibir un toque en la vista superior más visible .

- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
        return nil;

    // 2
    UIView *hitView = self;
    if (![self pointInside:point withEvent:event]) {
        if (self.clipsToBounds) return nil;
        else hitView = nil;
    }

    // 3
    for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
        CGPoint insideSubview = [self convertPoint:point toView:subview];
        UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
        if (sview) return sview;
    }

    // 4
    return hitView;
}
  1. No debemos enviar eventos táctiles para vistas ocultas o transparentes, o vistas con userInteractionEnabledset to NO;
  2. Si el tacto está adentro self, selfse considerará como resultado potencial.
  3. Verifique recursivamente todas las subvistas para el éxito. Si alguno, devuélvalo.
  4. De lo contrario, regrese a sí mismo o nulo según el resultado del paso 2.

Tenga en cuenta que era [self.subviewsreverseObjectEnumerator]necesario seguir la jerarquía de vistas de arriba hacia abajo. Y compruebe para clipsToBoundsasegurarse de no probar subvistas enmascaradas.

Uso:

  1. Importar categoría en su vista subclase.
  2. Reemplazar hitTest:withEvent:con esto
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

La Guía oficial de Apple también ofrece algunas buenas ilustraciones.

Espero que esto ayude a alguien.

León
fuente
¡Asombroso! ¡Gracias por la lógica clara y el GRAN fragmento de código, resolvió mi rascador de cabeza!
Thompson
@ León, buena respuesta. También puede verificar la igualdad para borrar el color en el primer paso.
aquarium_moose
3

¡Se muestra como este fragmento!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
    {
        return nil;
    }

    if (![self pointInside:point withEvent:event])
    {
        return nil;
    }

    __block UIView *hitView = self;

    [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {   

        CGPoint thePoint = [self convertPoint:point toView:obj];

        UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];

        if (theSubHitView != nil)
        {
            hitView = theSubHitView;

            *stop = YES;
        }

    }];

    return hitView;
}
hipopótamo
fuente
Encuentro que esta es la respuesta más fácil de entender, y coincide con mis observaciones del comportamiento real muy de cerca. La única diferencia es que las subvistas se enumeran en orden inverso, por lo que las subvistas más cercanas al frente reciben toques con preferencia a los hermanos detrás de ellos.
Douglas Hill
@ DouglasHill gracias a su corrección. Saludos cordiales
hipopótamo
1

El fragmento de @lion funciona a las mil maravillas. Lo porté a Swift 2.1 y lo usé como una extensión de UIView. Lo estoy publicando aquí en caso de que alguien lo necesite.

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

Para usarlo, simplemente anule hitTest: point: withEvent en su uiview de la siguiente manera:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}
mortadelo
fuente
0

Diagrama de clase

Prueba de golpe

Encontrar un First Responder

First Responderen este caso es el UIView point()método más profundo de los cuales devuelve verdadero

func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
func point(inside point: CGPoint, with event: UIEvent?) -> Bool

Internamente se hitTest()ve como

hitTest() {

    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }

    return nil

}

Enviar evento táctil al First Responder

//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

Veamos un ejemplo.

Cadena de respuesta

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

Mira el ejemplo

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

[Android onTouch]

yoAlex5
fuente