ปฏิสัมพันธ์เหนือขอบเขตของ UIView


94

เป็นไปได้ไหมที่ UIButton (หรือส่วนควบคุมอื่น ๆ สำหรับเรื่องนั้น) จะรับเหตุการณ์การสัมผัสเมื่อเฟรมของ UIButton อยู่นอกเฟรมของผู้ปกครอง ทำให้เมื่อฉันลองสิ่งนี้ UIButton ของฉันดูเหมือนจะไม่สามารถรับเหตุการณ์ใด ๆ ได้ ฉันจะแก้ไขปัญหานี้ได้อย่างไร


1
มันทำงานได้ดีสำหรับฉัน developer.apple.com/library/ios/qa/qa2013/qa1812.html Best
Frank Li

2
สิ่งนี้เป็นสิ่งที่ดีและเกี่ยวข้อง (หรืออาจเป็นการหลอกลวง): stackoverflow.com/a/31420820/8047
Dan Rosenstark

คำตอบ:


94

ใช่. คุณสามารถแทนที่hitTest:withEvent:วิธีการคืนค่ามุมมองสำหรับชุดของจุดที่ใหญ่กว่าที่มุมมองนั้นมีได้ ดูUIView ชั้นอ้างอิง

แก้ไข:ตัวอย่าง:

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

แก้ไข 2: (หลังจากการชี้แจง :) เพื่อให้แน่ใจว่าปุ่มได้รับการปฏิบัติว่าอยู่ในขอบเขตของผู้ปกครองคุณต้องแทนที่pointInside:withEvent:ในพาเรนต์เพื่อรวมกรอบของปุ่ม

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

โปรดสังเกตว่ารหัสตรงนั้นสำหรับการแทนที่ pointInside นั้นไม่ถูกต้องนัก ตามที่ Summon อธิบายไว้ด้านล่างให้ทำสิ่งนี้:

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

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

โปรดทราบว่าคุณมีแนวโน้มที่จะใช้self.oversizeButtonเป็น IBOutlet ในคลาสย่อย UIView นี้ จากนั้นคุณสามารถลาก "ปุ่มขนาดใหญ่" ที่ต้องการไปยังมุมมองพิเศษที่เป็นปัญหาได้ (หรือหากคุณทำสิ่งนี้เป็นจำนวนมากในโครงการด้วยเหตุผลบางประการคุณจะมีคลาสย่อย UIButton พิเศษและคุณสามารถดูรายการมุมมองย่อยของชั้นเรียนเหล่านั้นได้) หวังว่าจะช่วยได้


คุณสามารถช่วยฉันเพิ่มเติมเกี่ยวกับการใช้โค้ดตัวอย่างได้อย่างถูกต้องหรือไม่ นอกจากนี้ฉันอ่านข้อมูลอ้างอิงและเนื่องจากบรรทัดต่อไปนี้ฉันจึงสับสน: "จุดที่อยู่นอกขอบเขตของผู้รับจะไม่ถูกรายงานว่าเป็น Hit แม้ว่าจริงๆแล้วจะอยู่ในมุมมองย่อยของผู้รับก็ตามการดูย่อยอาจขยายเกินขอบเขตด้วยสายตา ของพาเรนต์หากคุณสมบัติ clipsToBounds ของมุมมองพาเรนต์ถูกตั้งค่าเป็น NO อย่างไรก็ตามการทดสอบ Hit จะไม่สนใจจุดที่อยู่นอกขอบเขตของมุมมองพาเรนต์เสมอ "
ThomasM

โดยเฉพาะส่วนนี้ที่ทำให้ดูเหมือนว่านี่ไม่ใช่วิธีแก้ปัญหาที่ฉันต้องการ: "อย่างไรก็ตามการทดสอบการเข้าชมมักจะละเลยจุดที่อยู่นอกขอบเขตของมุมมองของผู้ปกครอง" .. แต่ฉันอาจคิดผิดแน่นอนฉันพบว่าการอ้างอิงของ Apple บางครั้งก็สับสนจริงๆ ..
ThomasM

อ่าฉันเข้าใจแล้ว คุณต้องลบล้างพาเรนต์pointInside:withEvent:เพื่อรวมเฟรมของปุ่ม ฉันจะเพิ่มโค้ดตัวอย่างในคำตอบด้านบน
jnic

มีวิธีดำเนินการโดยไม่ลบล้างวิธีการของ UIView หรือไม่? ตัวอย่างเช่นหากมุมมองของคุณเป็นแบบทั่วไปสำหรับ UIKit และเริ่มต้นผ่าน Nib คุณสามารถแทนที่วิธีการเหล่านี้จากภายใน UIViewController ของคุณได้หรือไม่
RLH

1
hitTest:withEvent:และpointInside:withEvent:พื้นที่ที่ไม่เรียกหาฉันเมื่อฉันกดปุ่มที่อยู่นอกขอบเขตของผู้ปกครอง มีอะไรผิดพลาดได้บ้าง?
iosdude

24

@jnic ฉันกำลังทำงานกับ iOS SDK 5.0 และเพื่อให้โค้ดของคุณทำงานได้อย่างถูกต้องฉันต้องทำสิ่งนี้:

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

มุมมองคอนเทนเนอร์ในกรณีของฉันคือ UIButton และองค์ประกอบลูกทั้งหมดยังเป็น UIButtons ที่สามารถย้ายออกนอกขอบเขตของ UIButton หลัก

ดีที่สุด


20

ในมุมมองระดับบนสุดคุณสามารถแทนที่วิธีการทดสอบ Hit:

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

}

ในกรณีนี้หากจุดอยู่ในขอบเขตของปุ่มคุณจะโอนสายไปที่นั่น หากไม่เป็นเช่นนั้นให้เปลี่ยนกลับไปใช้การใช้งานเดิม


