การจับภาพบนมุมมองย่อยนอกกรอบของซูเปอร์วิวโดยใช้ hitTest: withEvent:


94

ปัญหาของฉัน:ฉันได้ SuperView EditViewที่เกิดขึ้นโดยทั่วไปกรอบโปรแกรมทั้งหมดและ subview MenuViewซึ่งใช้เวลาเพียงด้านล่าง ~ 20% แล้วMenuViewมี subview ของตัวเองButtonViewซึ่งอันที่จริงอาศัยอยู่นอกMenuView's ขอบเขต (บางอย่างเช่นนี้ButtonView.frame.origin.y = -100)

(หมายเหตุ: EditViewมีMenuViewมุมมองย่อยอื่น ๆ ที่ไม่ได้เป็นส่วนหนึ่งของลำดับชั้นการดู แต่อาจส่งผลต่อคำตอบ)

คุณอาจทราบปัญหาแล้ว: เมื่อButtonViewใดที่อยู่ในขอบเขตของMenuView(หรือโดยเฉพาะอย่างยิ่งเมื่อการสัมผัสของฉันอยู่ในMenuViewขอบเขต) ButtonViewตอบสนองต่อเหตุการณ์การสัมผัส เมื่อสัมผัสของฉันอยู่นอกMenuViewขอบเขตของ (แต่ยังอยู่ในButtonViewขอบเขตของ) จะไม่ได้รับเหตุการณ์สัมผัสใดButtonView

ตัวอย่าง:

  • (E) คือพาเรนEditViewต์ของมุมมองทั้งหมด
  • (M) คือMenuViewมุมมองย่อยของ EditView
  • (B) คือButtonViewมุมมองย่อยของ MenuView

แผนภาพ:

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

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

เป้าหมาย:ฉันรวบรวมว่าการลบล้างhitTest:withEvent:สามารถแก้ปัญหานี้ได้ แต่ฉันไม่เข้าใจว่าจะทำอย่างไร ในกรณีของฉันควรhitTest:withEvent:จะแทนที่ในEditView(superview 'master' ของฉัน) หรือไม่ หรือควรจะลบล้างในMenuViewsuperview โดยตรงของปุ่มที่ไม่ได้รับการสัมผัส? หรือฉันคิดเรื่องนี้ไม่ถูกต้อง?

หากต้องการคำอธิบายที่ยาวแหล่งข้อมูลออนไลน์ที่ดีจะเป็นประโยชน์ยกเว้นเอกสาร UIView ของ Apple ซึ่งยังไม่ชัดเจนสำหรับฉัน

ขอบคุณ!

คำตอบ:


147

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

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

ฉันหวังว่านี่จะช่วยทุกคนที่พยายามใช้โซลูชันนี้สำหรับกรณีการใช้งานที่ซับซ้อนมากขึ้น


สิ่งนี้ดูดีและขอบคุณที่ทำ อย่างไรก็ตามมีคำถามหนึ่งข้อ: ทำไมคุณถึงกลับ [super hitTest: point withEvent: event]; เหรอ? คุณจะไม่คืนค่าศูนย์ถ้าไม่มีมุมมองย่อยที่ทำให้เกิดการสัมผัสหรือไม่? Apple กล่าวว่า hitTest จะคืนค่าศูนย์หากไม่มีมุมมองย่อยที่มีการสัมผัส
Ser Pounce

หืม ... เออฟังดูแล้วใช่เลย นอกจากนี้สำหรับพฤติกรรมที่ถูกต้องจำเป็นต้องทำซ้ำวัตถุในลำดับย้อนกลับ (เนื่องจากวัตถุสุดท้ายเป็นวัตถุที่อยู่บนสุดที่มองเห็นได้) แก้ไขรหัสให้ตรงกัน
Noam

3
ฉันเพิ่งใช้โซลูชันของคุณเพื่อสร้าง UIButton จับการสัมผัสและมันอยู่ใน UIView ที่อยู่ภายใน UICollectionViewCell ภายใน UICollectionView (ชัดเจน) ฉันต้อง subclass UICollectionView และ UICollectionViewCell เพื่อแทนที่ hitTest: withEvent: ในสามคลาสนี้ และใช้ได้อย่างมีเสน่ห์ !! ขอบคุณ !!
Daniel García

3
ขึ้นอยู่กับการใช้งานที่ต้องการควรคืนค่า [super hitTest: point withEvent: event] หรือ nil การกลับตัวเองย่อมทำให้ได้รับทุกสิ่ง
Noam

1
นี่คือเอกสารทางเทคนิคถาม & ตอบจาก Apple เกี่ยวกับเทคนิคเดียวกันนี้: developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang

33

