Câu hỏi Chụp chạm trên một khung nhìn bên ngoài khung của superview của nó bằng cách sử dụng hitTest: withEvent:


Vấn đề của tôi: Tôi có một người giám sát EditView chiếm toàn bộ khung ứng dụng và một khung nhìn con MenuView chỉ chiếm ~ 20%, và sau đó MenuView chứa subview riêng của nó ButtonView mà thực sự nằm bên ngoài MenuView's giới hạn (một cái gì đó như thế này: ButtonView.frame.origin.y = -100).

(chú thích: EditView có các bản xem phụ khác không phải là một phần của MenuView's xem hệ thống phân cấp, nhưng có thể ảnh hưởng đến câu trả lời.)

Có thể bạn đã biết vấn đề: khi nào ButtonView nằm trong giới hạn của MenuView (hoặc, cụ thể hơn, khi chạm của tôi nằm trong MenuViewgiới hạn), ButtonView phản hồi các sự kiện liên lạc. Khi chạm của tôi ở bên ngoài MenuViewgiới hạn của (nhưng vẫn nằm trong ButtonView's giới hạn), không có sự kiện cảm ứng được nhận bởi ButtonView.

Thí dụ:

  • (E) là EditView, cha mẹ của tất cả các chế độ xem
  • (M) là MenuView, một chế độ xem phụ của EditView
  • (B) là ButtonView, một chế độ xem con của MenuView

Sơ đồ: 

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

Bởi vì (B) nằm ngoài khung (M), một vòi ở khu vực (B) sẽ không bao giờ được gửi đến (M) - trên thực tế, (M) không bao giờ phân tích cảm ứng trong trường hợp này, và cảm ứng được gửi đến đối tượng tiếp theo trong cấu trúc phân cấp.

Mục tiêu: Tôi thu thập điều đó hitTest:withEvent: có thể giải quyết vấn đề này, nhưng tôi không hiểu chính xác như thế nào. Trong trường hợp của tôi, nên hitTest:withEvent: được ghi đè trong EditView (giám sát 'chủ nhân' của tôi)? Hoặc nó nên được ghi đè trong MenuView, người giám sát trực tiếp của nút không nhận được chạm? Hay tôi đang nghĩ về điều này không chính xác?

Nếu điều này đòi hỏi một lời giải thích dài dòng, một nguồn tài nguyên trực tuyến tốt sẽ hữu ích - ngoại trừ tài liệu UIView của Apple, những tài liệu này chưa làm rõ với tôi.

Cảm ơn!


76
2017-08-02 03:41


gốc




Các câu trả lời:


Tôi đã sửa đổi mã của câu trả lời được chấp nhận chung hơn - nó xử lý các trường hợp mà chế độ xem clip xem các giới hạn của nó, có thể bị ẩn và quan trọng hơn: nếu các bản xem phụ là cấu trúc phân cấp phức tạp, thì chế độ xem chính xác sẽ được trả lại.

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

Tôi hy vọng điều này sẽ giúp bất cứ ai đang cố gắng sử dụng giải pháp này cho các trường hợp sử dụng phức tạp hơn.


123
2018-02-14 13:13



Điều này có vẻ tốt và cảm ơn vì đã làm điều đó. Tuy nhiên, có một câu hỏi: tại sao bạn lại quay lại [super hitTest: point withEvent: event]; ? Bạn sẽ không chỉ trở lại nil nếu không có subview mà gây ra các liên lạc? Apple nói hitTest trả về nil nếu không có subview chứa các liên lạc. - Ser Pounce
Hm ... Yea, nghe có vẻ đúng. Ngoài ra, đối với hành vi chính xác, các đối tượng cần phải được lặp lại theo thứ tự ngược lại (vì đối tượng cuối cùng là một đối tượng trực quan nhất). Đã chỉnh sửa mã để khớp. - Noam
Hoàn hảo, điều này đã giúp tôi tạo ra một menu phụ tùy chỉnh cho dự án của tôi, nơi tôi mang theo UIButtons tùy chỉnh làm menu - Jason Renaldo
Tôi đã chỉ sử dụng giải pháp của bạn để làm cho một UIButton nắm bắt các liên lạc, và nó là bên trong của một UIView đó là bên trong của một UICollectionViewCell bên trong của (rõ ràng) một UICollectionView. Tôi đã phải phân lớp UICollectionView và UICollectionViewCell để ghi đè hitTest: withEvent: trên ba lớp này. Và nó hoạt động như sự quyến rũ !! Cảm ơn !! - Daniel García
Tùy thuộc vào mức sử dụng mong muốn, nó phải trả về [super hitTest: point withEvent: event] hoặc nil. Tự quay trở lại sẽ khiến nó nhận mọi thứ. - Noam


Ok, tôi đã làm một số đào và thử nghiệm, đây là cách hitTest:withEvent công trình - ít nhất ở mức cao. Hình ảnh kịch bản này:

  • (E) là EditView, cha mẹ của tất cả các chế độ xem
  • (M) là MenuView, một chế độ xem phụ của EditView
  • (B) là ButtonView, một chế độ xem con của MenuView

Sơ đồ:

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

Bởi vì (B) nằm ngoài khung (M), một vòi ở khu vực (B) sẽ không bao giờ được gửi đến (M) - trên thực tế, (M) không bao giờ phân tích cảm ứng trong trường hợp này, và cảm ứng được gửi đến đối tượng tiếp theo trong cấu trúc phân cấp.

Tuy nhiên, nếu bạn triển khai hitTest:withEvent: trong (M), vòi bất cứ nơi nào trong ứng dụng sẽ được gửi đến (M) (hoặc ít nhất nó biết về chúng). Bạn có thể viết mã để xử lý các liên lạc trong trường hợp đó và trả lại đối tượng mà sẽ nhận được liên lạc.

Cụ thể hơn: mục tiêu của hitTest:withEvent: là trả về đối tượng sẽ nhận được lần truy cập. Vì vậy, trong (M) bạn có thể viết mã như thế này:

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

Tôi vẫn còn rất mới với phương pháp này và vấn đề này, vì vậy nếu có cách hiệu quả hơn hoặc chính xác để viết mã, xin vui lòng bình luận.

Tôi hy vọng rằng sẽ giúp bất cứ ai khác truy cập câu hỏi này sau. :)


26
2017-08-03 17:39



Cảm ơn, điều này đã làm việc cho tôi, mặc dù tôi đã phải làm một số logic hơn để xác định xem các bài đánh giá nào thực sự sẽ nhận được lượt truy cập. Tôi đã xóa một khoảng ngắt không cần thiết khỏi btw mẫu của bạn. - Daniel Saidi
Khi khung subview là khung của cha mẹ xem, bấm vào sự kiện đã không được đáp ứng! Giải pháp của bạn đã khắc phục sự cố này. Cảm ơn rất nhiều :-) - byJeevan


