Tương tác vượt quá giới hạn của UIView


92

Liệu UIButton (hoặc bất kỳ điều khiển nào khác cho vấn đề đó) có thể nhận các sự kiện cảm ứng khi khung của UIButton nằm ngoài khung của chính nó không? Vì khi tôi thử điều này, UIButton của tôi dường như không thể nhận bất kỳ sự kiện nào. Làm cách nào để giải quyết vấn đề này?


1
Điều này hoàn toàn phù hợp với tôi. developer.apple.com/library/ios/qa/qa2013/qa1812.html Tốt nhất
Frank Li

2
Đây cũng là tốt và phù hợp (hoặc có lẽ một dupe): stackoverflow.com/a/31420820/8047
Dan Rosenstark

Câu trả lời:


93

Đúng. Bạn có thể ghi đè hitTest:withEvent:phương thức để trả về một dạng xem cho một tập hợp các điểm lớn hơn dạng xem đó chứa. Xem Tham chiếu Lớp học UIView .

Chỉnh sửa: Ví dụ:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGFloat radius = 100.0;
    CGRect frame = CGRectMake(-radius, -radius,
                              self.frame.size.width + radius,
                              self.frame.size.height + radius);

    if (CGRectContainsPoint(frame, point)) {
        return self;
    }
    return nil;
}

Chỉnh sửa 2: (Sau khi làm rõ :) Để đảm bảo rằng nút được coi là nằm trong giới hạn của cha mẹ, bạn cần ghi đè pointInside:withEvent:trong cha mẹ để bao gồm khung của nút.

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    if (CGRectContainsPoint(self.view.bounds, point) ||
        CGRectContainsPoint(button.view.frame, point))
    {
        return YES;
    }
    return NO;
}

Lưu ý rằng mã ở đó để ghi đè pointInside không hoàn toàn chính xác. Như Summon giải thích bên dưới, hãy làm điều này:

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    {
    if ( CGRectContainsPoint(self.oversizeButton.frame, point) )
        return YES;

    return [super pointInside:point withEvent:event];
    }

Lưu ý rằng bạn rất có thể làm điều đó với self.oversizeButtontư cách là IBOutlet trong lớp con UIView này; thì bạn có thể chỉ cần kéo "nút quá khổ" được đề cập đến, đến chế độ xem đặc biệt được đề cập. (Hoặc, nếu vì lý do nào đó mà bạn làm việc này rất nhiều trong một dự án, bạn sẽ có một lớp con UIButton đặc biệt và bạn có thể xem qua danh sách lượt xem phụ của mình cho các lớp đó.) Hy vọng nó sẽ hữu ích.


Bạn có thể giúp tôi thêm về cách triển khai điều này một cách chính xác với một số mã mẫu không? Ngoài ra, tôi đã đọc tài liệu tham khảo và vì dòng sau, tôi bối rối: "Các điểm nằm ngoài giới hạn của người nhận không bao giờ được báo cáo là lần truy cập, ngay cả khi chúng thực sự nằm trong một trong các lượt xem phụ của người nhận. Các lượt xem phụ có thể mở rộng trực quan ra ngoài giới hạn của cấp độ gốc nếu thuộc tính clipsToBounds của chế độ xem gốc được đặt thành KHÔNG. Tuy nhiên, thử nghiệm lần truy cập luôn bỏ qua các điểm nằm ngoài giới hạn của chế độ xem gốc. "
ThomasM

Đặc biệt là phần này khiến có vẻ như đây không phải là giải pháp tôi cần: "Tuy nhiên, thử nghiệm lần truy cập luôn bỏ qua các điểm nằm ngoài giới hạn của chế độ xem gốc" .. Nhưng tất nhiên tôi có thể đã sai, tôi thấy các tham chiếu apple đôi khi thực sự khó hiểu ..
ThomasM

À, giờ thì tôi hiểu rồi. Bạn cần ghi đè của phụ huynh pointInside:withEvent:để bao gồm khung của nút. Tôi sẽ thêm một số mã mẫu vào câu trả lời của tôi ở trên.
jnic

