Manipulação de eventos para iOS - como hitTest: withEvent: e pointInside: withEvent: estão relacionados?

145

Embora a maioria dos documentos da apple seja muito bem escrita, acho que o ' Event Handling Guide for iOS ' é uma exceção. É difícil para mim entender claramente o que foi descrito lá.

O documento diz:

No teste de acerto, uma janela chama hitTest:withEvent:a visualização mais superior da hierarquia de visualizações; esse método prossegue chamando recursivamente pointInside:withEvent:cada exibição na hierarquia de exibição que retorna YES, prosseguindo pela hierarquia até encontrar a subvisão dentro de cujos limites o toque ocorreu. Essa visualização se torna a visualização de teste de acerto.

Então é assim que apenas hitTest:withEvent:a visão superior é chamada pelo sistema, que chama pointInside:withEvent:todas as subvisões, e se o retorno de uma subvisualização específica for SIM, as chamadas pointInside:withEvent:das subclasses dessa subvisualização?

realstuff02
fonte
3
Um tutorial muito bom que me ajudou a link
anneblue
O documento mais recente equivalente para isso poderia ser agora developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Respostas:

173

Parece uma pergunta bastante básica. Mas eu concordo com você que o documento não é tão claro quanto outros documentos, então aqui está a minha resposta.

A implementação do hitTest:withEvent:UIResponder faz o seguinte:

  • Chama pointInside:withEvent:deself
  • Se o retorno for NÃO, hitTest:withEvent:retornará nil. o fim da história.
  • Se o retorno for SIM, ele envia hitTest:withEvent:mensagens para suas subvisões. inicia na subvisão de nível superior e continua em outras visualizações até que uma subvisão retorne um não- nilobjeto ou todas as subvisões recebam a mensagem.
  • Se uma subvisão retornar um não- nilobjeto na primeira vez, a primeira hitTest:withEvent:retornará esse objeto. o fim da história.
  • Se nenhuma subvisão retornar um não- nilobjeto, a primeira hitTest:withEvent:retornaráself

Esse processo se repete recursivamente; portanto, normalmente a exibição em folha da hierarquia da exibição é retornada eventualmente.

No entanto, você pode substituir hitTest:withEventpara fazer algo diferente. Em muitos casos, a substituição pointInside:withEvent:é mais simples e ainda oferece opções suficientes para ajustar a manipulação de eventos em seu aplicativo.

