98

私の問題:EditView基本的にアプリケーションフレーム全体を占めるスーパービューとMenuView、下部の約20%のみを占めるサブビューがあり、実際には範囲外にあるMenuView独自のサブビューが含まれています(次のようなものです)。ButtonViewMenuViewButtonView.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オーバーライドする必要がありますか?MenuViewそれとも私はこれについて間違って考えていますか?

これに長い説明が必要な場合は、優れたオンラインリソースが役立ちます。ただし、AppleのUIViewドキュメントは、私にはわかりません。

ありがとう!

4

8 に答える 8

152

受け入れられた回答のコードをより一般的なものに変更しました-ビューがサブビューをその境界にクリップし、非表示になる可能性があり、さらに重要なことに、サブビューが複雑なビュー階層である場合、正しいサブビューが返される場合を処理します。

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

これが、より複雑なユースケースでこのソリューションを使用しようとしている人に役立つことを願っています。

于 2013-02-14T13:13:11.897 に答える
33

さて、私はいくつかの掘り下げとテストを行いました。これがどのように機能するかですhitTest:withEvent-少なくとも高レベルで。このシナリオをイメージしてください:

  • (E)は、すべてのビューの親であるEditViewです。
  • (M)は、EditViewのサブビューであるMenuViewです。
  • (B)は、MenuViewのサブビューであるButtonViewです。

図:

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

私はまだこの方法とこの問題に非常に慣れていないので、コードを書くためのより効率的または正しい方法がある場合は、コメントしてください。

後でこの質問に答える人の助けになることを願っています。:)

于 2012-08-03T17:39:13.773 に答える
30

Swift5では

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
}
于 2015-03-28T14:24:54.277 に答える
2

私が行うことは、ButtonViewとMenuViewの両方を、フレームが両方に完全に適合するコンテナーに配置することにより、ビュー階層の同じレベルに存在させることです。このように、クリップされたアイテムのインタラクティブ領域は、スーパービューの境界のために無視されません。

于 2012-08-02T04:07:01.913 に答える
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)
    }
}
于 2017-11-03T06:51:21.773 に答える
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」。

お役に立てば幸いです...!!!

于 2015-05-19T10:50:45.783 に答える
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
}
于 2016-08-26T17:16:59.213 に答える
0

ネストされたサブビューが多数あるため、上記のソリューションが機能しなかったため、これがどのように機能するかを理解するのに時間がかかりました。状況に応じて誰もが自分のコードを適応できるように、単にhitTestについて説明しようと思います。

この状況を想像してみてください。GrandParentと呼ばれるビューには、Parentと呼ばれる境界に完全にサブビューがあります。この親には、親の境界の外側に境界がある子と呼ばれるサブビューがあります。

-------------------------------------- -> Grand parent 
|              ------- -> Child      |
|              |     |               |
|          ----|-----|--- -> Parent  |
|          |   |   ^ |   |           |
|          |   |     |   |           |
|          ----|-----|---            |
|              | +   |               |
|     *        |-----|               |
|                                    |
--------------------------------------

* +^は3つの異なるユーザータッチです。+子ではタッチが認識されない境界のどこかで祖父母に触れると、祖父母のどこに触れたかに関係なく、祖父母はすべての直接サブビューを呼び出します.hitTest。したがって、ここでは、3回のタッチで、祖父母がを呼び出しますparent.hitTest()

hitTestは、指定されたポイントを含む最も遠い子孫を返すことになっています(ポイントは自己境界に関連して指定されます。この例では、祖父母は親境界に関連するポイントを使用してParent.hitTest()を呼び出します)。

タッチの場合*、parent.hitTestはnilを返し、完璧です。タッチの場合^、parent.hitTestは子を返します。これは、その範囲内にあるためです(hitTestのデフォルトの実装)。

ただし、+タッチの場合、タッチは親のバインドされていないため、parent.hitTestはデフォルトでnilを返します。したがって、基本的に親の境界に関連するポイントを子の境界に関連するポイントに変換するには、hitTestのカスタム実装が必要です。次に、子に対してhitTestを呼び出して、タッチが子の境界内にあるかどうかを確認します(子は、hitTestのデフォルトの実装を持ち、その境界内から最も遠い子孫、つまり:自体を返します)。

複雑なネストされたサブビューがあり、上記のソリューションがうまく機能しない場合、この説明は、ビュー階層に合わせてカスタム実装を作成するのに役立つ場合があります。

于 2021-01-30T15:22:34.550 に答える