如何将UIScrollView的触摸事件发送到其后面的视图?

18

我有一个透明的UIScrollView在另一个视图的上面。

滚动视图中有内容-文本和图像,用于显示信息。

它后面的视图上有一些图像,用户应该能够点击它们。 而覆盖它们的内容可以使用上述滚动视图进行滚动。

我希望能够正常使用滚动视图(不缩放),但当该滚动视图实际上没有滚动时,可以让点击事件穿透到其后面的视图中。

使用触摸和滚动事件的组合,我可以确定何时允许点击事件穿过去。 但是,它后面的视图仍然无法接收到它们。

我尝试为所有触摸事件使用这样的代码:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 
{
    NSLog(@"touchesBegan %@", (_isScrolling ? @"YES" : @"NO"));

    if(!_isScrolling) 
    {
        NSLog(@"sending");
        [self.viewBehind touchesBegan:touches withEvent:event];
    }
}

但它并没有起作用。

在我的情况下,由于使用的案例,我无法真正应用hitTest和pointInside的解决方案。


你能发一下你的轻拍手势识别器的代码吗? - Lyndsey Scott
总结一下我想做的事情:如果没有滚动,就让点击事件通过 :) 我没有手势识别器...我需要一个吗? - memical
你需要向UIScrollView添加一个UITapGestureRecognizer,因为它只能本质上识别平移和缩放手势。 - Lyndsey Scott
好的,Lyndsey,让我研究一下,然后我会回来。 - memical
在我的情况下,我也无法真正应用hitTest和pointInside的解决方案,考虑到我的使用情况。能否解释一下?;) 这似乎是一个相当大胆的声明。 - Daij-Djan
显示剩余2条评论
4个回答

16

首先,UIScrollView只能自动识别UIPanGestureRecognizerUIPinchGestureRecognizer手势,所以您需要将UITapGestureRecognizer添加到UIScrollView中,以便它也可以识别任何点击手势:

UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];

// To prevent the pan gesture of the UIScrollView from swallowing up the
// touch event
tap.cancelsTouchesInView = NO;

[scrollView addGestureRecognizer:tap];

当你收到这个轻拍手势并触发handleTap:动作时,你可以使用locationInView:来检测轻拍手势的位置是否确实在滚动视图下方的一个图像框架内,例如:

- (void)handleTap:(UITapGestureRecognizer *)recognizer {

    // First get the tap gesture recognizers's location in the entire 
    // view's window
    CGPoint tapPoint = [recognizer locationInView:self.view];

    // Then see if it falls within one of your below images' frames
    for (UIImageView* image in relevantImages) {

        // If the image's coordinate system isn't already equivalent to
        // self.view, convert it so it has the same coordinate system
        // as the tap.
        CGRect imageFrameInSuperview = [image.superview convertRect:image toView:self.view]

        // If the tap in fact lies inside the image bounds, 
        // perform the appropriate action.
        if (CGRectContainsPoint(imageFrameInSuperview, tapPoint)) {
            // Perhaps call a method here to react to the image tap
            [self reactToImageTap:image];
            break;
        }
    }
}

这样,只有在识别到轻拍手势时才执行上述代码,如果轻拍的位置不在图像内,则你的程序只会对滚动视图上的轻拍作出反应;否则,你可以像往常一样滚动你的 UIScrollView


你在哪里让任何东西通过?你捕获事件没问题 - 看起来很好:) 但是接下来呢?你把事件转发到哪里? - Daij-Djan
是的,但他希望点击事件能够穿过ScrollView传递到被其覆盖的视图中,而不仅仅在识别器中处理: "它后面的视图有一些图片,用户应该能够点击它们。" - Daij-Djan
1
我删掉了我的答案。在我看来,这是正确的方法。需要做一些“转发”事件的工作,但它很酷。 - Daij-Djan
1
@Daij-Djan 哦,好的。我其实真的很想理解你的答案,但我认为还有一些需要解决的问题。是的,我明白你所说的“转发”事件的意思,但我并不完全这样看待它...更像是scrollview在为图像“处理”事件。或者像你在倒数第二条评论中所说的“手动处理”。 - Lyndsey Scott
1
对于任何在这里谷歌的人,还有@LyndseyScott,十年后做这件事现在变得非常简单了。只需要一行代码就可以了。 - Fattie
显示剩余3条评论

5

在这里,我呈现了完整的解决方案:

  • 直接将触摸转发到视图,而不是调用控件事件。
  • 用户可以指定要转发的类。
  • 用户可以指定需要检查是否需要转发的视图。

以下是界面:

/**
 * This subclass of UIScrollView allow views in a deeper Z index to react when touched, even if the scrollview instance is in front of them.
 **/
