Interação além dos limites do UIView

92

É possível para um UIButton (ou qualquer outro controle para esse assunto) receber eventos de toque quando o quadro do UIButton está fora do quadro do pai? Porque quando tento fazer isso, meu UIButton não parece ser capaz de receber nenhum evento. Como faço para contornar isso?

ThomasM
fonte
1
Isso funciona perfeitamente para mim. developer.apple.com/library/ios/qa/qa2013/qa1812.html Best
Frank Li
2
Isso também é bom e relevante (ou talvez enganoso): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Respostas:

93

Sim. Você pode substituir o hitTest:withEvent:método para retornar uma vista para um conjunto maior de pontos do que aquele que a vista contém. Consulte a Referência de classe UIView .

Editar: Exemplo:

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

Edição 2: (Após esclarecimento :) Para garantir que o botão seja tratado como estando dentro dos limites do pai, você precisa substituir pointInside:withEvent:no pai para incluir o quadro do botão.

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

Observe que o código ali para substituir pointInside não está totalmente correto. Como Summon explica abaixo, faça o seguinte:

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

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

Observe que você provavelmente faria isso com self.oversizeButtonum IBOutlet nesta subclasse de UIView; então você pode simplesmente arrastar o "botão de tamanho grande" em questão, para a vista especial em questão. (Ou, se por algum motivo você estivesse fazendo muito isso em um projeto, você teria uma subclasse UIButton especial e poderia procurar por essas classes em sua lista de subvisualização.) Espero que ajude.

jnic
fonte
Você pode me ajudar a implementar isso corretamente com algum código de exemplo? Também li a referência e por causa da linha a seguir, estou confuso: "Os pontos que ficam fora dos limites do receptor nunca são relatados como acertos, mesmo se eles realmente estiverem dentro de uma das subvisualizações do receptor. As subvisualizações podem se estender visualmente além dos limites de seu pai se a propriedade clipsToBounds da exibição pai estiver definida como NO. No entanto, o teste de clique sempre ignora pontos fora dos limites da exibição pai. "
ThomasM,
É especialmente esta parte que faz parecer que esta não é a solução de que preciso: "No entanto, o teste de clique sempre ignora pontos fora dos limites da visão pai". Mas eu posso estar errado, é claro, acho as referências da maçã às vezes muito confusas ..
ThomasM
Ah, agora entendo. Você precisa substituir o dos pais pointInside:withEvent:para incluir a moldura do botão. Vou adicionar alguns códigos de amostra à minha resposta acima.
jnic
Existe uma maneira de fazer isso sem substituir os métodos de um UIView? Por exemplo, se suas visualizações são inteiramente genéricas para UIKit e inicializadas por meio de um Nib, você pode substituir esses métodos de dentro de seu UIViewController?
RLH
1
hitTest:withEvent:e a pointInside:withEvent:área não é chamada por mim quando pressiono um botão que está fora dos limites dos pais. O que pode estar errado?
iosdude
24

@jnic, estou trabalhando no iOS SDK 5.0 e para que seu código funcione corretamente, tive que fazer o seguinte:

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

A visualização do contêiner no meu caso é um UIButton e todos os elementos filhos também são UIButtons que podem se mover para fora dos limites do UIButton pai.

Melhor

Convocar
fonte
20

Na visualização pai, você pode substituir o método de teste de clique:

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

}

Nesse caso, se o ponto estiver dentro dos limites do seu botão, você encaminha a chamada para lá; se não, reverta para a implementação original.

Ja͢ck
fonte
17

No meu caso, eu tinha uma UICollectionViewCellsubclasse que continha a UIButton. Desativei clipsToBoundsa célula e o botão ficou visível fora dos limites da célula. No entanto, o botão não estava recebendo eventos de toque. Consegui detectar os eventos de toque no botão usando a resposta de Jack: https://stackoverflow.com/a/30431157/3344977

Esta é uma versão do 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)
}

Swift 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)
}
user3344977
fonte
Esse era exatamente o meu cenário. O outro ponto é que você deve colocar esse método dentro da classe CollectionViewCell.
Hamid Reza Ansari
Mas por que você injetaria o botão em um método sobrescrito ???
rommex
10

Porque isso está acontecendo?

Isso ocorre porque quando sua subvisualização está fora dos limites de sua supervisão, os eventos de toque que realmente acontecem nessa subvisualização não serão entregues a ela. No entanto, ele vai ser entregue ao seu superview.

Independentemente de as subvisualizações serem ou não recortadas visualmente, os eventos de toque sempre respeitam o retângulo de limites da supervisualização de destino. Em outras palavras, eventos de toque que ocorrem em uma parte de uma visualização que está fora do retângulo de limites da visualização não são entregues a essa visualização. Ligação

O que você precisa fazer?

Quando seu superview receber o evento de toque mencionado acima, você precisará informar ao UIKit explicitamente que meu subview deve ser o único a receber este evento de toque.

E quanto ao código?

Em sua visão geral, 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)
    }

Fascinante gotchya: você deve ir para a "visão superior muito pequena"

Você tem que ir "para cima" até a visão "mais elevada", da qual a visão do problema está fora.

Exemplo típico:

Digamos que você tenha uma tela S, com uma visualização de contêiner C. A visualização do controlador de visualização de contêiner é V. (Lembre-se de que V ficará dentro de C e terá o mesmo tamanho.) V tem uma subvisualização (talvez um botão) B. B é o problema vista que está realmente fora de V.

Mas observe que B também está fora de C.

Neste exemplo você tem que aplicar a solução override hitTestde fato para C, para não V . Se você aplicá-lo a V - não faz nada.

Scott Zhu
fonte
4
Obrigado por isso! Passei horas e horas em um problema decorrente disso. Eu realmente aprecio seu tempo para postar isso. :)
ClayJ
@ScottZhu: Eu adicionei um gotchya importante à sua resposta. Claro, você deve se sentir à vontade para excluir ou alterar minha adição! Obrigado novamente!
Fattie 01 de
9

Rápido:

Onde targetView é a visualização que você deseja receber o evento (que pode estar total ou parcialmente localizada fora do quadro de sua visualização pai).

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)
}
Luke Bartolomeo
fonte
5

Para mim (como para outros), a resposta não foi ignorar o hitTestmétodo, mas sim o pointInsidemétodo. Eu tive que fazer isso em apenas dois lugares.

A Supervisão Esta é a visão que se fosse clipsToBoundsdefinida como verdadeira, faria todo o problema desaparecer. Então aqui está o meu simples UIViewno Swift 3:

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

A subvisualização Sua milhagem pode variar, mas no meu caso também tive que substituir o pointInsidemétodo da subvisualização que decidiu que o toque estava fora dos limites. O meu está em Objective-C, então:

- (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 resposta (e algumas outras sobre a mesma pergunta) pode ajudar a esclarecer seu entendimento.

Dan Rosenstark
fonte
2

Swift 4.1 atualizado

Para obter eventos de toque no mesmo UIView, você só precisa adicionar o seguinte método de substituição, ele identificará a visualização específica tocada no UIView que está fora do limite

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
fonte