İOS için olay işleme - hitTest: withEvent: ve pointInside: withEvent: nasıl ilişkilidir?


147

Çoğu elma belgesi çok iyi yazılmış olsa da, ' iOS için Olay İşleme Kılavuzu'nun bir istisna olduğunu düşünüyorum . Orada anlatılanları açıkça anlamak benim için zor.

Belge şöyle diyor:

İsabet testinde, bir pencere hitTest:withEvent:, görünüm hiyerarşisinin en üstteki görünümünü çağırır ; bu yöntem pointInside:withEvent:, görünüm hiyerarşisindeki her bir görünümü yinelemeli olarak çağırarak ilerler ve YES döndürür, dokunmanın sınırları içinde gerçekleşen alt görünümü bulana kadar hiyerarşide aşağı doğru ilerler. Bu görünüm, isabet testi görünümü haline gelir.

Öyleyse, tüm alt görünümleri hitTest:withEvent:çağıran sistem tarafından yalnızca en üstteki görünüm çağrılır pointInside:withEvent:ve belirli bir alt görünümden geri dönüş EVET ise, o pointInside:withEvent:alt görünümün alt sınıflarının çağrıları gibi mi?



Yanıtlar:


174

Oldukça basit bir soru gibi görünüyor. Ama seninle aynı fikirdeyim, belge diğer belgeler kadar net değil, bu yüzden cevabım burada.

hitTest:withEvent:UIResponder'da uygulaması aşağıdakileri yapar:

  • Bu çağrıları pointInside:withEvent:arasındaself
  • Dönüş HAYIR ise, hitTest:withEvent:döner nil. hikayenin sonu.
  • Dönüş EVET ise, hitTest:withEvent:alt görünümlerine mesaj gönderir . en üst düzey alt görünümden başlar ve bir alt görünüm nilnesne olmayan bir öğe döndürene veya tüm alt görünümler mesajı alana kadar diğer görünümlere devam eder .
  • Bir alt görünüm nililk kez nesne olmayan bir öğe döndürürse , ilki hitTest:withEvent:o nesneyi döndürür. hikayenin sonu.
  • Hiçbir alt görünüm nilnesne olmayan bir öğe döndürmezse , ilk hitTest:withEvent:döndürürself

Bu süreç özyinelemeli olarak tekrar eder, bu nedenle normalde görünüm hiyerarşisinin yaprak görünümü eninde sonunda döndürülür.

Ancak, bir hitTest:withEventşeyi farklı yapmak için geçersiz kılabilirsiniz . Çoğu durumda, geçersiz kılma pointInside:withEvent:daha basittir ve yine de uygulamanızda olay işlemeyi ayarlamak için yeterli seçenek sağlar.


hitTest:withEvent:Tüm alt görünümlerin sonunda yürütüldüğünü mü kastediyorsunuz ?
realstuff02