Có cách nào để thực hiện việc này mà không cần ghi đè các phương thức của UIView không? Ví dụ: nếu các chế độ xem của bạn hoàn toàn chung chung với UIKit và được khởi tạo thông qua Nib, bạn có thể ghi đè các phương thức này từ bên trong UIViewController của mình không?
RLH

1
hitTest:withEvent:pointInside:withEvent:khu vực không được gọi cho tôi khi tôi nhấn vào một nút nằm ngoài giới hạn của phụ huynh. Điều gì có thể sai?
iosdude

24

@jnic, tôi đang làm việc trên iOS SDK 5.0 và để mã của bạn hoạt động tốt, tôi phải làm như sau:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(button.frame, point)) {
    return YES;
}
return [super pointInside:point withEvent:event]; }

Chế độ xem vùng chứa trong trường hợp của tôi là UIButton và tất cả các phần tử con cũng là UIButton có thể di chuyển ra ngoài giới hạn của UIButton mẹ.

Tốt


20

Trong chế độ xem gốc, bạn có thể ghi đè phương pháp thử nghiệm lần truy cập:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGPoint translatedPoint = [_myButton convertPoint:point fromView:self];

    if (CGRectContainsPoint(_myButton.bounds, translatedPoint)) {
        return [_myButton hitTest:translatedPoint withEvent:event];
    }
    return [super hitTest:point withEvent:event];

}

Trong trường hợp này, nếu điểm nằm trong giới hạn nút của bạn, bạn chuyển tiếp cuộc gọi đến đó; nếu không, hãy hoàn nguyên về triển khai ban đầu.


17

Trong trường hợp của tôi, tôi có một UICollectionViewCelllớp con chứa a UIButton. Tôi đã tắt clipsToBoundstrên ô và nút hiển thị bên ngoài giới hạn của ô. Tuy nhiên, nút này không nhận được các sự kiện chạm. Tôi có thể phát hiện các sự kiện chạm trên nút bằng cách sử dụng câu trả lời của Jack: https://stackoverflow.com/a/30431157/3344977

Đây là phiên bản Swift:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {

    let translatedPoint = button.convertPoint(point, fromView: self)

    if (CGRectContainsPoint(button.bounds, translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, withEvent: event)
    }
    return super.hitTest(point, withEvent: event)
}

Swift 4:

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

    let translatedPoint = button.convert(point, from: self)

    if (button.bounds.contains(translatedPoint)) {
        print("Your button was pressed")
        return button.hitTest(translatedPoint, with: event)
    }
    return super.hitTest(point, with: event)
}

Đó chính xác là kịch bản của tôi. Điểm khác là bạn nên đặt phương thức này bên trong Lớp CollectionViewCell.
Hamid Reza Ansari

Nhưng tại sao bạn lại đưa nút vào một phương thức bị ghi đè ???
rommex

10

Tại sao điều này lại xảy ra?

Điều này là do khi lượt xem phụ của bạn nằm ngoài giới hạn của lượt xem phụ, các sự kiện chạm thực sự xảy ra trên lượt xem phụ đó sẽ không được chuyển đến lượt xem phụ đó. Tuy nhiên, nó SẼ được chuyển đến superview của nó.

Bất kể lượt xem phụ có được cắt bớt một cách trực quan hay không, các sự kiện chạm luôn tuân theo hình chữ nhật giới hạn của chế độ xem mục tiêu. Nói cách khác, các sự kiện chạm xảy ra trong một phần của chế độ xem nằm bên ngoài hình chữ nhật giới hạn của chế độ xem siêu cao sẽ không được phân phối đến chế độ xem đó. Liên kết

Bạn cần gì để làm?

Khi superview của bạn nhận được sự kiện chạm được đề cập ở trên, bạn sẽ cần phải nói rõ ràng với UIKit rằng subview của tôi phải là người nhận được sự kiện chạm này.

Điều gì về mã?

