การจัดการเหตุการณ์สำหรับ iOS - hitTest: withEvent: และ pointInside: withEvent: เกี่ยวข้องอย่างไร


145

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

เอกสารกล่าวว่า

ในการทดสอบ Hit, หน้าต่างจะเรียกhitTest:withEvent:ใช้มุมมองบนสุดของลำดับชั้นมุมมอง วิธีนี้ดำเนินการโดยเรียกซ้ำpointInside:withEvent:ในแต่ละมุมมองในลำดับชั้นมุมมองที่ส่งกลับใช่ดำเนินการตามลำดับชั้นจนกว่าจะพบมุมมองย่อยที่มีขอบเขตสัมผัสที่เกิดขึ้น มุมมองนั้นกลายเป็นมุมมองการทดสอบ Hit

เป็นเช่นนั้นหรือไม่เฉพาะhitTest:withEvent:มุมมองบนสุดเท่านั้นที่ถูกเรียกโดยระบบซึ่งเรียกใช้pointInside:withEvent:การดูย่อยทั้งหมดและหากการส่งคืนจากมุมมองย่อยเฉพาะคือใช่แล้วก็เรียกใช้pointInside:withEvent:คลาสย่อยของมุมมองย่อยนั้นได้หรือไม่


3
บทแนะนำที่ดีมากซึ่งช่วยฉันในการเชื่อมโยง
anneblue

เอกสารใหม่ที่เทียบเท่าสำหรับในตอนนี้อาจจะdeveloper.apple.com/documentation/uikit/uiview/1622469-hittest
Cœur

คำตอบ:


173

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

การติดตั้งhitTest:withEvent:ใน UIResponder ทำดังต่อไปนี้:

  • มันเรียกร้องpointInside:withEvent:ของself
  • ถ้ากลับมาคือไม่ผลตอบแทนhitTest:withEvent: nilจุดสิ้นสุดของเรื่องราว
  • หากการส่งคืนคือใช่มันจะส่งhitTest:withEvent:ข้อความไปยังผู้ชมย่อย มันเริ่มจากมุมมองย่อยระดับบนสุดและยังคงอยู่ในมุมมองอื่น ๆ จนกว่ามุมมองย่อยจะส่งคืนnilออบเจ็กต์ที่ไม่ใช่วัตถุ
  • หากมุมมองย่อยส่งกลับnilวัตถุที่ไม่ใช่ในครั้งแรกครั้งแรกhitTest:withEvent:ส่งกลับวัตถุนั้น จุดสิ้นสุดของเรื่องราว
  • ถ้าไม่มีการดูย่อยส่งคืนnilวัตถุที่ไม่hitTest:withEvent:ส่งคืนแรกself

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

อย่างไรก็ตามคุณอาจแทนที่hitTest:withEventการทำสิ่งที่แตกต่าง ในหลายกรณีการเอาชนะpointInside:withEvent:นั้นง่ายกว่าและยังมีตัวเลือกเพียงพอที่จะปรับแต่งการจัดการเหตุการณ์ในแอปพลิเคชันของคุณ


คุณหมายถึงhitTest:withEvent:การสัมภาษณ์ทั้งหมดจะถูกดำเนินการในที่สุดหรือไม่?
realstuff02

