HitTest: withEvent kullanarak süpervizör görünümünün çerçevesi dışındaki bir alt görünüme dokunma yakalama:


94

Benim sorun: Bir Superview var EditViewtemelde tüm uygulama çerçevesini kaplıyor ve bir subview MenuViewsadece alt ~% 20 kadar sürer ve sonra MenuViewkendi subview içeren ButtonViewaslında dışında bulunduğu MenuViewbireyin sınırları (böyle bir şey: ButtonView.frame.origin.y = -100).

(not: görünüm hiyerarşisinin bir EditViewparçası olmayan MenuView, ancak yanıtı etkileyebilecek başka alt görünümlere sahiptir .)

Muhtemelen sorunu zaten biliyorsunuzdur: ne zaman ButtonViewsınırları dahilinde olduğunda MenuView(veya daha spesifik olarak, dokunuşlarım MenuViewsınırlar içinde olduğunda ), ButtonViewdokunma olaylarına yanıt verir. Dokunuşlarım MenuViewsınırlarının dışında (ancak yine de ButtonViewsınırları içinde) olduğunda, dokunma olayı almıyor ButtonView.

Misal:

  • (E), EditViewtüm görünümlerin ebeveynidir
  • (M), MenuViewEditView'ın bir alt görünümüdür
  • (B) ButtonViewMenuView'un bir alt görünümüdür

Diyagram:

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

(B), (M) çerçevesinin dışında olduğu için, (B) bölgesindeki bir dokunma asla (M) 'ye gönderilmez - aslında, (M) bu durumda dokunuşu asla analiz etmez ve dokunma hiyerarşideki bir sonraki nesne.

Amaç: Üstesinden gelmenin hitTest:withEvent:bu sorunu çözebileceğini anlıyorum, ancak tam olarak nasıl olduğunu anlamıyorum. Benim durumumda, hitTest:withEvent:geçersiz EditViewkılınmalı ('ana' denetimim)? Yoksa MenuViewdokunuşları almayan düğmenin doğrudan denetiminde geçersiz kılınmalı mı? Yoksa bunu yanlış mı düşünüyorum?

Bu, uzun bir açıklama gerektiriyorsa, Apple'ın UIView belgeleri dışında iyi bir çevrimiçi kaynak yardımcı olacaktır, ancak bunu bana netleştirmemişti.

Teşekkürler!

Yanıtlar:


145

Kabul edilen yanıtın kodunu daha genel olacak şekilde değiştirdim - görünümün alt görünümleri sınırlarına kadar kırptığı, gizli olabileceği ve daha da önemlisi: alt görünümler karmaşık görünüm hiyerarşileriyse, doğru alt görünüm döndürülecek.

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

Umarım bu, bu çözümü daha karmaşık kullanım durumları için kullanmaya çalışan herkese yardımcı olur.


Bu iyi görünüyor ve bunu yaptığınız için teşekkürler. Ancak, bir sorum vardı: neden return yapıyorsun [super hitTest: point withEvent: event]; ? Dokunmayı tetikleyen alt görünüm yoksa sıfır döndürmez misiniz? Apple, hiçbir alt görünümde dokunma yoksa hitTest'in sıfır döndürdüğünü söylüyor.
Ser Pounce

Hm ... Evet, kulağa doğru geliyor. Ayrıca, doğru davranış için nesnelerin ters sırada yinelenmesi gerekir (çünkü sonuncusu görsel olarak en üstte olandır). Kod eşleşecek şekilde düzenlendi.
Noam

3
Çözümünüzü bir UIButton'un dokunuşu yakalamak için kullandım ve bir UICollectionViewCell'in (tabii ki) bir UICollectionView içinde bulunan bir UIView içinde. Bu üç sınıfta hitTest: withEvent: öğesini geçersiz kılmak için UICollectionView ve UICollectionViewCell'i alt sınıflara ayırmak zorunda kaldım. Ve cazibe gibi çalışıyor !! Teşekkürler !!
Daniel García

3
İstenen kullanıma bağlı olarak, ya [super hitTest: point withEvent: event] ya da nil döndürmelidir. Benliğe geri dönmek, her şeyi almasına neden olur.
Noam

