Penanganan acara untuk iOS - bagaimana hitTest: withEvent: dan pointInside: withEvent: terkait?


145

Sementara sebagian besar dokumen apel ditulis dengan sangat baik, saya pikir ' Panduan Penanganan Acara untuk iOS ' merupakan pengecualian. Sulit bagi saya untuk memahami dengan jelas apa yang telah dijelaskan di sana.

Dokumen itu mengatakan,

Dalam pengujian hit, jendela memanggil hitTest:withEvent:tampilan hierarki tampilan paling atas; metode ini dilanjutkan dengan secara rekursif memanggil pointInside:withEvent:setiap tampilan dalam hierarki tampilan yang mengembalikan YA, melanjutkan hierarki hingga menemukan subview di mana batas-batasnya terjadi. Pandangan itu menjadi tampilan uji coba.

Jadi, apakah hanya hitTest:withEvent:tampilan paling atas yang dipanggil oleh sistem, yang memanggil pointInside:withEvent:semua subview, dan jika pengembalian dari subview tertentu adalah YA, maka panggilan pointInside:withEvent:subclass subview itu?


3
Sebuah tutorial yang sangat bagus yang membantu saya keluar dari tautan
anneblue

Dokumen serupa yang lebih baru untuk ini sekarang adalah developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

Jawaban:


173

Sepertinya pertanyaan yang cukup mendasar. Tapi saya setuju dengan Anda dokumennya tidak sejelas dokumen lainnya, jadi inilah jawaban saya.

Implementasi hitTest:withEvent:di UIResponder melakukan hal berikut:

  • Itu panggilan pointInside:withEvent:dariself
  • Jika pengembaliannya TIDAK, hitTest:withEvent:pengembalian nil. akhir dari cerita.
  • Jika pengembaliannya YA, ia mengirim hitTest:withEvent:pesan ke subview-nya. dimulai dari subview tingkat atas, dan berlanjut ke tampilan lain hingga subview mengembalikan non- nilobjek, atau semua subview menerima pesan.
  • Jika subview mengembalikan non- nilobjek di pertama kalinya, yang pertama hitTest:withEvent:mengembalikan objek itu. akhir dari cerita.
  • Jika tidak ada subview mengembalikan non- nilobjek, yang pertama hitTest:withEvent:mengembalikanself

Proses ini berulang secara berulang, jadi biasanya tampilan daun hierarki tampilan dikembalikan pada akhirnya.

Namun, Anda mungkin mengganti hitTest:withEventuntuk melakukan sesuatu yang berbeda. Dalam banyak kasus, mengesampingkan pointInside:withEvent:lebih sederhana dan masih menyediakan opsi yang cukup untuk mengubah penanganan acara di aplikasi Anda.


Apakah maksud Anda hitTest:withEvent:dari semua subview akhirnya dijalankan?
realstuff02

2
Iya. Timpa saja hitTest:withEvent:dalam tampilan Anda (dan pointInsidejika Anda mau), cetak log dan panggil [super hitTest...untuk mencari tahu siapa hitTest:withEvent:yang dipanggil dalam urutan mana.
MHC

seharusnya tidak langkah 3 di mana Anda menyebutkan "Jika pengembaliannya YA, ia mengirimkan hitTest: withEvent: ... tidakkah seharusnya pointInside: withEvent? Saya pikir ia mengirimkan pointInside ke semua subview?
prostock

Kembali pada bulan Februari pertama kali mengirim hitTest: withEvent :, di mana titikInside: withEvent: dikirim ke dirinya sendiri. Saya belum memeriksa ulang perilaku ini dengan versi SDK berikut, tapi saya pikir mengirim hitTest: withEvent: lebih masuk akal karena memberikan kontrol tingkat yang lebih tinggi dari apakah suatu acara menjadi milik tampilan atau tidak; pointInside: withEvent: memberi tahu apakah lokasi acara sedang dilihat atau tidak, bukan apakah acara tersebut milik tampilan. Misalnya, subview mungkin tidak ingin menangani suatu peristiwa bahkan jika lokasinya ada di subview.
MHC

1
WWDC2014 Sesi 235 - Tampilan Slide Tingkat Lanjut dan Teknik Penanganan Sentuh memberikan penjelasan dan contoh yang bagus untuk masalah ini.
antonio081014

297

Saya pikir Anda membingungkan subkelas dengan hierarki tampilan. Apa yang dikatakan dokter adalah sebagai berikut. Katakanlah Anda memiliki hierarki tampilan ini. Menurut hierarki, saya tidak berbicara tentang hierarki kelas, tetapi pandangan dalam hierarki tampilan, sebagai berikut:

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

Katakanlah Anda memasukkan jari Anda ke dalam D. Inilah yang akan terjadi:

  1. hitTest:withEvent:dipanggil A, tampilan paling atas dari hierarki tampilan.
  2. pointInside:withEvent: disebut secara rekursif pada setiap tampilan.
    1. pointInside:withEvent:dipanggil A, dan kembaliYES
    2. pointInside:withEvent:dipanggil B, dan kembaliNO
    3. pointInside:withEvent:dipanggil C, dan kembaliYES
    4. pointInside:withEvent:dipanggil D, dan kembaliYES
  3. Pada tampilan yang kembali YES, itu akan melihat hierarki untuk melihat subview di mana sentuhan itu terjadi. Dalam hal ini, dari A, Cdan D, itu akan terjadi D.
  4. D akan menjadi tampilan uji coba

Terima kasih atas jawabannya. Apa yang Anda jelaskan juga apa yang ada dalam pikiran saya, tetapi @MHC mengatakan hitTest:withEvent:tentang B, C dan D juga dipanggil. Apa yang terjadi jika D adalah subview dari C, bukan A? Saya pikir saya bingung ...
realstuff02

2
Dalam gambar saya, D adalah subview dari C.
pgb

1
Tidak akan Akembali YESjuga, sama seperti Cdan Dtidak?
Martin Wickman

2
Jangan lupa bahwa tampilan yang tidak terlihat (baik oleh .hidden atau opacity di bawah 0,1), atau menonaktifkan interaksi pengguna tidak akan pernah menanggapi hitTest. Saya tidak berpikir hitTest dipanggil pada benda-benda ini di tempat pertama.
Jonny

Hanya ingin menambahkan hitTest: withEvent: dapat dipanggil pada semua tampilan tergantung pada hierarki mereka.
Adithya

47

Saya menemukan Hit-Testing ini di iOS sangat membantu

masukkan deskripsi gambar di sini

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

Edit Swift 4:

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
}