MHC
fonte
Você quer dizer que hitTest:withEvent:todas as subvisões são executadas eventualmente?
realstuff02
2
Sim. Apenas substitua hitTest:withEvent:as visualizações (e pointInsidese desejar), imprima um log e ligue [super hitTest...para descobrir quem hitTest:withEvent:é chamado em que ordem.
MHC
não deve passo 3, onde se menciona "Se o retorno for SIM, ele envia hitTest: withEvent: ... não deveria ser pointInside: withEvent eu pensei que envia pointInside a todos os subviews?
ProStock
Em fevereiro, ele enviou hitTest: withEvent :, no qual um pointInside: withEvent: foi enviado a si mesmo. Não verifiquei novamente esse comportamento com as seguintes versões do SDK, mas acho que o envio de hitTest: withEvent: faz mais sentido, pois fornece um controle de nível superior se um evento pertence a uma exibição ou não; pointInside: withEvent: informa se o local do evento está na exibição ou não, não se o evento pertence à exibição. Por exemplo, uma subvisão pode não querer manipular um evento, mesmo que seu local esteja na subvisão.
MHC
1
WWDC2014 Sessão 235 - Técnicas avançadas de visualizações de rolagem e manipulação de toque fornecem uma ótima explicação e exemplo para este problema.
Antonio081014
297

Eu acho que você está confundindo subclasses com a hierarquia de visualizações. O que o documento diz é o seguinte. Digamos que você tenha essa hierarquia de visualizações. Por hierarquia, não estou falando sobre hierarquia de classes, mas visualizações na hierarquia de visualizações, da seguinte maneira:

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

Diga que você coloca o dedo dentro D. Aqui está o que vai acontecer:

  1. hitTest:withEvent: é chamado A , a visualização superior da hierarquia de visualizações.
  2. pointInside:withEvent: é chamado recursivamente em cada visualização.
    1. pointInside:withEvent:é chamado Ae retornaYES
    2. pointInside:withEvent:é chamado Be retornaNO
    3. pointInside:withEvent:é chamado Ce retornaYES
    4. pointInside:withEvent:é chamado De retornaYES
  3. Nas exibições que retornaram YES, ele olhará para baixo na hierarquia para ver a subvisão onde o toque ocorreu. Neste caso, a partir de A, Ce D, seráD .
  4. D será a visualização de teste de impacto
pgb
fonte
Obrigado pela resposta. O que você descreveu também é o que estava em minha mente, mas o @MHC diz hitTest:withEvent:sobre B, C e D também são invocados. O que acontece se D é uma subvisão de C, não A? Acho que fiquei confuso ...
realstuff02
2
No meu desenho, D é um subexibição de C.
PGB
1
Não Avoltaria YEStão bem quanto Ce volta D?
Martin Wickman
2
Não se esqueça de que as visualizações invisíveis (por .hidden ou opacidade abaixo de 0,1) ou com a interação do usuário desativada nunca responderão ao hitTest. Eu não acho que hitTest esteja sendo chamado nesses objetos em primeiro lugar.
51313 Jonny
Só queria adicionar que hitTest: withEvent: pode ser chamado em todas as visualizações, dependendo de sua hierarquia.
Adithya
47

Acho esse teste de sucesso no iOS muito útil

insira a descrição da imagem aqui

- (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
fonte
Então você precisa adicionar isso a uma subclasse do UIView e ter todas as visualizações em sua hierarquia herdadas?
Guig
21

Obrigado pelas respostas, eles me ajudaram a resolver a situação com visualizações de "sobreposição".

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

Suponha X- toque do usuário. pointInside:withEvent:em Bretornos NO, então hitTest:withEvent:retorna A. Escrevi a categoria UIViewpara lidar com o problema quando você precisa receber um toque na vista superior mais visível .

- (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. Não devemos enviar eventos de toque para visualizações ocultas ou transparentes, ou visualizações userInteractionEnableddefinidas comoNO ;
  2. Se o toque estiver dentro self,self será considerado como resultado potencial.
  3. Verifique recursivamente todas as subvisões quanto a ocorrência. Se houver, devolva-o.
  4. Caso contrário, retorne a si próprio ou nulo, dependendo do resultado da etapa 2.

Observe que é [self.subviewsreverseObjectEnumerator]necessário seguir a hierarquia da vista de cima para baixo. E verifique para clipsToBoundsnão testar subvisões mascaradas.

Uso:

  1. Categoria de importação em sua exibição em subclasse.
  2. Substitua hitTest:withEvent:por este
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

O Guia oficial da Apple também fornece algumas boas ilustrações.

Espero que isso ajude alguém.

Leão
fonte
Surpreendente! Obrigado pela lógica clara e pelo ótimo trecho de código, resolvi meu arranhão!
Thompson
@ Leão, boa resposta. Além disso, você pode verificar a igualdade para limpar a cor na primeira etapa.
Aquarium_moose
3

Mostra como este trecho!

- (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
fonte
Acho que essa é a resposta mais fácil de entender e corresponde muito bem às minhas observações sobre o comportamento real. A única diferença é que as subvisões são enumeradas em ordem inversa; portanto, as subvisões mais próximas à frente recebem toques em preferência aos irmãos por trás deles.
Douglas Hill
@DouglasHill graças à sua correção. Atenciosamente
hipopótamo
1

O trecho de @lion funciona como um encanto. Eu o carreguei para o swift 2.1 e o usei como uma extensão para o UIView. Estou postando aqui, caso alguém precise.

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 usá-lo, basta substituir hitTest: point: withEvent na sua visualização da seguinte maneira:

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
fonte
0

Diagrama de classe

Teste de Acerto

Encontre um First Responder

First ResponderNesse caso, o UIView point()método mais profundo retornou verdadeiro

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

hitTest()Parece internamente

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 de toque para o 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?)

Vamos dar uma olhada no exemplo

Cadeia de resposta

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

Veja o exemplo

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
fonte