1
İşte aynı teknikle ilgili Apple'dan bir Soru-Cevap teknik dokümanı: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang

33

Tamam, biraz kazma ve test yaptım, işte nasıl hitTest:withEventçalışıyor - en azından yüksek seviyede. Bu senaryoyu görüntüleyin:

  • (E) EditView , tüm görünümlerin ebeveyni
  • (M) MenuView , bir EditView alt görünümüdür
  • (B) ButtonView , MenuView'ün bir alt görünümüdür

Diyagram:

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

(B), (M) çerçevesinin dışında olduğu için, (B) bölgesindeki bir dokunma asla (M) 'ye gönderilmez - aslında, (M) bu durumda dokunuşu asla analiz etmez ve dokunma hiyerarşideki bir sonraki nesne.

Bununla birlikte, hitTest:withEvent:(M) 'de uygularsanız , uygulamadaki herhangi bir yere dokunma (M)' ye gönderilecektir (veya en azından onlar hakkında bilgisi vardır). Bu durumda dokunuşu işlemek için kod yazabilir ve dokunuşu alması gereken nesneyi iade edebilirsiniz.

Daha spesifik olarak: hedefi hitTest:withEvent:isabet alması gereken nesneyi döndürmektir. Yani, (M) 'ye şu şekilde kod yazabilirsiniz:

// 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];
}

Bu yöntemde ve bu sorunda hala çok yeniyim, bu nedenle kodu yazmanın daha verimli veya doğru yolları varsa, lütfen yorum yapın.

Umarım bu, bu soruyu daha sonra soran herkese yardımcı olur. :)


Teşekkürler, bu benim için işe yaradı, ancak hangi alt görünümlerin gerçekten isabet alması gerektiğini belirlemek için biraz daha mantık yapmam gerekti. Örneğinizden gereksiz bir molayı kaldırdım btw.
Daniel Saidi

Alt görünüm çerçevesi üst görünümün dışındayken, tıklama olayı yanıt vermiyordu! Çözümünüz bu sorunu çözdü. Çok teşekkürler :-)
byJeevan

@toblerpwn (komik takma ad :)) bu mükemmel eski yanıtı, bunu eklemeniz gereken sınıfa çok açık hale getirmek için düzenlemelisiniz (BÜYÜK KALIN BÜYÜK HARFLER KULLANIN) . Şerefe!
Şişko

26

Swift 5'de

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
}

Üzerinde geçersiz kılmayı uygulamak Bu yalnızca çalışacaktır doğrudan "Hatalı davranan" görünümünün Superview
Hudi Ilfeld

2

Yapacağım şey, hem ButtonView hem de MenuView'ün görünüm hiyerarşisinde aynı seviyede var olmasını sağlamak ve ikisini de çerçevesi ikisine de tamamen uyan bir konteynere yerleştirmek. Bu şekilde, kırpılan öğenin etkileşimli bölgesi, süpervizörün sınırları nedeniyle göz ardı edilmeyecektir.


bu geçici çözümü de düşündüm - bu, bazı yerleştirme mantığını çoğaltmam (veya bazı ciddi kodları yeniden düzenlemem) gerekeceği anlamına geliyor, ancak sonunda en iyi seçimim olabilir ..
toblerpwn

1

Ebeveyn görünümünüzde başka birçok alt görünümünüz varsa, yukarıdaki çözümleri kullanırsanız muhtemelen diğer etkileşimli görünümlerin çoğu çalışmayacaktır, bu durumda şöyle bir şey kullanabilirsiniz (Swift 3.2'de):

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

0

İhtiyaç duyan varsa, işte hızlı alternatif

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
}

0

Görünüm hiyerarşinize aşağıdaki kod satırlarını yerleştirin:

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

Daha fazla açıklama için, blogumda "Özel belirtme çizgisi: Düğme tıklanabilir bir sorun değil" ile ilgili "goaheadwithiphonetech" açıklandı.

Umarım bu size yardımcı olur ... !!!


Blogunuz kaldırıldı, öyleyse açıklamayı nerede bulabiliriz lütfen?
ishahak
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.