Menangkap sentuhan pada subview di luar bingkai superview menggunakan hitTest: withEvent:

94

Masalah saya: Saya memiliki superview EditViewyang pada dasarnya menempati seluruh bingkai aplikasi, dan subview MenuViewyang hanya menempati bagian bawah ~ 20%, dan kemudian MenuViewberisi subviewnya sendiri ButtonViewyang sebenarnya berada di luar MenuViewbatas (seperti ini ButtonView.frame.origin.y = -100:).

(catatan: EditViewmemiliki subview lain yang bukan bagian dari MenuViewhierarki tampilan, tetapi dapat mempengaruhi jawabannya.)

Anda mungkin sudah tahu masalahnya: kapan ButtonViewberada dalam batas-batas MenuView(atau, lebih khusus lagi, ketika sentuhan saya berada dalam MenuViewbatas-batas), ButtonViewmenanggapi peristiwa sentuh. Saat sentuhan saya berada di luar MenuViewbatas (namun masih dalam ButtonViewbatas), tidak ada peristiwa sentuhan yang diterima oleh ButtonView.

Contoh:

  • (E) adalah EditView, induk dari semua tampilan
  • (M) adalah MenuView, subview dari EditView
  • (B) adalah ButtonView, subview dari MenuView

Diagram:

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

Karena (B) berada di luar bingkai (M), ketukan di wilayah (B) tidak akan pernah dikirim ke (M) - faktanya, (M) tidak pernah menganalisis sentuhan dalam kasus ini, dan sentuhan dikirim ke objek berikutnya dalam hierarki.

Sasaran: Saya menyimpulkan bahwa penimpaan hitTest:withEvent:dapat menyelesaikan masalah ini, tetapi saya tidak mengerti persis bagaimana caranya. Dalam kasus saya, harus hitTest:withEvent:diganti di EditView(superview 'master' saya)? Atau haruskah diganti MenuView, tampilan langsung tombol yang tidak menerima sentuhan? Atau apakah saya memikirkan hal ini dengan tidak benar?

Jika ini membutuhkan penjelasan yang panjang, sumber online yang bagus akan membantu - kecuali dokumen UIView Apple, yang belum menjelaskannya kepada saya.

Terima kasih!

toblerpwn
sumber

Jawaban:

145

Saya telah memodifikasi kode jawaban yang diterima menjadi lebih umum - ini menangani kasus di mana tampilan melakukan subview klip ke batasnya, mungkin disembunyikan, dan yang lebih penting: jika subview adalah hierarki tampilan yang kompleks, subview yang benar akan dikembalikan.

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

Saya harap ini membantu siapa pun yang mencoba menggunakan solusi ini untuk kasus penggunaan yang lebih kompleks.

Noam
sumber
Ini terlihat bagus dan terima kasih telah melakukannya. Namun, ada satu pertanyaan: mengapa Anda melakukan return [super hitTest: point withEvent: event]; ? Bukankah Anda akan mengembalikan nol jika tidak ada subview yang memicu sentuhan? Apple mengatakan hitTest mengembalikan nol jika tidak ada subview berisi sentuhan.
Ser Pounce
Hm ... Ya, kedengarannya benar. Selain itu, untuk perilaku yang benar, objek perlu diiterasi dalam urutan terbalik (karena yang terakhir secara visual paling atas). Kode diedit untuk dicocokkan.
Noam
3
Saya baru saja menggunakan solusi Anda untuk membuat UIButton menangkap sentuhan, dan itu ada di dalam UIView yang ada di dalam UICollectionViewCell di dalam (jelas) UICollectionView. Saya harus membuat subkelas UICollectionView dan UICollectionViewCell untuk mengganti hitTest: withEvent: pada tiga kelas ini. Dan itu bekerja seperti pesona !! Terima kasih !!
Daniel García
3
Bergantung pada penggunaan yang diinginkan, itu harus mengembalikan [super hitTest: point withEvent: event] atau nil. Mengembalikan diri akan menyebabkannya menerima segalanya.
Noam
1
Berikut adalah dokumen teknis Tanya Jawab dari Apple tentang teknik yang sama ini: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang
33