Trong chế độ giám sát của bạn, hãy triển khai func hitTest(_ point: CGPoint, with event: UIEvent?)

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        if isHidden || alpha == 0 || clipsToBounds { return super.hitTest(point, with: event) }
        // convert the point into subview's coordinate system
        let subviewPoint = self.convert(point, to: subview)
        // if the converted point lies in subview's bound, tell UIKit that subview should be the one that receives this event
        if !subview.isHidden && subview.bounds.contains(subviewPoint) { return subview }
        return super.hitTest(point, with: event)
    }

Gotchya hấp dẫn: bạn phải đi đến "siêu chế độ siêu nhỏ cao nhất"

Bạn phải đi "lên" chế độ xem "cao nhất" mà chế độ xem vấn đề nằm bên ngoài.

Ví dụ điển hình:

Giả sử bạn có một màn hình S, với một khung nhìn vùng chứa C. Khung nhìn của bộ điều khiển khung nhìn vùng chứa là V. (Nhớ lại V sẽ nằm bên trong C và có cùng kích thước.) V có một khung nhìn phụ (có thể là một nút) B. B là vấn đề chế độ xem thực sự nằm ngoài V.

Nhưng lưu ý rằng B cũng nằm ngoài C.

Trong ví dụ này, bạn phải áp dụng các giải pháp override hitTesttrên thực tế đến C, chứ không phải để V . Nếu bạn áp dụng nó cho V - nó không có tác dụng gì.


4
Cảm ơn vì điều này! Tôi đã dành hàng giờ đồng hồ cho một vấn đề bắt nguồn từ việc này. Tôi thực sự đánh giá cao việc bạn dành thời gian để đăng bài này. :)
ClayJ

@ScottZhu: Tôi đã thêm một gotchya quan trọng vào câu trả lời của bạn. Tất nhiên, bạn có thể thoải mái xóa hoặc thay đổi phần bổ sung của tôi! Cảm ơn một lần nữa!
Fattie

9

Nhanh:

Trong đó targetView là dạng xem bạn muốn nhận sự kiện (có thể nằm toàn bộ hoặc một phần bên ngoài khung của dạng xem mẹ của nó).

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        let pointForTargetView: CGPoint = targetView.convertPoint(point, fromView: self)
        if CGRectContainsPoint(targetView.bounds, pointForTargetView) {
            return closeButton
        }
        return super.hitTest(point, withEvent: event)
}

5

Đối với tôi (cũng như những người khác), câu trả lời không phải là ghi đè hitTestphương thức, mà là pointInsidephương thức. Tôi chỉ phải làm điều này ở hai nơi.

Các SuperView Đây là quan điểm cho rằng nếu nó đã clipsToBoundsthiết lập là true, nó sẽ làm cho toàn bộ vấn đề này biến mất. Vì vậy, đây là đơn giản của tôi UIViewtrong Swift 3:

class PromiscuousView : UIView {
    override func point(inside point: CGPoint,
               with event: UIEvent?) -> Bool {
        return true
    }
} 

Lượt xem phụ Số dặm của bạn có thể khác nhau, nhưng trong trường hợp của tôi, tôi cũng phải ghi đè pointInsidephương pháp cho lượt xem phụ đã được quyết định là lần chạm ngoài giới hạn. Của tôi nằm trong Objective-C, vì vậy:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    WEPopoverController *controller = (id) self.delegate;
    if (self.takeHitsOutside) {
        return YES;
    } else {
        return [super pointInside:point withEvent:event];
    }
}

Câu trả lời này (và một số câu trả lời khác cho cùng câu hỏi) có thể giúp bạn hiểu rõ hơn.


2

nhanh chóng 4.1 được cập nhật

Để nhận các sự kiện liên lạc trong cùng một UIView, bạn chỉ cần thêm phương thức ghi đè sau, nó sẽ xác định chế độ xem cụ thể được nhấn trong UIView được đặt ngoài giới hạn

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let pointforTargetview:CGPoint = self.convert(point, to: btnAngle)
    if  btnAngle.bounds.contains(pointforTargetview) {
        return  self
    }
    return super.hitTest(point, with: event)
}
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.