@interface MJForwardingTouchesScrollView : UIScrollView

/**
 * Set of Class objects. The scrollview will events pass through if the initial tap is not over a view of the specified classes.
 **/
@property (nonatomic, strong) NSSet <Class> *forwardsTouchesToClasses;

/**
 * Optional array of underlying views to test touches forward. Default is nil.
 * @discussion By default the scroll view will attempt to forward to views located in the same self.superview.subviews array. However, optionally by providing specific views inside this property, the scroll view subclass will check als among them.
 **/
@property (nonatomic, strong) NSArray <__kindof UIView*> *underlyingViews;

@end

并且实现:

#import "MJForwardingTouchesScrollView.h"
#import "UIView+Additions.h"

@implementation MJForwardingTouchesScrollView

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self != nil)
    {
        _forwardsTouchesToClasses = [NSSet setWithArray:@[UIControl.class]];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self != nil)
    {
        _forwardsTouchesToClasses = [NSSet setWithArray:@[UIControl.class]];
    }
    return self;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    BOOL pointInside = [self mjz_mustCapturePoint:point withEvent:event];

    if (!pointInside)
        return NO;

    return [super pointInside:point withEvent:event];
}

#pragma mark Private Methods

- (BOOL)mjz_mustCapturePoint:(CGPoint)point withEvent:(UIEvent*)event
{
    if (![self mjz_mustCapturePoint:point withEvent:event view:self.superview])
        return NO;

    __block BOOL mustCapturePoint = YES;
    [_underlyingViews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (![self mjz_mustCapturePoint:point withEvent:event view:obj])
        {
            mustCapturePoint = NO;
            *stop = YES;
        }
    }];

    return mustCapturePoint;
}

- (BOOL)mjz_mustCapturePoint:(CGPoint)point withEvent:(UIEvent *)event view:(UIView*)view
{
    CGPoint tapPoint = [self convertPoint:point toView:view];

    __block BOOL mustCapturePoint = YES;
    [view add_enumerateSubviewsPassingTest:^BOOL(UIView * _Nonnull testView) {
        BOOL forwardTouches = [self mjz_forwardTouchesToClass:testView.class];
        return forwardTouches;
    } objects:^(UIView * _Nonnull testView, BOOL * _Nullable stop) {
        CGRect imageFrameInSuperview = [testView.superview convertRect:testView.frame toView:view];

        if (CGRectContainsPoint(imageFrameInSuperview, tapPoint))
        {
            mustCapturePoint = NO;
            *stop = YES;
        }
    }];

    return mustCapturePoint;
}

- (BOOL)mjz_forwardTouchesToClass:(Class)class
{
    while ([class isSubclassOfClass:NSObject.class])
    {
        if ([_forwardsTouchesToClasses containsObject:class])
            return YES;

        class = [class superclass];
    }
    return NO;
}

@end

唯一使用的额外代码位于UIView+Additions.h分类内,其中包含以下方法:
- (void)add_enumerateSubviewsPassingTest:(BOOL (^_Nonnull)(UIView * _Nonnull view))testBlock
                                 objects:(void (^)(id _Nonnull obj, BOOL * _Nullable stop))block
{
    if (!block)
        return;

    NSMutableArray *array = [NSMutableArray array];
    [array addObject:self];

    while (array.count > 0)
    {
        UIView *view = [array firstObject];
        [array removeObjectAtIndex:0];

        if (view != self && testBlock(view))
        {
            BOOL stop = NO;
            block(view, &stop);
            if (stop)
                return;
        }

        [array addObjectsFromArray:view.subviews];
    }
}

谢谢


1

这非常简单:

class TrickScrollView: UIScrollView {
    
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        
        print("point! \(point)")
        
        if point.y < 400 { // example
            print("     .. pass on to whatever is behind")
            return false
        }
        
        // operate normally
        return super.point(inside: point, with: event)
    }
}

就是这样。在代码行“y < 400”处,只需使用自己的逻辑来决定是否要通过或正常处理。


0
问题在于你的UIScrollView正在消耗事件。要将其传递,您需要禁用用户与其的交互,但那样它就无法滚动了。然而,如果您有触摸位置,可以使用convertPoint:toView:方法计算出它在底层视图上的位置,并通过传递CGPoint调用其上的方法。从那里,您可以计算出点击了哪个图像。

好的...那就意味着实际计算图像并直接调用其方法,而不仅仅是通过传递事件来完成。但为什么事件转发不起作用呢? - memical
不是真的,你不必禁用交互。将最热的内容保持远离视图即可正常工作。 - Daij-Djan
@Daij-Djan,您是什么意思?有没有办法既消耗事件又将其转发到响应者链下方? - Levi

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