2
Evet. Sadece hitTest:withEvent:görünümlerinizi geçersiz kılın (ve pointInsideisterseniz), bir günlük yazdırın ve [super hitTest...kimin hitTest:withEvent:hangi sırayla çağrıldığını bulmak için arayın .
MHC

"Dönüş EVET ise, hitTest gönderir: withEvent: ... pointInside: withEvent olmalı mı? Tüm alt görünümlere pointInside gönderdiğini düşündüm
prostock

Şubat ayında ilk olarak kendisine bir pointInside: withEvent: gönderildiği hitTest: withEvent: öğesini gönderdi. Bu davranışı aşağıdaki SDK sürümleriyle yeniden kontrol etmedim, ancak sanırım hitTest: withEvent: daha mantıklı çünkü bir olayın bir görünüme ait olup olmadığına dair daha yüksek düzeyde bir kontrol sağlıyor; pointInside: withEvent: olayın görünüme ait olup olmadığını değil, olay konumunun görünümde olup olmadığını söyler. Örneğin, bir alt görünüm, konumu alt görünümde olsa bile bir olayı işlemek istemeyebilir.
MHC

1
WWDC2014 Oturum 235 - Gelişmiş Kaydırma Görünümleri ve Dokunarak İşleme Teknikleri, bu sorun için harika bir açıklama ve örnek sunar.
antonio081014

299

Görünüş hiyerarşisiyle alt sınıfları karıştırdığınızı düşünüyorum. Doktorun söylediği aşağıdaki gibidir. Bu görünüm hiyerarşisine sahip olduğunuzu varsayalım. Hiyerarşiye göre sınıf hiyerarşisinden değil, aşağıdaki gibi görünüm hiyerarşisi içindeki görüşlerden bahsediyorum:

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

Parmağını içeri soktuğunu söyle D. İşte olacaklar:

  1. hitTest:withEvent:Agörünüm hiyerarşisinin en üst görünümü olan çağrılır .
  2. pointInside:withEvent: her görünümde yinelemeli olarak çağrılır.
    1. pointInside:withEvent:çağrılır Ave geri dönerYES
    2. pointInside:withEvent:çağrılır Bve geri dönerNO
    3. pointInside:withEvent:çağrılır Cve geri dönerYES
    4. pointInside:withEvent:çağrılır Dve geri dönerYES
  3. Geri dönen görünümlerde YES, dokunmanın gerçekleştiği alt görünümü görmek için hiyerarşiye bakacaktır. Bu durumda, gelen A, Cve D, öyle olacak D.
  4. D isabet testi görünümü olacak

Cevap için teşekkür ederim. Tanımladığınız şey de aklımda olan şeydi, ancak @MHC hitTest:withEvent:B, C ve D'nin de çağrıştırıldığını söylüyor . D, A'nın değil, C'nin bir alt görünümü ise ne olur? Sanırım kafam karıştı ...
realstuff02

2
Çizim, D, C bir Subview olduğu
PGB

1
Olmaz Adönmek YES, hem de tıpkı Cve Dyapar?
Martin Wickman

2
Görünmez olan (.hidden veya 0.1'in altında opaklık) veya kullanıcı etkileşiminin kapatıldığı görünümlerin hitTest'e asla yanıt vermeyeceğini unutmayın. İlk etapta bu nesnelere hitTest'in çağrıldığını sanmıyorum.
Jonny

Sadece hitTest'i eklemek istedim: withEvent: hiyerarşilerine bağlı olarak tüm görünümlerde çağrılabilir.
Adithya

47

İOS'taki bu Hit-Testing'i çok yardımcı buluyorum

görüntü açıklamasını buraya girin

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

Swift 4'ü düzenleyin:

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
}

Yani bunu bir UIView alt sınıfına eklemeniz ve hiyerarşinizdeki tüm görünümlerin ondan devralması gerekiyor mu?
Guig

21

Cevaplar için teşekkürler, durumu "bindirme" görünümleriyle çözmeme yardımcı oldular.

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

Varsayalım X- kullanıcının dokunuşu. pointInside:withEvent:üzerinde Bgetiri NO, yani hitTest:withEvent:getiriler A. UIViewEn görünür üst görünümde dokunmaya ihtiyaç duyduğunuzda sorunu halletmek için kategori yazdım .

- (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. Gizli veya şeffaf görünümler için dokunma olayları veya userInteractionEnabledayarlı görünümler göndermemeliyiz NO;
  2. Dokunma içeride ise self, selfpotansiyel sonuç olarak değerlendirilecektir.
  3. İsabet için tüm alt görünümleri yinelemeli olarak kontrol edin. Varsa iade edin.
  4. Adım 2'deki sonuca bağlı olarak başka türlü kendi kendine veya sıfıra dön.

[self.subviewsreverseObjectEnumerator]En üstten alta doğru görünüm hiyerarşisine uyulması gerektiğini unutmayın . Ve clipsToBoundsmaskelenmiş alt görünümlerin test edilmediğinden emin olmak için kontrol edin .

Kullanım:

  1. Alt sınıf görünümünüzdeki kategoriyi içe aktarın.
  2. Değiştir hitTest:withEvent:bununla
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Resmi Apple'ın Kılavuzu da bazı iyi örnekler sunar.

Umarım bu birine yardımcı olur.


İnanılmaz! Net mantık ve HARİKA kod pasajı için teşekkürler, kafa karıştırıcımı çözdüm!
Thompson

@ Aslan, Güzel cevap. Ayrıca ilk adımda rengi temizlemek için eşitliği kontrol edebilirsiniz.
aquarium_moose

3

Bu pasaj gibi görünüyor!

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

Bunu anlaşılması en kolay cevap olarak buluyorum ve gerçek davranışla ilgili gözlemlerime çok yakından uyuyor. Tek fark, alt görünümlerin ters sırada numaralandırılmasıdır, bu nedenle öne daha yakın olan alt görünümler, arkalarındaki kardeşlere göre daha çok dokunuşlar alır.
Douglas Hill

@DouglasHill, düzeltmeniz için teşekkürler. Saygılarımızla
hippo

1

@Lion pasajı bir cazibe gibi çalışır. 2.1'i hızlı bir şekilde taşıdım ve UIView için bir uzantı olarak kullandım. Birinin ihtiyacı olursa diye buraya gönderiyorum.

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

Bunu kullanmak için uiview'unuzda hitTest: point: withEvent'i aşağıdaki gibi geçersiz kılmanız yeterlidir:

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

0

Sınıf diyagramı

Hit Testing

Bulmak bir First Responder

First Responderbu durumda, UIView point()doğru döndürülen en derin yöntemdir

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

Dahili olarak hitTest()görünüyor

func hitTest() -> View? {

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

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

}

Dokunma Etkinliğini şuraya gönder: 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?)

Örneğe bir göz atalım

Yanıtlayıcı Zinciri

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

Örneğe bir göz atın

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]

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.