Xử lý sự kiện cho iOS - cách hitTest: withEvent: và pointInside: withEvent: có liên quan?


145

Mặc dù hầu hết các tài liệu của apple đều được viết rất tốt, tôi nghĩ rằng ' Hướng dẫn xử lý sự kiện cho iOS ' là một ngoại lệ. Thật khó cho tôi để hiểu rõ những gì được mô tả ở đó.

Tài liệu nói rằng,

Trong thử nghiệm nhấn, một cửa sổ gọi hitTest:withEvent:trên chế độ xem trên cùng của hệ thống phân cấp chế độ xem; phương pháp này tiến hành bằng cách gọi đệ quy pointInside:withEvent:trên mỗi chế độ xem trong hệ thống phân cấp chế độ xem trả về CÓ, tiếp tục phân cấp cho đến khi tìm thấy cuộc phỏng vấn trong phạm vi giới hạn của việc chạm. Khung nhìn đó trở thành khung nhìn hit-test.

Vì vậy, có phải chỉ có hitTest:withEvent:chế độ xem trên cùng được gọi bởi hệ thống, mà gọi pointInside:withEvent:tất cả các cuộc phỏng vấn và nếu trả về từ một cuộc phỏng vấn cụ thể là CÓ, thì các cuộc gọi pointInside:withEvent:của các lớp con của cuộc phỏng vấn đó là gì?


3
Một hướng dẫn rất hay giúp tôi thoát khỏi liên kết
anneblue

Tài liệu mới hơn tương đương cho điều này bây giờ có thể là developer.apple.com/documentation/uikit/uiview/1622469-hittest
Cur

Câu trả lời:


173

Có vẻ như một câu hỏi cơ bản. Nhưng tôi đồng ý với bạn tài liệu không rõ ràng như các tài liệu khác, vì vậy đây là câu trả lời của tôi.

Việc thực hiện hitTest:withEvent:trong UIResponder thực hiện như sau:

  • Nó kêu gọi pointInside:withEvent:củaself
  • Nếu lợi nhuận là KHÔNG, hitTest:withEvent:trả lại nil. kết thúc câu chuyện.
  • Nếu trả về là CÓ, nó sẽ gửi hitTest:withEvent:tin nhắn đến các cuộc phỏng vấn của nó. nó bắt đầu từ chế độ xem phụ cấp cao nhất và tiếp tục đến các chế độ xem khác cho đến khi một cuộc phỏng vấn trả về một nilđối tượng không hoặc tất cả các cuộc phỏng vấn nhận được thông báo.
  • Nếu nillần đầu tiên trả về một đối tượng không phải là đối tượng, thì lần đầu tiên hitTest:withEvent:trả về đối tượng đó. kết thúc câu chuyện.
  • Nếu không có subview trả về một nilđối tượng không , thì lần đầu tiên hitTest:withEvent:trả vềself

Quá trình này lặp lại một cách đệ quy, do đó, thông thường chế độ xem lá của hệ thống phân cấp chế độ xem cuối cùng được trả về.

Tuy nhiên, bạn có thể ghi đè hitTest:withEventđể làm một cái gì đó khác nhau. Trong nhiều trường hợp, ghi đè pointInside:withEvent:đơn giản hơn và vẫn cung cấp đủ tùy chọn để điều chỉnh xử lý sự kiện trong ứng dụng của bạn.


Bạn có nghĩa là hitTest:withEvent:tất cả các cuộc phỏng vấn cuối cùng được thực hiện?
real ware02