Oke, saya melakukan penggalian dan pengujian, berikut cara hitTest:withEventkerjanya - setidaknya pada level tinggi. Gambar skenario ini:

  • (E) adalah EditView , induk dari semua tampilan
  • (M) adalah MenuView , subview dari EditView
  • (B) adalah ButtonView , subview dari MenuView

Diagram:

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

Karena (B) berada di luar bingkai (M), ketukan di wilayah (B) tidak akan pernah dikirim ke (M) - faktanya, (M) tidak pernah menganalisis sentuhan dalam kasus ini, dan sentuhan dikirim ke objek berikutnya dalam hierarki.

Namun, jika Anda mengimplementasikan hitTest:withEvent:di (M), tap di mana saja dalam aplikasi akan dikirim ke (M) (atau setidaknya ia tahu tentang mereka). Anda dapat menulis kode untuk menangani sentuhan dalam kasus itu dan mengembalikan objek yang seharusnya menerima sentuhan tersebut.

Lebih khusus lagi: tujuannya hitTest:withEvent:adalah mengembalikan objek yang seharusnya menerima pukulan. Jadi, di (M) Anda mungkin menulis kode seperti ini:

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

Saya masih sangat baru dalam metode ini dan masalah ini, jadi jika ada cara yang lebih efisien atau benar untuk menulis kode, silakan beri komentar.

Saya harap itu membantu siapa pun yang menjawab pertanyaan ini nanti. :)

toblerpwn
sumber
Terima kasih, ini berhasil untuk saya, meskipun saya harus melakukan beberapa logika lagi untuk menentukan subview mana yang seharusnya benar-benar menerima hit. Saya menghapus jeda yang tidak diperlukan dari contoh btw Anda.
Daniel Saidi
Ketika bingkai subview berada di sekitar bingkai tampilan induk, peristiwa klik tidak merespons! Solusi Anda memperbaiki masalah ini. Terima kasih banyak :-)
byJeevan
@toblerpwn (nama panggilan lucu :)) Anda harus mengedit jawaban lama yang luar biasa ini untuk membuatnya sangat jelas (GUNAKAN HURUF BESAR BESAR) di kelas mana Anda harus menambahkan ini. Bersulang!
Fattie
26

Di 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
sumber
Ini hanya akan berfungsi jika Anda menerapkan penggantian pada tampilan super langsung dari tampilan "berperilaku tidak semestinya"
Hudi Ilfeld
2

Yang akan saya lakukan adalah membuat ButtonView dan MenuView berada pada level yang sama dalam hierarki tampilan dengan menempatkan keduanya dalam wadah yang bingkainya benar-benar sesuai dengan keduanya. Dengan cara ini, wilayah interaktif dari item yang terpotong tidak akan diabaikan karena itu adalah batas tampilan super.

mamackenzie
sumber
saya memikirkan solusi ini juga - itu berarti saya harus menduplikasi beberapa logika penempatan (atau memfaktor ulang beberapa kode serius!), tetapi pada akhirnya ini mungkin menjadi pilihan terbaik saya ..
toblerpwn
1

Jika Anda memiliki banyak subview lain di dalam tampilan induk Anda, maka mungkin sebagian besar tampilan interaktif lainnya tidak akan berfungsi jika Anda menggunakan solusi di atas, dalam hal ini Anda dapat menggunakan sesuatu seperti ini (Di 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
sumber
0

Jika ada yang membutuhkannya, berikut adalah alternatif cepat

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
}
Nak
sumber
0

Tempatkan di bawah baris kode ke dalam hierarki tampilan Anda:

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

Untuk lebih jelasnya dijelaskan di blog saya: "goaheadwithiphonetech" mengenai "Custom callout: Button is not clickable issue".

Saya harap itu membantu Anda ... !!!

Sandip Patel - SM
sumber
Blog Anda telah dihapus, jadi di mana kami dapat menemukan penjelasannya?
ishahak