Trong Swift 4

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
}

18
2018-03-28 14:24





Những gì tôi sẽ làm là có cả ButtonView và MenuView tồn tại ở cùng cấp trong hệ thống phân cấp khung nhìn bằng cách đặt chúng cả trong một thùng chứa có khung hoàn toàn phù hợp với cả hai. Bằng cách này, khu vực tương tác của mục được cắt bớt sẽ không bị bỏ qua vì đó là ranh giới của người giám sát.


2
2017-08-02 04:07



tôi cũng nghĩ về cách giải quyết này - có nghĩa là tôi sẽ phải lặp lại một số logic vị trí (hoặc refactor một số mã nghiêm trọng!), nhưng nó thực sự có thể là sự lựa chọn tốt nhất của tôi cuối cùng .. - toblerpwn


Nếu có ai cần, đây là giải pháp thay thế nhanh chóng

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
2017-08-26 17:16





Nếu bạn có nhiều subviews khác trong view cha của bạn thì có lẽ hầu hết các view tương tác khác sẽ không hoạt động nếu bạn sử dụng các giải pháp trên, trong trường hợp đó bạn có thể sử dụng một cái gì đó như thế này (Trong 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)
    }
}

0
2017-11-03 06:51





Đặt các dòng mã dưới đây vào phân cấp chế độ xem của bạn:

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

Để giải thích rõ hơn, nó đã được giải thích trong blog của tôi: "goaheadwithiphonetech" về "Chú dẫn tùy chỉnh: Nút không phải là vấn đề có thể nhấp".

Tôi hy vọng rằng sẽ giúp bạn...!!!


-1
2018-05-19 10:50



Blog của bạn đã bị xóa, vậy chúng tôi có thể tìm lời giải thích ở đâu? - ishahak