Ghi lại các lần chạm vào một chế độ xem phụ bên ngoài khung của chế độ xem siêu cao của nó bằng hitTest: withEvent:


94

Vấn đề của tôi: Tôi có một chế độ EditViewxem siêu cấp chiếm cơ bản toàn bộ khung ứng dụng và một chế độ MenuViewxem phụ chỉ chiếm phần dưới cùng ~ 20%, sau đó MenuViewchứa chế độ xem phụ của chính ButtonViewnó thực sự nằm ngoài MenuViewgiới hạn của nó (đại loại như thế này ButtonView.frame.origin.y = -100:).

(lưu ý: EditViewcó các MenuViewchế độ xem phụ khác không thuộc hệ thống phân cấp chế độ xem của, nhưng có thể ảnh hưởng đến câu trả lời.)

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

Thí dụ:

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

Biểu đồ:

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

Bởi vì (B) nằm ngoài khung của (M), một lần nhấn trong vùng (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 lần chạm trong trường hợp này và lần chạm được gửi đến đối tượng tiếp theo trong hệ thống phân cấp.

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

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

Cảm ơn!

Câu trả lời:


145

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

- (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 kỳ 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.


Đ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 trả về [super hitTest: point withEvent: event]; ? Bạn sẽ không trả về con số không nếu không có lượt xem phụ nào kích hoạt liên lạc? Apple cho biết hitTest trả về con số không nếu không có lượt xem phụ nào có liên quan.
Ser Pounce

Hm ... Yea, nghe có vẻ vừa phải. Ngoài ra, để có hành vi chính xác, các đối tượng cần được lặp lại theo thứ tự ngược lại (vì đối tượng cuối cùng là đối tượng trên cùng về mặt trực quan). Đã chỉnh sửa mã để phù hợp.
Noam

3
Tôi vừa sử dụng giải pháp của bạn để làm cho một UIButton nắm bắt được cảm giác và nó nằm bên trong một UIView, bên trong một UICollectionViewCell bên trong (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ư một sự quyến rũ !! Cảm ơn !!
Daniel García,

3
Tùy thuộc vào mục đích sử dụng mong muốn, nó sẽ trả về [super hitTest: point withEvent: event] hoặc nil. Trả lại bản thân sẽ khiến nó nhận được mọi thứ.
Noam

1
Dưới đây là một Q & A doc kỹ thuật từ Apple về kỹ thuật này giống nhau: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang

33

Được rồi, tôi đã đào và thử nghiệm, đây là cách hitTest:withEventhoạt động - ít nhất là ở cấp độ cao. Hình ảnh kịch bản này:

  • (E) là EditView , cha 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 phụ của MenuView

Biểu đồ:

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

Bởi vì (B) nằm ngoài khung của (M), một lần nhấn trong vùng (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 lần chạm trong trường hợp này và lần chạm được gửi đến đối tượng tiếp theo trong hệ thống phân cấp.

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

Cụ thể hơn: mục tiêu hitTest:withEvent:là trả lại đối tượng sẽ nhận đòn đánh. 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 nào 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 điều đó sẽ giúp bất kỳ ai khác gặp câu hỏi này sau này. :)


Cảm ơn, điều này đã hiệu quả với tôi, mặc dù tôi phải thực hiện thêm một số logic để xác định lượt xem phụ nào thực sự sẽ nhận được lượt truy cập. Tôi đã loại bỏ ngắt quãng không cần thiết khỏi btw ví dụ của bạn.
Daniel Saidi

Khi khung chế độ xem phụ là khung của chế độ xem gốc, sự kiện nhấp chuột không phản hồi! Giải pháp của bạn đã khắc phục sự cố này. Cảm ơn rất nhiều :-)
byJeevan

@toblerpwn (biệt danh hài hước :)) bạn nên chỉnh sửa câu trả lời cũ tuyệt vời này để làm cho nó thật rõ ràng (SỬ DỤNG CHỮ HOA LỚN) trong lớp nào bạn phải thêm câu này. Chúc mừng!
Fattie

26

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

Điều này sẽ chỉ hoạt động nếu bạn áp dụng ghi đè trên chế độ xem trực tiếp của chế độ xem "hoạt động sai"
Hudi Ilfeld

2

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


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

1

Nếu bạn có nhiều chế độ xem phụ khác bên trong chế độ xem mẹ của mình thì có lẽ hầu hết các chế độ xem 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

Nếu ai cần 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

Đặt các dòng mã bên dưới vào hệ thống 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;
}

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

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


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