iOSのイベント処理-hitTest:withEvent:とpointInside:withEvent:はどのように関連していますか?


145

ほとんどのアップルドキュメントは非常によく書かれていますが、「iOS向けイベント処理ガイド」は例外だと思います。そこに書かれていることをはっきりと理解するのは難しいです。

文書によれば、

ヒットテストでは、ウィンドウがhitTest:withEvent:ビュー階層の最上位のビューを呼び出します。このメソッドは、pointInside:withEvent:YESを返すビュー階層の各ビューを再帰的に呼び出し、タッチが行われた境界内のサブビューが見つかるまで階層を下っていきます。そのビューがヒットテストビューになります。

それではhitTest:withEvent:、最上位のビューのみがシステムによって呼び出され、pointInside:withEvent:すべてのサブビューが呼び出され、特定のサブビューからの戻り値がYESの場合pointInside:withEvent:、そのサブビューのサブクラスが呼び出されるのでしょうか。



これに相当する新しいドキュメントは、developer.apple.com
hittest –CœurMar

回答:


173

それはかなり基本的な質問のようです。しかし、この文書は他の文書ほど明確ではないことに同意します。ここに私の答えを示します。

hitTest:withEvent:UIResponder のの実装は、次のことを行います。

  • これは、呼び出したpointInside:withEvent:self
  • 戻り値がNOの場合、をhitTest:withEvent:返しますnil。物語の終わり。
  • 戻り値がYESの場合は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

??withEvent:...それはpointInsideすべきではない:withEvent私はそれがすべてのサブビューにpointInsideを送る思っリターンはYES、それがhitTest送信している場合は、」言及ところ3ステップべきではない
prostock

2月に戻ると、最初にhitTest:withEvent:が送信され、pointInside:withEvent:が自身に送信されました。次のSDKバージョンではこの動作を再確認していませんが、hitTest:withEvent:の送信は、イベントがビューに属しているかどうかのより高いレベルの制御を提供するため、より意味があると思います。pointInside:withEvent:イベントがビューに属しているかどうかではなく、イベントの場所がビュー上にあるかどうかを示します。たとえば、サブビューがその場所にある場合でも、サブビューはイベントを処理したくない場合があります。
MHCは、

1
WWDC2014セッション235-高度なスクロールビューとタッチ処理テクニックは、この問題の優れた説明と例を示しています。
antonio081014 2015

297

サブクラス化とビュー階層を混同していると思います。博士が言うことは次のとおりです。このビュー階層があるとします。階層では、次のように、クラス階層ではなく、ビュー階層内のビューについて話しています。

+----------------------------+
|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、階層が見下ろされて、タッチが行われたサブビューが表示されます。この場合は、からACそしてD、それは次のようになりますD
  4. D ヒットテストビューになります

答えてくれてありがとう。あなたが説明したことは私の心の中にあったことでもありますが、@ MHCはhitTest:withEvent:B、C、Dについても発言しています。DがAではなくCのサブビューである場合はどうなりますか?私は混乱したと思います...
realstuff02

2
私の図では、DはCのサブビューです
pgb

1
ではないでしょうA返すYESだけでなく、同じようにCしてDいますか?
Martin Wickman、2011年

2
見えない(.hiddenまたは0.1未満の不透明度のいずれかによる)ビュー、またはユーザー操作をオフにしたビューは、hitTestに応答しないことを忘れないでください。そもそもこれらのオブジェクトに対してhitTestが呼び出されているとは思いません。
ジョニー2013年

追加したかったのは、そのhitTest:withEvent:が、その階層に応じてすべてのビューで呼び出される可能性があることです。
Adithya 2013

47

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:on 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. すべてのサブビューのヒットを再帰的にチェックします。あれば、それを返します。
  4. それ以外の場合は、手順2の結果に応じてselfまたはnilを返します。

注:[self.subviewsreverseObjectEnumerator]ビューの階層を上から下までたどる必要があります。また、clipsToBoundsマスクされたサブビューをテストしないように確認してください。

使用法:

  1. サブクラス化されたビューにカテゴリをインポートします。
  2. 置き換えhitTest:withEvent:これに
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    return [self overlapHitTest:point withEvent:event];
}

公式Apple's Guideもいくつかの良いイラストを提供しています。

これが誰かを助けることを願っています。


すごい!明確なロジックと素晴らしいコードスニペットをありがとう、私の頭のスクラッチャーを解決しました!
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のスニペットは魅力のように機能します。私はそれをswift 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
    }
}

これを使用するには、次のようにuiviewでhitTest:point:withEventをオーバーライドします。

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()メソッドがtrueを返します

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

}

にタッチイベントを送信 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 onTouch]

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.