允许UIScrollView及其子视图同时响应触摸

26

我希望我的UIScrollView及其子视图都能接收到子视图内部的所有触摸事件,每个视图都可以按自己的方式响应。

或者,如果点击手势被转发到子视图,一切都将很好。

许多人在这个领域都在苦苦挣扎。以下是其中的一些相关问题:

UIScrollView如何从其子视图中夺取触摸
如何从UIScrollView中夺取触摸?
如何取消UIScrollView中的滚动

顺便说一下,如果我在滚动视图中重写hitTest:withEvent:方法,并确保userInteractionEnabled为YES,那么我就可以看到触摸事件。但这并不能真正解决我的问题,因为:

1)此时,我不知道它是否为tap手势。
2)有时我需要将userInteractionEnabled设置为NO。

编辑:澄清一下,是的,我想对待tap和pan手势的方式不同。tap手势应由子视图处理。滑动手势则可以按照通常的方式由滚动视图处理。


4
将userInteractionEnabled设置为NO,同时期望用户交互数据,看起来像是一个颠覆宇宙的悖论。 - borrrden
你想区分触摸和滚动吗? - shannoga
5个回答

40
首先,需要声明的是,如果在UIScrollView上将userInteractionEnabled设置为NO,则不会将任何触摸事件传递给子视图。据我所知,除了一种例外情况之外,没有其他方法可以解决这个问题:拦截UIScrollView的父视图上的触摸事件,并明确将这些事件传递到UIScrollView的子视图。但老实说,我不知道你为什么要这样做。如果你想禁用特定的UIScrollView功能(比如...滚动),你可以很容易地做到这一点,而无需禁用用户交互。

如果我理解你的问题正确,你需要处理UIScrollView中的触摸事件并且将其传递给子视图?无论如何(无论手势是什么),我认为你正在寻找的是gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:协议方法,在协议UIGestureRecognizerDelegate中。在你的子视图中,无论你设置了什么手势识别器,请在手势识别器上设置一个委托(可能是设置UIGestureReconginzer的类)。重写上述方法并返回YES。现在,这个手势将与任何可能“窃取”手势的其他识别器一起被识别(在你的情况下,是点击操作)。使用这种方法,甚至可以精细调整代码,只将某些类型的手势发送到子视图或仅在特定情况下发送手势。它给你很多控制权。只需确保阅读该方法,特别是这部分:

当gestureRecognizer或otherGestureRecognizer的手势识别会阻止另一个手势识别器识别其手势时,将调用此方法。请注意,返回YES保证允许同时识别;另一方面,返回NO不能保证防止同时识别,因为其他手势识别器的委托可能会返回YES。
当然,这只适用于手势识别器。如果你想使用touchesBegan:touchesEnded等来处理触摸事件,仍可能会遇到问题。你可以使用hitTest:将原始的触摸事件发送到子视图,但为什么要这样做呢?当你可以将UIGestureRecognizer附加到一个视图上并免费获得所有这些功能时,为什么还要使用UIView中的这些方法来处理事件呢?如果需要以没有标准UIGestureRecognizer可以提供的方式处理触摸,则可以子类化UIGestureRecognizer并在那里处理触摸。这样,您就可以获得UIGestureRecognizer的所有功能以及自己的自定义触摸处理。我真的认为Apple打算让UIGestureRecognizer取代大多数(如果不是全部)开发者在UIView上使用的自定义触摸处理代码。它允许代码重用,并且在缓解哪个代码处理哪个触摸事件时更容易处理。

1
太棒了,谢谢!如果还有其他人在阅读这篇文章,你们也可以尝试使用UIScrollView方法touchesShouldCancelInContentView来获得类似的效果。 - William Jockusch
如果您想禁用特定的UIScrollView功能(例如...滚动),则可以轻松地实现此操作,而无需禁用UserInteraction。-> "myScrollView.scrollEnabled = false" - RGML

3

我曾经遇到过同样的问题,但是这个UIScrollViewUIPageViewController中,所以需要稍微不同的处理方式。

通过将每个识别器(Recognizer)的cancelsTouchesInView属性设置为false,我能够接收到UIPageViewController内部按钮的触摸事件。

我通过将以下代码添加到viewDidLoad来完成这个操作:

guard let recognizers = self.pageViewController.view.subviews[0].gestureRecognizers else {
     print("No gesture recognizers on scrollview.")
     return
}

for recognizer in recognizers {
    recognizer.cancelsTouchesInView = false
}