Jadi, Anda perlu menambahkan ini ke subkelas dari UIView dan memiliki semua tampilan dalam hierarki yang Anda warisi darinya?
Guig

21

Terima kasih atas jawaban, mereka membantu saya untuk menyelesaikan situasi dengan pandangan "overlay".

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

Asumsikan X- sentuhan pengguna. pointInside:withEvent:pada Bpengembalian NO, jadi hitTest:withEvent:kembali A. Aku menulis kategori pada UIViewisu pegangan ketika Anda harus menerima sentuhan di atas yang paling terlihat pandangan.

- (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. Kita tidak boleh mengirim acara sentuh untuk tampilan tersembunyi atau transparan, atau tampilan dengan userInteractionEnabledset ke NO;
  2. Jika sentuhan ada di dalam self, selfakan dianggap sebagai hasil yang potensial.
  3. Periksa secara rekursif semua subview untuk klik. Jika ada, kembalikan.
  4. Kembalikan diri atau nol tergantung pada hasil dari langkah 2.

Catatan, [self.subviewsreverseObjectEnumerator]diperlukan untuk mengikuti hierarki tampilan dari paling atas ke bawah. Dan periksa untuk clipsToBoundsmemastikan tidak menguji subview bertopeng.

Pemakaian:

  1. Impor kategori dalam tampilan subkelas Anda.
  2. Ganti hitTest:withEvent:dengan ini
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Official Apple's Guide juga memberikan beberapa ilustrasi yang bagus.

Semoga ini bisa membantu seseorang.


Luar biasa! Terima kasih atas logika yang jelas dan cuplikan kode HEBAT, memecahkan kepalaku!
Thompson

@ Singa, jawaban yang bagus. Anda juga dapat memeriksa kesetaraan untuk menghapus warna di langkah pertama.
aquarium_moose

3

Itu menunjukkan seperti cuplikan ini!

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

Saya menemukan ini jawaban yang paling mudah dimengerti, dan sangat cocok dengan pengamatan saya tentang perilaku aktual. Satu-satunya perbedaan adalah bahwa subview dihitung dalam urutan terbalik, sehingga subview lebih dekat ke depan menerima sentuhan dalam preferensi untuk saudara kandung di belakang mereka.
Douglas Hill

@DouglasHill terima kasih atas koreksi Anda. Hormat kami
hippo

1

Potongan @lion bekerja seperti pesona. Saya porting ke swift 2.1 dan menggunakannya sebagai ekstensi untuk UIView. Saya mempostingnya di sini kalau-kalau ada yang membutuhkannya.

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

Untuk menggunakannya, cukup ganti hitTest: point: withEvent di uiview Anda sebagai berikut:

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

Diagram kelas

Hit Testing

Menemukan sebuah First Responder

First Responderdalam hal ini adalah UIView point()metode terdalam yang mengembalikan true

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

Secara internal hitTest()terlihat seperti

hitTest() {

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

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

    return nil

}

Kirim Sentuh Acara ke 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?)

Mari kita lihat contohnya

Rantai Penanggap

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

Lihatlah contohnya

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]

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.