web-dev-qa-db-ja.com

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

ほとんどのAppleドキュメントは非常によく書かれていますが、「 iOSのイベント処理ガイド 」は例外だと思います。そこに記載されていることを明確に理解するのは難しいです。

文書によれば、

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

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

143
realstuff02

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

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

  • selfpointInside:withEvent:を呼び出します
  • 戻りがNOの場合、hitTest:withEvent:nilを返します。物語の終わり。
  • 戻り値がYESの場合、hitTest:withEvent:メッセージをそのサブビューに送信します。最上位のサブビューから開始し、サブビューがnil以外のオブジェクトを返すか、すべてのサブビューがメッセージを受信するまで、他のビューに続きます。
  • サブビューが最初にnil以外のオブジェクトを返す場合、最初のhitTest:withEvent:はそのオブジェクトを返します。物語の終わり。
  • サブビューがnil以外のオブジェクトを返す場合、最初のhitTest:withEvent:selfを返します

このプロセスは再帰的に繰り返されるため、通常は最終的にビュー階層のリーフビューが返されます。

ただし、hitTest:withEventをオーバーライドして、何か別のことを行うことができます。多くの場合、pointInside:withEvent:のオーバーライドはより簡単であり、アプリケーションでイベント処理を微調整するための十分なオプションを提供します。

168
MHC

サブクラス化とビュー階層を混同していると思います。ドキュメントには次のように書かれています。このビュー階層があるとします。階層では、次のようにクラス階層についてではなく、ビュー階層内のビューについて説明しています。

+----------------------------+
|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はヒットテストビューになります
292
pgb

私はこれを見つける iOSのヒットテスト は非常に役立つ

enter image description here

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

答えてくれてありがとう、彼らは「オーバーレイ」ビューで状況を解決するのを助けてくれました。

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

X-ユーザーのタッチを想定します。 pointInside:withEvent: on BNOを返すため、hitTest:withEvent:Aを返します。一番上visibleビューでタッチを受け取る必要がある場合に問題を処理するために、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. 非表示または透過ビュー、またはuserInteractionEnabledNOに設定されているビューに対してタッチイベントを送信しないでください。
  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];
}

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

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

21
Lion

このスニペットのように表示されます!

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

@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)
}
1
mortadelo