3

我不知道这是否能帮到你,但我曾遇到过类似的问题,即我想让滚动视图处理双击操作,但是将单击操作转发给子视图。下面是在一个CustomScrollView中使用的代码:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {

    UITouch* touch = [touches anyObject];
    // Coordinates
    CGPoint point = [touch locationInView:[self.subviews objectAtIndex:0]];

    // One tap, forward
    if(touch.tapCount == 1){
        // for each subview
        for(UIView* overlayView in self.subviews){
            // Forward to my subclasss only
            if([overlayView isKindOfClass:[OverlayView class]]){
                // translate coordinate
                CGPoint newPoint = [touch locationInView:overlayView];
                //NSLog(@"%@",NSStringFromCGPoint(newPoint));

                BOOL isInside = [overlayView pointInside:newPoint withEvent:event];
                //if subview is hit
                if(isInside){
                    Forwarding
                    [overlayView touchesEnded:touches withEvent:event];
                    break;
                }
            }
        }

    }
    // double tap : handle zoom
    else if(touch.tapCount == 2){

        if(self.zoomScale == self.maximumZoomScale){
            [self setZoomScale:[self minimumZoomScale] animated:YES];
        } else {
            CGRect zoomRect = [self zoomRectForScrollView:self withScale:self.maximumZoomScale withCenter:point];            

            [self zoomToRect:zoomRect animated:YES];
        }

        [self setNeedsDisplay];

    }
}

当然,有效的代码应该被更改,但是此时你应该已经拥有所有必要的信息来决定是否需要转发事件。您可能需要在另一个方法中实现这一点,例如touchesMoved:withEvent:。
希望这可以帮到您。

0
一个达到你目标的hackish方法 - 不是100%准确的 - 是子类化UIWindow并重写- (void)sendEvent:(UIEvent *)event;
一个快速的例子:
在SecondResponderWindow.h头文件中
//SecondResponderWindow.h

@protocol SecondResponderWindowDelegate
- (void)userTouchBegan:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchMoved:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchEnded:(id)tapPoint onView:(UIView*)aView;
@end

@interface SecondResponderWindow : UIWindow
@property (nonatomic, retain) UIView *viewToObserve;
@property (nonatomic, assign) id <SecondResponderWindowDelegate> controllerThatObserves;
@end

在SecondResponderWindow.m文件中

//SecondResponderWindow.m

- (void)forwardTouchBegan:(id)touch onView:(UIView*)aView {
    [controllerThatObserves userTouchBegan:touch onView:aView];
}
- (void)forwardTouchMoved:(id)touch onView:(UIView*)aView {
    [controllerThatObserves userTouchMoved:touch onView:aView];
}
- (void)forwardTouchEnded:(id)touch onView:(UIView*)aView {
    [controllerThatObserves userTouchEnded:touch onView:aView];
}

- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];

    if (viewToObserve == nil || controllerThatObserves == nil) return;

    NSSet *touches = [event allTouches];
    UITouch *touch = [touches anyObject];
    if ([touch.view isDescendantOfView:viewToObserve] == NO) return;

    CGPoint tapPoint = [touch locationInView:viewToObserve];
    NSValue *pointValue = [NSValue valueWithCGPoint:tapPoint];

    if (touch.phase == UITouchPhaseBegan)
        [self forwardTouchBegan:pointValue onView:touch.view];
    else if (touch.phase == UITouchPhaseMoved)
        [self forwardTouchMoved:pointValue onView:touch.view];
    else if (touch.phase == UITouchPhaseEnded)
        [self forwardTouchEnded:pointValue onView:touch.view];
    else if (touch.phase == UITouchPhaseCancelled)
        [self forwardTouchEnded:pointValue onView:touch.view];
}

它并不完全符合您的期望 - 因为您的第二个响应者视图没有通过-touchDidBegin:或其他方式本地处理触摸事件,并且必须实现SecondResponderWindowDelegate。然而,这种方法确实允许您处理其他响应者上的触摸事件。

此方法受到MITHIN KUMAR的TapDetectingWindow的启发和扩展。


0
如果您需要区分触摸和滚动,则可以测试是否已移动触摸。 如果这是轻击,则不会调用touchHasBeenMoved,那么您可以假定这是一个触摸。
此时,您可以设置一个布尔值来指示是否发生了移动,并将此布尔值设置为其他方法中的条件。
我在旅途中,但如果这是您需要的,稍后我将能够更好地解释。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接