Capturando toques em uma subvisualização fora do quadro de sua visão geral usando hitTest: withEvent:

94

Meu problema: eu tenho uma supervisualização EditViewque ocupa basicamente todo o quadro do aplicativo e uma subvisualização MenuViewque ocupa apenas ~ 20% da parte inferior, e então MenuViewcontém sua própria subvisualização ButtonViewque na verdade reside fora dos MenuViewlimites de (algo como isto ButtonView.frame.origin.y = -100:).

(observação: EditViewpossui outras subvisualizações que não fazem parte da MenuViewhierarquia de visões de, mas podem afetar a resposta.)

Você provavelmente já conhece o problema: quando ButtonViewestá dentro dos limites de MenuView(ou, mais especificamente, quando meus toques estão dentro MenuViewdos limites de), ButtonViewresponde a eventos de toque. Quando meus toques estão fora dos MenuViewlimites de (mas ainda dentro ButtonViewdos limites de), nenhum evento de toque é recebido por ButtonView.

Exemplo:

  • (E) é EditView, o pai de todas as visualizações
  • (M) é MenuViewuma subvisão de EditView
  • (B) é ButtonViewuma subvisão do MenuView

Diagrama:

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

Como (B) está fora do quadro de (M), um toque na região (B) nunca será enviado para (M) - na verdade, (M) nunca analisa o toque neste caso, e o toque é enviado para o próximo objeto na hierarquia.

Objetivo: presumo que substituir hitTest:withEvent:pode resolver esse problema, mas não entendo exatamente como. No meu caso, deve hitTest:withEvent:ser sobrescrito em EditView(minha supervisão 'master')? Ou deveria ser sobrescrito na MenuViewvisualização direta do botão que não está recebendo toques? Ou estou pensando sobre isso incorretamente?

Se isso exigir uma explicação longa, um bom recurso online seria útil - exceto os documentos UIView da Apple, que não deixaram isso claro para mim.

Obrigado!

toblerpwn
fonte

Respostas:

145

Modifiquei o código da resposta aceita para ser mais genérico - ele lida com os casos em que a visualização corta as subvisualizações até seus limites, pode estar oculta e, mais importante: se as subvisualizações forem hierarquias de visão complexas, a subvisualização correta será retornada.

- (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 isso ajude qualquer pessoa que tente usar essa solução para casos de uso mais complexos.

Noam
fonte
Parece bom e obrigado por fazer isso. Porém, tinha uma dúvida: por que você está fazendo return [super hitTest: point withEvent: event]; ? Você não retornaria nulo se não houvesse subvisualização que acionasse o toque? A Apple diz que hitTest retorna nulo se nenhuma subvisualização contém o toque.
Ser Pounce
Hm ... Sim, isso soa quase certo. Além disso, para um comportamento correto, os objetos precisam ser iterados na ordem inversa (porque o último é o mais superior visualmente). Código editado para corresponder.
Noam
3
Acabei de usar sua solução para fazer um UIButton capturar o toque, e está dentro de um UIView que está dentro de um UICollectionViewCell dentro de (obviamente) um UICollectionView. Tive que criar uma subclasse de UICollectionView e UICollectionViewCell para substituir hitTest: withEvent: nestas três classes. E funciona como um encanto !! Obrigado !!
Daniel García
3
Dependendo do uso desejado, ele deve retornar [super hitTest: point withEvent: event] ou nil. Retornar a si mesmo faria com que recebesse tudo.
Noam,
1
Aqui está um documento técnico de perguntas e respostas da Apple sobre essa mesma técnica: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang,
33

Ok, eu fiz algumas escavações e testes, veja como hitTest:withEventfunciona - pelo menos em um nível alto. Imagine este cenário:

  • (E) é EditView , o pai de todas as visualizações
  • (M) é MenuView , uma subvisão de EditView
  • (B) é ButtonView , uma subvisão de MenuView

Diagrama:

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

Como (B) está fora do quadro de (M), um toque na região (B) nunca será enviado para (M) - na verdade, (M) nunca analisa o toque neste caso, e o toque é enviado para o próximo objeto na hierarquia.

No entanto, se você implementar hitTest:withEvent:em (M), os toques em qualquer lugar no aplicativo serão enviados para (M) (ou pelo menos ele sabe sobre eles). Você pode escrever um código para lidar com o toque nesse caso e retornar o objeto que deve receber o toque.

Mais especificamente: o objetivo do hitTest:withEvent:é devolver o objeto que deve receber o golpe. Então, em (M) você pode escrever 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];
}

Eu ainda sou muito novo neste método e neste problema, então se houver maneiras mais eficientes ou corretas de escrever o código, por favor, comente.

Espero que isso ajude quem responder a essa pergunta mais tarde. :)

toblerpwn
fonte
Obrigado, isso funcionou para mim, embora eu tivesse que fazer um pouco mais de lógica para determinar qual das subvisualizações deveria realmente receber ocorrências. Removi uma pausa desnecessária do seu exemplo aliás.
Daniel Saidi
Quando o quadro da subvisualização estava além do quadro principal, o evento de clique não estava respondendo! Sua solução corrigiu esse problema. Muito obrigado :-)
porJeevan
@toblerpwn (apelido engraçado :)) você deve editar esta excelente resposta antiga para deixar bem claro (USE MAIÚSCULAS GRANDES) em qual classe você deve adicionar isso. Felicidades!
Fattie de
26

Em Swift 5

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
}
Duan
fonte
Isso só funcionará se você aplicar a substituição na visão geral direta da visão de "comportamento inadequado"
Hudi Ilfeld
2

O que eu faria é ter o ButtonView e o MenuView existindo no mesmo nível na hierarquia de exibição, colocando-os em um contêiner cujo quadro se ajusta completamente a ambos. Desta forma, a região interativa do item recortado não será ignorada por causa dos limites de sua supervisão.

mamackenzie
fonte
pensei sobre esta solução alternativa também - significa que terei que duplicar alguma lógica de posicionamento (ou refatorar algum código sério!), mas pode de fato ser minha melhor escolha no final ..
toblerpwn
1

Se você tiver muitas outras subvisualizações dentro de sua visão pai, provavelmente a maioria das outras visões interativas não funcionaria se você usar as soluções acima, nesse caso, você pode usar algo como isto (no 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)
    }
}
Paras gupta
fonte
0

Se alguém precisar, aqui está a 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
}
Sonny
fonte
0

Coloque as linhas de código abaixo em sua hierarquia de visualização:

- (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 maiores esclarecimentos, foi explicado em meu blog: "goaheadwithiphonetech" sobre "Texto explicativo personalizado: o botão não é clicável."

Espero que isso te ajude...!!!

Sandip Patel - SM
fonte
Seu blog foi removido, então onde podemos encontrar a explicação?
ishahak