2
ใช่. เพียงแทนที่hitTest:withEvent:ในมุมมองของคุณ (และpointInsideถ้าคุณต้องการ) พิมพ์บันทึกและการโทร[super hitTest...เพื่อค้นหาว่าใครhitTest:withEvent:ถูกเรียกตามลำดับใด
MHC

ไม่ควรขั้นตอนที่ 3 ที่คุณพูดถึง "ถ้าผลตอบแทนเป็น YES มันจะส่ง hitTest: withEvent: ... ไม่ควรจะเป็น pointInside: withEvent? ฉันคิดว่ามันส่ง pointInside ไปยังการสัมภาษณ์ทั้งหมดหรือไม่
prostock

ย้อนกลับไปในเดือนกุมภาพันธ์มันส่ง hitTest ครั้งแรก: withEvent: ซึ่งเป็น pointInside: withEvent: ถูกส่งไปที่ตัวเอง ฉันไม่ได้ตรวจสอบพฤติกรรมนี้อีกครั้งด้วย SDK เวอร์ชันต่อไปนี้ แต่ฉันคิดว่าการส่ง hitTest: withEvent: สมเหตุสมผลมากกว่าเนื่องจากมีการควบคุมระดับสูงว่ากิจกรรมเป็นของมุมมองหรือไม่ pointInside: withEvent: บอกว่าตำแหน่งของเหตุการณ์อยู่ในมุมมองหรือไม่ไม่ใช่ว่าเป็นของเหตุการณ์หรือไม่ ตัวอย่างเช่นมุมมองย่อยอาจไม่ต้องการจัดการเหตุการณ์แม้ว่าตำแหน่งของมันจะอยู่บนมุมมองย่อย
MHC

1
WWDC2014 ช่วงเวลา 235 - ขั้นสูงการเลื่อนดูภาพและเทคนิคการจัดการแบบสัมผัสให้คำอธิบายและตัวอย่างที่ดีสำหรับปัญหานี้
antonio081014

297

ฉันคิดว่าคุณสับสน subclassing กับลำดับชั้นการดู สิ่งที่หมอพูดมีดังนี้ สมมติว่าคุณมีลำดับชั้นมุมมองนี้ ตามลำดับชั้นฉันไม่ได้พูดถึงลำดับชั้นของชั้นเรียน แต่มีมุมมองภายในลำดับชั้นของมุมมองดังนี้:

+----------------------------+
|A                           |
|+--------+   +------------+ |
||B       |   |C           | |
||        |   |+----------+| |
|+--------+   ||D         || |
|             |+----------+| |
|             +------------+ |
+----------------------------+

Dสมมติว่าคุณใส่ในนิ้วของคุณ นี่คือสิ่งที่จะเกิดขึ้น:

  1. hitTest:withEvent:ถูกเรียกAใช้มุมมองบนสุดของลำดับชั้นมุมมอง
  2. pointInside:withEvent: ถูกเรียกซ้ำในแต่ละมุมมอง
    1. pointInside:withEvent:ถูกเรียกใช้Aและส่งคืนYES
    2. pointInside:withEvent:ถูกเรียกใช้Bและส่งคืนNO
    3. pointInside:withEvent:ถูกเรียกใช้Cและส่งคืนYES
    4. pointInside:withEvent:ถูกเรียกใช้Dและส่งคืนYES
  3. ในมุมมองที่ส่งคืนYESจะมองลงไปที่ลำดับชั้นเพื่อดูมุมมองย่อยที่เกิดการสัมผัส ในกรณีนี้จากA, Cและมันจะเป็นDD
  4. D จะเป็นมุมมองการทดสอบการตี

ขอบคุณสำหรับคำตอบ สิ่งที่คุณอธิบายคือสิ่งที่อยู่ในใจของฉัน แต่ @MHC พูดhitTest:withEvent:ถึง B, C และ D ก็ถูกเรียกเช่นกัน จะเกิดอะไรขึ้นถ้า D เป็นมุมมองย่อยของ C ไม่ใช่ A ฉันคิดว่าฉันสับสน ...
realstuff02

2
ในภาพวาดของฉัน D เป็นมุมมองย่อยของ C.
pgb

1
จะไม่AกลับมาYESเช่นกันเช่นเดียวกับที่CและDไม่?
Martin Wickman

2
อย่าลืมว่ามุมมองที่มองไม่เห็น (โดยซ่อน. หรือความทึบต่ำกว่า 0.1) หรือการปิดการโต้ตอบผู้ใช้จะไม่ตอบสนองต่อ HitTest ฉันไม่คิดว่า HitTest จะถูกเรียกบนวัตถุเหล่านี้ตั้งแต่แรก
Jonny

เพียงแค่ต้องการเพิ่ม hitTest นั้น: withEvent: อาจถูกเรียกใช้ในมุมมองทั้งหมดขึ้นอยู่กับลำดับชั้น
Adithya

47

ฉันพบว่าการทดสอบ Hit ใน iOSเป็นประโยชน์อย่างมาก

ป้อนคำอธิบายรูปภาพที่นี่

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
    }
    return nil;
}

แก้ไข Swift 4:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    if self.point(inside: point, with: event) {
        return super.hitTest(point, with: event)
    }
    guard isUserInteractionEnabled, !isHidden, alpha > 0 else {
        return nil
    }

    for subview in subviews.reversed() {
        let convertedPoint = subview.convert(point, from: self)
        if let hitView = subview.hitTest(convertedPoint, with: event) {
            return hitView
        }
    }
    return nil
}

ดังนั้นคุณต้องเพิ่มสิ่งนี้ในคลาสย่อยของ UIView และมีมุมมองทั้งหมดในลำดับชั้นของคุณที่สืบทอดมาหรือไม่
Guig

21

ขอบคุณสำหรับคำตอบพวกเขาช่วยฉันแก้สถานการณ์ด้วยมุมมอง "ซ้อนทับ"

+----------------------------+
|A +--------+                |
|  |B  +------------------+  |
|  |   |C            X    |  |
|  |   +------------------+  |
|  |        |                |
|  +--------+                | 
|                            |
+----------------------------+

สมมติX- การสัมผัสของผู้ใช้ pointInside:withEvent:เกี่ยวกับBผลตอบแทนNOดังนั้นผลตอบแทนhitTest:withEvent: Aฉันเขียนหมวดหมู่UIViewเพื่อจัดการกับปัญหาเมื่อคุณจำเป็นต้องได้รับการสัมผัสจากมุมมองด้านบนที่มองเห็นได้มากที่สุด