2
Đúng. Chỉ cần ghi đè lên hitTest:withEvent:chế độ xem của bạn (và pointInsidenếu bạn muốn), in nhật ký và gọi [super hitTest...để tìm ra ai hitTest:withEvent:được gọi theo thứ tự nào.
MHC

không nên bước 3 trong đó bạn đề cập "Nếu lợi nhuận là CÓ, nó sẽ gửi hitTest: withEvent: ... không nên là pointInside: withEvent? Tôi nghĩ rằng nó sẽ gửi pointInside cho tất cả các
cuộc phỏng vấn

Trở lại vào tháng Hai, lần đầu tiên nó đã gửi hitTest: withEvent:, trong đó một pointInside: withEvent: đã được gửi tới chính nó. Tôi chưa kiểm tra lại hành vi này bằng các phiên bản SDK sau, nhưng tôi nghĩ việc gửi hitTest: withEvent: có ý nghĩa hơn vì nó cung cấp quyền kiểm soát cấp cao hơn về việc một sự kiện có thuộc về chế độ xem hay không; pointInside: withEvent: cho biết vị trí sự kiện có ở chế độ xem hay không, không phải là sự kiện có thuộc về chế độ xem hay không. Ví dụ: một khung nhìn phụ có thể không muốn xử lý một sự kiện ngay cả khi vị trí của nó nằm trên khung nhìn phụ.
MHC

1
WWDC2014 Phiên 235 - Kỹ thuật xử lý cuộn và xử lý cảm ứng nâng cao đưa ra lời giải thích và ví dụ tuyệt vời cho vấn đề này.
antonio081014

297

Tôi nghĩ rằng bạn đang nhầm lẫn phân lớp với phân cấp xem. Những gì các tài liệu nói là như sau. Nói rằng bạn có quan điểm phân cấp này. Theo phân cấp Tôi không nói về phân cấp lớp, nhưng các khung nhìn trong phân cấp khung nhìn, như sau:

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

Nói rằng bạn đặt ngón tay của bạn vào bên trong D. Đây là những gì sẽ xảy ra:

  1. hitTest:withEvent:được gọi trên A, chế độ xem trên cùng của hệ thống phân cấp chế độ xem.
  2. pointInside:withEvent: được gọi đệ quy trên mỗi khung nhìn.
    1. pointInside:withEvent:được gọi Avà trả vềYES
    2. pointInside:withEvent:được gọi Bvà trả vềNO
    3. pointInside:withEvent:được gọi Cvà trả vềYES
    4. pointInside:withEvent:được gọi Dvà trả vềYES
  3. Trên các khung nhìn được trả về YES, nó sẽ nhìn xuống hệ thống phân cấp để xem các khung nhìn phụ nơi diễn ra cảm ứng. Trong trường hợp này, từ A, CD, nó sẽ được D.
  4. D sẽ là chế độ xem thử nghiệm

Cảm ơn bạn đã trả lời. Những gì bạn mô tả cũng là những gì trong tâm trí của tôi, nhưng @MHC nói hitTest:withEvent:về B, C và D cũng được viện dẫn. Điều gì xảy ra nếu D là một subview của C, không phải A? Tôi nghĩ rằng tôi đã nhầm lẫn ...
real ware02

2
Trong bản vẽ của tôi, D là một cuộc
phỏng vấn

1
Sẽ không Atrở lại YESlà tốt, cũng như CDkhông?
Martin Wickman

2
Đừng quên rằng các chế độ xem vô hình (bằng .hidden hoặc opacity dưới 0,1) hoặc tắt tương tác người dùng sẽ không bao giờ phản hồi với hitTest. Tôi không nghĩ hitTest đang được gọi trên các đối tượng này ngay từ đầu.
Jonny

Chỉ muốn thêm hitTest: withEvent: có thể được gọi trên tất cả các chế độ xem tùy theo phân cấp của chúng.
Adithya

47

Tôi thấy việc Thử nghiệm Hit trong iOS này rất hữu ích

nhập mô tả hình ảnh ở đây

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

Chỉnh sửa 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
}

Vì vậy, bạn cần thêm phần này vào một lớp con của UIView và tất cả các khung nhìn trong hệ thống phân cấp của bạn được thừa hưởng từ nó?
Guig

21

Cảm ơn câu trả lời, họ đã giúp tôi giải quyết tình huống với lượt xem "lớp phủ".

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

Giả sử X- liên lạc của người dùng. pointInside:withEvent:khi Btrở về NO, vì vậy hitTest:withEvent:trả lại A. Tôi đã viết chuyên mục UIViewđể xử lý vấn đề khi bạn cần nhận được liên lạc trên chế độ xem dễ nhìn nhất .

- (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. Chúng ta không nên gửi các sự kiện chạm cho các chế độ xem ẩn hoặc trong suốt hoặc các chế độ xem userInteractionEnabledđược đặt thành NO;
  2. Nếu cảm ứng là bên trong self, selfsẽ được coi là kết quả tiềm năng.
  3. Kiểm tra đệ quy tất cả các cuộc phỏng vấn cho hit. Nếu có, trả lại nó.
  4. Khác tự trả về hoặc không tùy thuộc vào kết quả từ bước 2.

Lưu ý, [self.subviewsreverseObjectEnumerator]cần thiết để theo dõi phân cấp xem từ trên cùng xuống dưới. Và kiểm tra clipsToBoundsđể đảm bảo không kiểm tra các cuộc phỏng vấn đeo mặt nạ.

Sử dụng:

  1. Thể loại nhập trong chế độ xem phân lớp của bạn.
  2. Thay thế hitTest:withEvent:bằng cái này
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

Hướng dẫn chính thức của Apple cũng cung cấp một số hình ảnh minh họa tốt.

Hy vọng điều này sẽ giúp ai đó.


Kinh ngạc! Cảm ơn logic rõ ràng và đoạn mã TUYỆT VỜI, đã giải quyết vấn đề đầu của tôi!
Thompson

@Lion, Câu trả lời hay. Ngoài ra, bạn có thể kiểm tra sự bình đẳng để xóa màu trong bước đầu tiên.
hồ

3

Nó cho thấy như đoạn trích này!

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

Tôi thấy đây là câu trả lời dễ hiểu nhất và nó rất phù hợp với những quan sát của tôi về hành vi thực tế. Sự khác biệt duy nhất là các cuộc phỏng vấn được liệt kê theo thứ tự ngược lại, vì vậy các cuộc phỏng vấn gần phía trước sẽ nhận được sự ưu tiên cho anh chị em đằng sau chúng.
Đồi Douglas

@DoumundHill nhờ sự điều chỉnh của bạn. Trân trọng
hà mã

1

Đoạn trích của @lion hoạt động như một lá bùa. Tôi đã chuyển nó sang swift 2.1 và sử dụng nó như một phần mở rộng cho UIView. Tôi đang đăng nó ở đây trong trường hợp ai đó cần nó.

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

Để sử dụng nó, chỉ cần ghi đè hitTest: point: withEvent trong uiview của bạn như sau:

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ơ đồ lớp

Lượt kiểm tra

Tìm một First Responder

First Respondertrong trường hợp này là UIView point()phương pháp sâu nhất mà trả về đúng

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

Bên trong hitTest()trông giống như

hitTest() {

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

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

    return nil

}

Gửi sự kiện cảm ứng đến 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?)

Hãy xem ví dụ

Chuỗi phản hồi

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

Hãy xem ví dụ

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]

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.