ตกลงฉันทำการขุดและทดสอบแล้วนี่คือวิธีการhitTest:withEventทำงาน - อย่างน้อยก็ในระดับสูง สร้างภาพสถานการณ์นี้:

  • (E) คือ EditViewซึ่งเป็นพาเรนต์ของมุมมองทั้งหมด
  • (M) คือ MenuViewซึ่งเป็นมุมมองย่อยของ EditView
  • (B) คือ ButtonViewซึ่งเป็นมุมมองย่อยของ MenuView

แผนภาพ:

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

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

อย่างไรก็ตามหากคุณใช้งานhitTest:withEvent:ใน (M) การแตะที่ใดก็ได้ในแอปพลิเคชันจะถูกส่งไปที่ (M) (หรืออย่างน้อยที่สุดก็จะรู้เกี่ยวกับพวกเขา) คุณสามารถเขียนโค้ดเพื่อจัดการการสัมผัสในกรณีนั้นและส่งคืนวัตถุที่ควรได้รับการสัมผัส

โดยเฉพาะอย่างยิ่ง: เป้าหมายhitTest:withEvent:คือการส่งคืนวัตถุที่ควรได้รับการตี ดังนั้นใน (M) คุณอาจเขียนโค้ดดังนี้:

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

ฉันยังใหม่มากสำหรับวิธีนี้และปัญหานี้ดังนั้นหากมีวิธีการเขียนโค้ดที่มีประสิทธิภาพหรือถูกต้องมากกว่านี้โปรดแสดงความคิดเห็น

ฉันหวังว่าจะช่วยใครก็ตามที่ตอบคำถามนี้ในภายหลัง :)


ขอบคุณสิ่งนี้ใช้ได้ผลสำหรับฉันแม้ว่าฉันจะต้องใช้ตรรกะเพิ่มเติมเพื่อพิจารณาว่ามุมมองย่อยใดที่ควรได้รับความนิยมจริงๆ ฉันลบช่วงพักที่ไม่จำเป็นออกจากตัวอย่างของคุณ btw
Daniel Saidi

เมื่อเฟรมมุมมองย่อยเป็นกรอบของมุมมองหลักเหตุการณ์การคลิกไม่ตอบสนอง! โซลูชันของคุณแก้ไขปัญหานี้ ขอบคุณมาก :-)
Jeevan

@toblerpwn (ชื่อเล่นตลก ๆ :)) คุณควรแก้ไขคำตอบเก่า ๆ ที่ยอดเยี่ยมนี้เพื่อให้ชัดเจนมาก (ใช้ฝาพับขนาดใหญ่) ในคลาสที่คุณต้องเพิ่มสิ่งนี้ ไชโย!
Fattie

27

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

สิ่งนี้จะใช้ได้ผลก็ต่อเมื่อคุณใช้การลบล้างในมุมมองที่
เหนือกว่า

2

สิ่งที่ฉันจะทำคือให้ทั้ง ButtonView และ MenuView อยู่ในระดับเดียวกันในลำดับชั้นของมุมมองโดยวางทั้งสองอย่างไว้ในคอนเทนเนอร์ที่เฟรมพอดีกับทั้งสองอย่างสมบูรณ์ วิธีนี้จะไม่เพิกเฉยต่อพื้นที่โต้ตอบของรายการที่ถูกตัดเนื่องจากเป็นขอบเขตของ superview


ฉันคิดเกี่ยวกับวิธีแก้ปัญหานี้เช่นกัน - หมายความว่าฉันจะต้องทำซ้ำตรรกะตำแหน่งบางอย่าง (หรือ refactor รหัสที่ร้ายแรงบางอย่าง!) แต่มันอาจเป็นทางเลือกที่ดีที่สุดของฉันในตอนท้าย ..
toblerpwn

1

หากคุณมีมุมมองย่อยอื่น ๆ อีกมากมายในมุมมองหลักของคุณมุมมองแบบโต้ตอบอื่น ๆ ส่วนใหญ่อาจไม่ทำงานหากคุณใช้โซลูชันข้างต้นในกรณีนี้คุณสามารถใช้สิ่งนี้ได้ (ใน 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

หากใครต้องการนี่คือทางเลือกที่รวดเร็ว

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

วางบรรทัดด้านล่างของโค้ดลงในลำดับชั้นมุมมองของคุณ:

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

สำหรับคำชี้แจงเพิ่มเติมเราได้อธิบายไว้ในบล็อกของฉัน: "goaheadwithiphonetech" เกี่ยวกับ "ข้อความเสริมที่กำหนดเอง: ปุ่มไม่สามารถคลิกได้"

หวังว่าจะช่วยคุณได้ ... !!!


บล็อกของคุณถูกลบแล้วเราจะหาคำอธิบายได้ที่ไหน
ishahak
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.