- (UIView *)overlapHitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 1
    if (!self.userInteractionEnabled || [self isHidden] || self.alpha == 0)
        return nil;

    // 2
    UIView *hitView = self;
    if (![self pointInside:point withEvent:event]) {
        if (self.clipsToBounds) return nil;
        else hitView = nil;
    }

    // 3
    for (UIView *subview in [self.subviewsreverseObjectEnumerator]) {
        CGPoint insideSubview = [self convertPoint:point toView:subview];
        UIView *sview = [subview overlapHitTest:insideSubview withEvent:event];
        if (sview) return sview;
    }

    // 4
    return hitView;
}
  1. เราไม่ควรส่งกิจกรรมแบบสัมผัสสำหรับมุมมองที่ซ่อนอยู่หรือมุมมองที่โปร่งใสหรือมุมมองพร้อมการuserInteractionEnabledตั้งค่าเป็นNO;
  2. หากสัมผัสอยู่ภายในself, selfจะได้รับการพิจารณาเป็นผลที่อาจเกิดขึ้น
  3. ตรวจสอบการดูย่อยทั้งหมดซ้ำเพื่อให้ได้ Hit หากมีให้ส่งคืน
  4. อื่นคืนตัวเองหรือศูนย์ขึ้นอยู่กับผลลัพธ์จากขั้นตอนที่ 2

หมายเหตุ[self.subviewsreverseObjectEnumerator]จำเป็นต้องทำตามลำดับชั้นการดูจากบนลงล่างสุด และตรวจสอบclipsToBoundsเพื่อไม่ให้ทดสอบหน้าย่อยที่ถูกหลอกลวง

การใช้งาน:

  1. นำเข้าหมวดหมู่ในมุมมองคลาสย่อยของคุณ
  2. แทนที่hitTest:withEvent:ด้วยสิ่งนี้
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

คู่มืออย่างเป็นทางการของ Appleให้ภาพประกอบที่ดีด้วย

หวังว่านี่จะช่วยใครซักคน


! ที่น่าตื่นตาตื่นใจ ขอบคุณสำหรับตรรกะที่ชัดเจนและข้อมูลโค้ดที่ยอดเยี่ยมได้แก้ไข head-scratcher ของฉัน!
Thompson

@ สิงโตคำตอบที่ดี นอกจากนี้คุณสามารถตรวจสอบความเท่าเทียมกันเพื่อล้างสีในขั้นตอนแรก
aquarium_moose

3

มันแสดงให้เห็นถึงตัวอย่างนี้!

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    if (self.hidden || !self.userInteractionEnabled || self.alpha < 0.01)
    {
        return nil;
    }

    if (![self pointInside:point withEvent:event])
    {
        return nil;
    }

    __block UIView *hitView = self;

    [self.subViews enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {   

        CGPoint thePoint = [self convertPoint:point toView:obj];

        UIView *theSubHitView = [obj hitTest:thePoint withEvent:event];

        if (theSubHitView != nil)
        {
            hitView = theSubHitView;

            *stop = YES;
        }

    }];

    return hitView;
}

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

@ DouglasHill ขอบคุณการแก้ไขของคุณ ขอแสดงความนับถือ
ฮิปโป

1

ตัวอย่างของ @lion ทำงานเหมือนมีเสน่ห์ ฉันบอกว่ามันรวดเร็ว 2.1 และใช้มันเป็นส่วนเสริมของ UIView ฉันโพสต์ไว้ที่นี่ในกรณีที่มีคนต้องการมัน

extension UIView {
    func overlapHitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // 1
        if !self.userInteractionEnabled || self.hidden || self.alpha == 0 {
            return nil
        }
        //2
        var hitView: UIView? = self
        if !self.pointInside(point, withEvent: event) {
            if self.clipsToBounds {
                return nil
            } else {
                hitView = nil
            }
        }
        //3
        for subview in self.subviews.reverse() {
            let insideSubview = self.convertPoint(point, toView: subview)
            if let sview = subview.overlapHitTest(insideSubview, withEvent: event) {
                return sview
            }
        }
        return hitView
    }
}

หากต้องการใช้เพียงแค่แทนที่ hitTest: point: withEvent ใน uiview ของคุณดังต่อไปนี้:

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let uiview = super.hitTest(point, withEvent: event)
    print("hittest",uiview)
    return overlapHitTest(point, withEvent: event)
}

0

แผนภาพคลาส

การทดสอบยอดฮิต

ค้นหา First Responder

First Responderในกรณีนี้คือUIView point()วิธีการที่ลึกที่สุดซึ่งส่งกลับจริง

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

hitTest()ดูเหมือนว่าภายใน

hitTest() {

    if (isUserInteractionEnabled == false || isHidden == true || alpha == 0 || point() == false) { return nil }

    for subview in subviews {
        if subview.hitTest() != nil {
            return subview
        }
    }

    return nil

}

ส่งกิจกรรม Touch ไปที่ First Responder

//UIApplication.shared.sendEvent()

//UIApplication, UIWindow
func sendEvent(_ event: UIEvent)

//UIResponder
func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

ลองมาดูตัวอย่าง

เครือข่ายตอบกลับ

//UIApplication.shared.sendAction()
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool

ลองดูตัวอย่าง

class AppDelegate: UIResponder, UIApplicationDelegate {
    @objc
    func foo() {
        //this method is called using Responder Chain
        print("foo") //foo
    }
}

class ViewController: UIViewController {
    func send() {
        UIApplication.shared.sendAction(#selector(AppDelegate.foo), to: nil, from: view1, for: nil)
    }
}

[Android บนทัช]

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