18

ในกรณีของฉันฉันมีUICollectionViewCellคลาสย่อยที่มีไฟล์UIButton. ฉันปิดการใช้งานclipsToBoundsบนเซลล์และปุ่มนี้มองเห็นได้นอกขอบเขตของเซลล์ อย่างไรก็ตามปุ่มไม่ได้รับเหตุการณ์การสัมผัส ฉันสามารถตรวจจับเหตุการณ์การสัมผัสบนปุ่มได้โดยใช้คำตอบของแจ็ค: https://stackoverflow.com/a/30431157/3344977

นี่คือเวอร์ชัน 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)
}

นั่นคือสถานการณ์ของฉัน อีกประเด็นคือคุณควรใส่วิธีนี้ในคลาส CollectionViewCell
Hamid Reza Ansari

แต่คุณจะฉีดปุ่มไปที่วิธีการลบล้างทำไม ???
rommex

10

เหตุใดจึงเกิดขึ้น

เนื่องจากเมื่อมุมมองย่อยของคุณอยู่นอกขอบเขตของซูเปอร์วิวของคุณเหตุการณ์การสัมผัสที่เกิดขึ้นจริงในมุมมองย่อยนั้นจะไม่ถูกส่งไปยังมุมมองย่อยนั้น อย่างไรก็ตามจะส่งไปยัง superview

ไม่ว่าจะมีการตัดมุมมองย่อยด้วยภาพหรือไม่ก็ตามเหตุการณ์การสัมผัสจะเคารพกรอบสี่เหลี่ยมของมุมมองยอดเยี่ยมของมุมมองเป้าหมายเสมอ กล่าวอีกนัยหนึ่งคือเหตุการณ์การสัมผัสที่เกิดขึ้นในส่วนหนึ่งของมุมมองที่อยู่นอกกรอบสี่เหลี่ยมขอบเขตของ superview จะไม่ถูกส่งไปยังมุมมองนั้น ลิงค์

สิ่งที่คุณต้องทำ?

เมื่อซูเปอร์วิวของคุณได้รับเหตุการณ์การสัมผัสที่กล่าวถึงข้างต้นคุณจะต้องบอก UIKit อย่างชัดเจนว่ามุมมองย่อยของฉันควรเป็นผู้ที่จะได้รับเหตุการณ์สัมผัสนี้

แล้วโค้ดล่ะ?

ใน superview ของคุณใช้ 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 ที่น่าสนใจ: คุณต้องไปที่ "superview ที่เล็กเกินไป"

คุณต้องไป "ขึ้น" ไปที่มุมมอง "สูงสุด" ซึ่งมุมมองปัญหาอยู่ภายนอก

ตัวอย่างทั่วไป:

สมมติว่าคุณมีหน้าจอ S พร้อมมุมมองคอนเทนเนอร์ C. มุมมองของคอนเทนเนอร์วิวคอนโทรลเลอร์คือ V. (เรียกคืน V จะอยู่ด้านในของ C และมีขนาดเท่ากัน) V มีมุมมองย่อย (อาจเป็นปุ่ม) B B คือปัญหา มุมมองที่อยู่นอก V.

แต่สังเกตว่า B ก็อยู่นอก C เช่นกัน

ในตัวอย่างนี้คุณต้องใช้วิธีการแก้ปัญหาoverride hitTestในความเป็นจริงที่ C ไม่ V หากคุณใช้กับ V - จะไม่ทำอะไรเลย


4
ขอบคุณสำหรับสิ่งนี้! ฉันใช้เวลาหลายชั่วโมงกับปัญหาที่เกิดจากสิ่งนี้ ขอขอบคุณที่สละเวลาโพสต์สิ่งนี้ :)
ClayJ

@ScottZhu: ฉันเพิ่ม gotchya ที่สำคัญในคำตอบของคุณ แน่นอนคุณควรอย่าลังเลที่จะลบหรือเปลี่ยนแปลงการเพิ่มของฉัน! ขอบคุณอีกครั้ง!
Fattie

9

รวดเร็ว:

โดยที่ targetView คือมุมมองที่คุณต้องการรับเหตุการณ์ (ซึ่งอาจอยู่นอกกรอบของมุมมองระดับบนสุดทั้งหมดหรือบางส่วน)

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

สำหรับฉัน (สำหรับคนอื่น ๆ ) คำตอบคือไม่ได้แทนที่hitTestวิธีการ แต่เป็นpointInsideวิธีการ ฉันต้องทำสิ่งนี้เพียงสองแห่ง

Superview นี่คือมุมมองที่หากclipsToBoundsตั้งค่าเป็นจริงจะทำให้ปัญหานี้หมดไป นี่เป็นวิธีง่ายๆของฉันUIViewใน Swift 3:

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

Subview ระยะทางของคุณอาจแตกต่างกันไป แต่ในกรณีของฉันฉันต้องเขียนทับpointInsideวิธีการสำหรับมุมมองย่อยที่ได้ตัดสินใจว่าการสัมผัสนั้นอยู่นอกขอบเขต เหมืองอยู่ใน Objective-C ดังนั้น:

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

คำตอบนี้ (และคำถามอื่น ๆ อีกสองสามข้อ) สามารถช่วยให้คุณเข้าใจชัดเจนขึ้น


2

อัปเดต swift 4.1 แล้ว

หากต้องการรับเหตุการณ์การสัมผัสใน UIView เดียวกันคุณเพียงแค่เพิ่มวิธีการลบล้างต่อไปนี้มันจะระบุมุมมองเฉพาะที่แตะใน UIView ซึ่งอยู่นอกขอบเขต

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)
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.