【问题标题】:How to send the touch events of UIScrollView to the view behind it?如何将 UIScrollView 的触摸事件发送到它背后的视图?
【发布时间】:2015-01-27 09:03:21
【问题描述】:

我在另一个视图之上有一个透明的 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 解决方案。

【问题讨论】:

  • 或许您可以在这种情况下使用 locationInView。但是看看你的代码,我不完全确定你想做什么......你想要任何点击都会产生相同的结果吗?或者您是否希望在某些图像上进行某些点击以执行某些操作?到目前为止,在 touchesBegan 中调用 touchesBegan 是没有意义的。
  • 你能发布你的点击手势识别器的代码吗?
  • 总结我想做的事情:if(!scroll) 让点击通过 :) 我没有手势识别器...我需要一个吗?
  • 100%。您需要将 UITapGestureRecognizer 添加到 UIScrollView,因为它只能识别平移和捏合手势。
  • 好的,Lyndsey,让我看看,我会回来的

标签: ios objective-c uiscrollview uikit


【解决方案1】:

首先UIScrollViews 只能识别UIPanGestureRecognizers 和UIPinchGestureRecognizers,因此您需要在UIScrollView 中添加UITapGestureRecognizer,以便它也可以识别任何点击手势:

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 你是什么意思让任何东西通过?这专门处理UIScrollView 上的所有tap 事件。如果是滚动手势,则不会调用 handleTap。如果是不在图像内的点击,则不会执行 if (CGRectContainsPoint... 条件。
  • 是的,但他想让点击进入滚动视图覆盖的视图,而不仅仅是在识别器中处理它:“它后面的视图有一些用户应该能够点击的图像开。”
  • 我删除了我的答案。这是走向国际海事组织的方式。它需要一些工作来“转发”事件,但它很酷
  • @Daij-Djan 哦,好的。我真的很想了解你的答案,但我确实认为还有一点需要解决。是的,我理解您所说的“转发”事件是什么意思,但我真的不这么看......这更像是滚动视图“处理”图像的事件。或者像您在倒数第二条评论中所说的“手动处理”。
【解决方案2】:

在这里我提出我的完整解决方案:

  • 直接向前触摸视图而不是调用控件事件。
  • 用户可以指定要转发的类。
  • 如果需要转发,用户可以指定检查哪些视图。

界面如下:

/**
 * 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];
    }
}

谢谢

【讨论】:

    【解决方案3】:

    问题是您的UIScrollView 正在使用该事件。要通过它,您必须禁用用户对其的交互,但它不会滚动。但是,如果您有触摸位置,则可以使用convertPoint:toView: 方法计算它落在底层视图的哪个位置,并通过传递CGPoint 对其调用一个数学方法。从那里,您可以计算出被点击的图像。

    【讨论】:

    • ok ... 所以这意味着实际计算图像并直接调用它的方法,而不是仅仅传递事件。但是为什么事件的转发不起作用呢?
    • 不正确,您不必禁用交互。让最热的人远离视线效果很好
    • @Daij-Djan 你是什么意思?有没有办法既消费事件又将其转发到响应者链上?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多