将触摸事件转发给UIScrollView

16

我有两个视图控制器。视图控制器A拥有一个UIScrollView并呈现视图控制器B。这个显示是交互式的,由scrollView.contentOffset来控制。

我想集成一个交互式的解除过渡效果:向上滑动时,应该交互地解除ViewController B。交互式解除过渡还应该控制ViewController A中的scrollView。

我的第一次尝试是使用UIPanGestureRecognizer,根据平移距离设置scrollView.contentOffset。这个方法可以运行,但是当平移手势结束时,scrollView偏移必须被动画到最终位置。使用-[UIScrollView setContentOffset:animated:]不是一个好的解决方案,因为它使用线性动画,不考虑当前的平移速度,并且不会漂亮地减速。

所以我认为应该可以将我的平移手势识别器中的触摸事件传递给滚动视图。这应该会给我们所有漂亮的滚动视图动画效果。

我尝试重写UIPanGestureRecognizer子类中的-touchesBegan/Moved/Ended/Cancelled withEvent:方法:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [scrollView touchesBegan:touches withEvent:event];
    [scrollView.panGestureRecognizer touchesBegan:touches withEvent:event];

    [super touchesBegan:touches withEvent:event];
}

但显然有些东西阻止了滚动视图进入跟踪模式(它确实会进入拖动= YES,但仅限于此)。我验证了scrollView的userInteractionEnabled属性已启用,而且没有隐藏并添加到视图层次结构中。

那么我该如何将我的触摸事件转发给UIScrollView


一个解决方案的建议,也许您可以手动从pan手势更新 contentOffset 而不带有动画效果? - coverback
这就是我在转换的交互部分所做的。然而,当我轻扫VC B视图时,我必须继续进行非交互式动画。 - Ortwin Gentz
如果我理解正确,你在视图控制器A的滚动视图中有一个视图控制器B?而且你的视图控制器B是由一个UIPanGesture手势来操作的?那么如果你向上滑动,它会使视图控制器B消失,并同时滚动视图控制器A? - Tancrede Chazallet
@AncAinu,不是的,两个视图控制器是独立的,VC B以模态方式呈现在A上。向上滑动时,VC B会交互式地消失。当手势识别器触发时,将调用dismissViewControllerAnimated:completion:,该方法调用-startInteractiveTransition:,并将VC A滚动视图添加回容器。从此时开始,我想将触摸事件转发到滚动视图。 - Ortwin Gentz
4个回答

15

在阅读一个有趣的答案描述了UIScrollView的事件流之后,我得出结论,尝试从手势识别器中“遥控”滚动视图可能非常难以实现,因为触摸在路由到视图和手势识别器时被改变。由于UITouch不符合NSCopying,我们也无法克隆触摸事件以便稍后以未修改状态发送它们。

虽然并没有真正解决我提出的问题,但我找到了一种解决方法来完成我所需要的功能。我只是向视图控制器B添加了一个滚动视图,并将其与VC A的滚动视图同步(当垂直滚动时,该滚动视图将添加到视图层次结构中):

// delegate of VC B's scrollView
- (void)scrollViewDidScroll:(UIScrollView*)scrollView
    scrollViewA.contentOffset = scrollView.contentOffset;
}

感谢弗里德里希·马克格拉夫提出这个想法


1
尽管没有真正解决问题,只是绕过了问题,但我还是将勾选标记授予了自己的答案。很乐意将勾选标记切换到任何真正有效的答案! - Ortwin Gentz

3

试着继承UIScrollView并重写hitTest:withEvent:方法,这样UIScrollView就能够捕捉到其边界之外的触摸事件。这样你就可以免费获得所有漂亮的UIScrollView动画效果了。代码示例:

@interface MagicScrollView : UIScrollView
@end

@implementation MagicScrollView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // Intercept touches 100pt outside this view's bounds on all sides. Customize this for the touch area you want to intercept
    if (CGRectContainsPoint(CGRectInset(self.bounds, -100, -100), point)) {
        return self;
    }
    return nil;
}

@end

或者在Swift中(感谢Edwin Vermeer!)

class MagicScrollView: UIScrollView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        if CGRectContainsPoint(CGRectInset(self.bounds, -100, -100), point) {
            return self
        }
        return nil
    } 
}

根据您的布局,您可能还需要覆盖UIScrollView的父视图上的pointInside:withEvent:方法。

请参阅以下问题获取更多信息:interaction beyond bounds of uiview


1
对于Swift,您可以使用: class MagicScrollView: UIScrollView { override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? { if CGRectContainsPoint(CGRectInset(self.bounds, -100, -100), point) { return self } return nil } } - Edwin Vermeer
1
感谢@EdwinVermeer!已将Swift版本添加到答案中。 - johnboiles

0

我在一个父视图中放置了一个滚动视图,以便具有左右边框。为了使这些边框可滚动,我的方法如下:

class CustomScrollView: UIScrollView {
  override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if let view = super.hitTest(point, withEvent: event) {
      return view
    }

    guard let superview = superview else {
      return nil
    }

    return superview.hitTest(superview.convertPoint(point, fromView: self), withEvent: event)
  }

  override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    if super.pointInside(point, withEvent: event) {
      return true
    }

    guard let superview = superview else {
      return false
    }

    return superview.pointInside(superview.convertPoint(point, fromView: self), withEvent: event)
  }
}

运行完美


0
作为对@johnboiles解决方案的补充。当您捕获整个矩形时,然后在滚动视图内部的元素上触摸将被跳过。您可以添加额外的触摸区域,对于普通的滚动视图,只需将其传递给super.hitTest即可:
class MagicScrollView: UIScrollView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        if CGRectContainsPoint(CGRect(x: self.bounds.origin.x - 30, y: self.bounds.origin.y, width: 30, height: self.bounds.size.height), point) {
            return self
        }
        if CGRectContainsPoint(CGRect(x: self.bounds.origin.x + self.bounds.size.width, y: self.bounds.origin.y, width: 30, height: self.bounds.size.height), point) {
            return self
        }
        return super.hitTest(point, withEvent: event)
    }
}

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