42

我有一个应用程序,我的主视图同时接受touchesBegantouchesMoved,因此接受单指触摸和拖动。我想实现 a UIScrollView,我让它工作,但它覆盖了拖动,因此我的 contentView 永远不会收到它们。我想实现 a UIScrollview,其中两指拖动表示滚动,并且单指拖动事件被传递给我的内容视图,因此它可以正常执行。我需要创建自己的子类UIScrollView吗?

这是我appDelegate实现UIScrollView.

@implementation MusicGridAppDelegate

@synthesize window;
@synthesize viewController;
@synthesize scrollView;


- (void)applicationDidFinishLaunching:(UIApplication *)application {    

    // Override point for customization after app launch    
    //[application setStatusBarHidden:YES animated:NO];
    //[window addSubview:viewController.view];

    scrollView.contentSize = CGSizeMake(720, 480);
    scrollView.showsHorizontalScrollIndicator = YES;
    scrollView.showsVerticalScrollIndicator = YES;
    scrollView.delegate = self;
    [scrollView addSubview:viewController.view];
    [window makeKeyAndVisible];
}


- (void)dealloc {
    [viewController release];
    [scrollView release];
    [window release];
    [super dealloc];
}
4

14 回答 14

65

在 SDK 3.2 中,UIScrollView 的触摸处理是使用手势识别器处理的。

如果要进行两指平移而不是默认的单指平移,可以使用以下代码:

for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {     
    if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
        panGR.minimumNumberOfTouches = 2;               
    }
}
于 2010-07-04T00:12:48.933 回答
45

对于 iOS 5+,设置此属性与 Mike Laurence 的回答具有相同的效果:

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

panGestureRecognizer 忽略一指拖动,因此一指拖动事件被传递到内容视图。

于 2013-08-04T02:24:49.637 回答
14

在 iOS 3.2+ 中,您现在可以很容易地实现两指滚动。只需将平移手势识别器添加到滚动视图并将其 maximumNumberOfTouches 设置为 1。它将要求所有单指滚动,但允许 2+ 手指滚动将链向上传递到滚动视图的内置平移手势识别器(因此允许正常的滚动行为)。

UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];
于 2010-09-25T05:08:06.460 回答
10

您需要继承 UIScrollView (当然!)。然后你需要:

  • 使单指事件转到您的内容视图(简单),并且

  • 使两指事件滚动滚动视图(可能很容易,可能很难,可能不可能)。

Patrick 的建议通常很好:让您的 UIScrollView 子类知道您的内容视图,然后在触摸事件处理程序中检查手指的数量并相应地转发事件。只需确保 (1) 您发送到内容视图的事件不会通过响应者链返回 UIScrollView(即确保处理所有事件),(2) 尊重通常的触摸事件流(即 touchesBegan,而不是一些 {touchesBegan, touchesMoved, touchesEnded},以 touchesEnded 或 touchesCancelled 结束),尤其是在处理 UIScrollView 时。#2 可能很棘手。

如果您决定事件是针对 UIScrollView,另一个技巧是让 UIScrollView 相信您的两指手势实际上是单指手势(因为 UIScrollView 不能用两根手指滚动)。尝试只将一根手指的数据传递给 super(通过过滤(NSSet *)touches参数——注意它只包含更改的触摸——并完全忽略错误手指的事件)。

如果这不起作用,你就有麻烦了。从理论上讲,您可以尝试通过创建一个看起来类似于 UITouch 的类来创建人工触摸以提供给 UIScrollView。底层 C 代码不检查类型,因此将 (YourTouch *) 转换为 (UITouch *) 可能会起作用,并且您将能够欺骗 UIScrollView 处理实际上并没有发生的触摸。

您可能想阅读我关于高级 UIScrollView 技巧的文章(并在那里查看一些完全不相关的 UIScrollView 示例代码)。

当然,如果你不能让它工作,总是可以选择手动控制 UIScrollView 的移动,或者使用完全自定义编写的滚动视图。Three20 库中有 TTScrollView 类;用户感觉不好,但程序员感觉不错。

于 2009-05-10T06:02:20.980 回答
8

这个答案一团糟,因为您只能通过阅读所有其他答案和评论来找到正确的答案(最接近的答案让问题倒退)。接受的答案太模糊而无用,并提出了一种不同的方法。

合成,这行得通

    // makes it so that only two finger scrolls go
    for (id gestureRecognizer in self.gestureRecognizers) {     
        if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]])
        {
            UIPanGestureRecognizer *panGR = gestureRecognizer;
            panGR.minimumNumberOfTouches = 2;              
            panGR.maximumNumberOfTouches = 2;
        }
    }   

这需要两个手指来滚动。我已经在子类中完成了此操作,但如果没有,只需替换self.gestureRecognizersmyScrollView.gestureRecognizers即可。

我唯一添加的是id用来避免丑陋的演员表:)

这可行,但如果您希望 UIScrollView 也进行缩放,则可能会变得非常混乱......手势无法正常工作,因为捏到缩放和滚动会解决它。如果我找到合适的答案,我会更新这个。

于 2011-03-09T14:00:42.800 回答
2

我们设法在我们的 iPhone 绘图应用程序中实现了类似的功能,方法是继承 UIScrollView 并以简单粗暴的方式根据触摸次数过滤事件:

//OCRScroller.h
@interface OCRUIScrollView: UIScrollView
{
    double pass2scroller;
}
@end

//OCRScroller.mm
@implementation OCRUIScrollView
- (id)initWithFrame:(CGRect)aRect {
    pass2scroller = 0;
    UIScrollView* newv = [super initWithFrame:aRect];
    return newv;
}
- (void)setupPassOnEvent:(UIEvent *)event {
    int touch_cnt = [[event allTouches] count];
    if(touch_cnt<=1){
        pass2scroller = 0;
    }else{
        double timems = double(CACurrentMediaTime()*1000);
        pass2scroller = timems+200;
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setupPassOnEvent:event];
    [super touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setupPassOnEvent:event];
    [super touchesMoved:touches withEvent:event];   
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    pass2scroller = 0;
    [super touchesEnded:touches withEvent:event];
}


- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
    return YES;
}

- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
    double timems = double(CACurrentMediaTime()*1000);
    if (pass2scroller == 0 || timems> pass2scroller){
        return NO;
    }
    return YES;
}
@end

ScrollView 设置如下:

scroll_view = [[OCRUIScrollView alloc] initWithFrame:rect];
scroll_view.contentSize = img_size;
scroll_view.contentOffset = CGPointMake(0,0);
scroll_view.canCancelContentTouches = YES;
scroll_view.delaysContentTouches = NO;
scroll_view.scrollEnabled = YES;
scroll_view.bounces = NO;
scroll_view.bouncesZoom = YES;
scroll_view.maximumZoomScale = 10.0f;
scroll_view.minimumZoomScale = 0.1f;
scroll_view.delegate = self;
self.view = scroll_view;

简单的点击什么都不做(你可以按照你需要的方式处理它),用两根手指点击滚动/缩放视图,如预期的那样。没有使用 GestureRecognizer,因此适用于 iOS 3.1

于 2011-03-10T09:29:36.297 回答
2

我对上面的代码做了进一步的改进。问题是,即使我们设置setCanCancelContentTouches:NO 了我们有问题,缩放手势也会中断内容。它不会取消内容触摸,但同时允许放大。为了防止这种情况,我通过每次将 minimumZoomScale 和 maximumZoomScale 设置为相同的值来锁定缩放,计时器会触发。

一个非常奇怪的行为是,当一个手指事件在允许的时间段内被两个手指手势取消时,计时器将被延迟。它在 touchCanceled 事件被调用后被触发。所以我们遇到了问题,尽管事件已经取消,但我们尝试锁定缩放,因此禁用下一个事件的缩放。为了处理这种行为,计时器回调方法检查是否之前调用了 touchesCanceled。 @implementation JWTwoFingerScrollView

#pragma mark -
#pragma mark Event Passing


- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        for (UIGestureRecognizer* r in self.gestureRecognizers) {
            if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
                [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
                [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
                zoomScale[0] = -1.0;
                zoomScale[1] = -1.0;
            }
            timerWasDelayed = NO;
        }
    }
    return self;
}
-(void)lockZoomScale {    
    zoomScale[0] = self.minimumZoomScale;
    zoomScale[1] = self.maximumZoomScale;
    [self setMinimumZoomScale:self.zoomScale];
    [self setMaximumZoomScale:self.zoomScale];
        NSLog(@"locked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
}
-(void)unlockZoomScale {
    if (zoomScale[0] != -1 && zoomScale[1] != -1) {
        [self setMinimumZoomScale:zoomScale[0]];
        [self setMaximumZoomScale:zoomScale[1]];
        zoomScale[0] = -1.0;
        zoomScale[1] = -1.0;
        NSLog(@"unlocked %.2f %.2f",self.minimumZoomScale,self.maximumZoomScale);
    }
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"began %i",[event allTouches].count);
    [self setCanCancelContentTouches:YES];
     if ([event allTouches].count == 1){
         touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
         [touchesBeganTimer retain];
         [touchFilter touchesBegan:touches withEvent:event];
     }
 }

//if one finger touch gets canceled by two finger touch, this timer gets delayed
// so we can! use this method to disable zooming, because it doesnt get called when two finger touch events are wanted; otherwise we would disable zooming while zooming
-(void)firstTouchTimerFired:(NSTimer*)timer {
    NSLog(@"fired");
    [self setCanCancelContentTouches:NO];
    //if already locked: unlock
    //this happens because two finger gesture delays timer until touch event finishes.. then we dont want to lock!
    if (timerWasDelayed) {
        [self unlockZoomScale];
    }
    else {
        [self lockZoomScale];
    }
    timerWasDelayed = NO;
 }

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
//    NSLog(@"moved %i",[event allTouches].count);
    [touchFilter touchesMoved:touches withEvent:event];
}

 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"ended %i",[event allTouches].count);
    [touchFilter touchesEnded:touches withEvent:event];
    [self unlockZoomScale];
 }

 //[self setCanCancelContentTouches:NO];
 -(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@"canceled %i",[event allTouches].count);
    [touchFilter touchesCancelled:touches withEvent:event];
    [self unlockZoomScale];
     timerWasDelayed = YES;
 }

@end

于 2011-12-28T00:27:14.240 回答
1

坏消息:iPhone SDK 3.0 及更高版本,不要再传递-touchesBegan:和 - touchesEnded:** UIScrollview**子类方法。你可以使用不一样的touchesShouldBegintouchesShouldCancelInContentView方法。

如果你真的想接触到这个,有一个允许这个的黑客。

在您的UIScrollView覆盖子类中,hitTest方法如下:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

  UIView *result = nil;
  for (UIView *child in self.subviews)
    if ([child pointInside:point withEvent:event])
      if ((result = [child hitTest:point withEvent:event]) != nil)
        break;

  return result;
}

这将传递给你这个触摸的子类,但是你不能取消对UIScrollView超类的触摸。

于 2009-11-03T16:48:59.077 回答
1

我要做的是让我的视图控制器设置滚动视图:

[scrollView setCanCancelContentTouches:NO];
[scrollView setDelaysContentTouches:NO];

在我的孩子看来,我有一个计时器,因为两指触摸通常从一根手指开始,然后是两根手指。

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    // Hand tool or two or more touches means a pan or zoom gesture.
    if ((selectedTool == kHandToolIndex) || (event.allTouches.count > 1)) {
        [[self parentScrollView] setCanCancelContentTouches:YES];
        [firstTouchTimer invalidate];
        firstTouchTimer = nil;
        return;
    }

    // Use a timer to delay first touch because two-finger touches usually start with one touch followed by a second touch.
    [[self parentScrollView] setCanCancelContentTouches:NO];
    anchorPoint = [[touches anyObject] locationInView:self];
    firstTouchTimer = [NSTimer scheduledTimerWithTimeInterval:kFirstTouchTimeInterval target:self selector:@selector(firstTouchTimerFired:) userInfo:nil repeats:NO];
    firstTouchTimeStamp = event.timestamp;
}

如果第二个 touchesBegan: 事件来自多于一根手指,则允许滚动视图取消触摸。因此,如果用户使用两根手指进行平移,该视图会收到一条touchesCanceled:消息。

于 2010-01-12T06:05:44.467 回答
1

这似乎是互联网上这个问题的最佳资源。可以在这里找到另一个接近的解决方案。

我以不同的方式以非常令人满意的方式解决了这个问题,主要是通过将我自己的手势识别器替换为等式。我强烈建议任何试图达到原始海报要求的效果的人考虑这种替代方法,而不是激进的UIScrollView.

以下过程将提供:

  • AUIScrollView包含您的自定义视图

  • 用两根手指缩放和平移(通过UIPinchGestureRecognizer

  • 您的视图对所有其他触摸的事件处理

首先,假设您有一个视图控制器及其视图。在 IB 中,使视图成为 scrollView 的子视图并调整视图的调整大小规则,使其不会调整大小。在滚动视图的属性中,打开任何“反弹”并关闭delaysContentTouches”。此外,您必须将缩放最小值和最大值设置为默认值 1.0 以外的值,因为正如 Apple 的文档所说,这是缩放工作所必需的。

创建 的自定义子类UIScrollView,并使此滚动视图成为自定义子类。为滚动视图添加一个出口到您的视图控制器并将它们连接起来。你现在已经完全配置好了。

您需要将以下代码添加到UIScrollView子类中,以便它透明地传递触摸事件(我怀疑这可以更优雅地完成,甚至可能完全绕过子类):

#pragma mark -
#pragma mark Event Passing

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesBegan:touches withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesMoved:touches withEvent:event];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [self.nextResponder touchesEnded:touches withEvent:event];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
    return NO;
}

将此代码添加到您的视图控制器:

- (void)setupGestures {
    UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinchGesture:)];
    [self.view addGestureRecognizer:pinchGesture];
    [pinchGesture release];
}

- (IBAction)handlePinchGesture:(UIPinchGestureRecognizer *)sender {
if ( sender.state == UIGestureRecognizerStateBegan ) {
    //Hold values
    previousLocation = [sender locationInView:self.view];
    previousOffset = self.scrollView.contentOffset;
    previousScale = self.scrollView.zoomScale;
} else if ( sender.state == UIGestureRecognizerStateChanged ) {
    //Zoom
    [self.scrollView setZoomScale:previousScale*sender.scale animated:NO];

    //Move
    location = [sender locationInView:self.view];
    CGPoint offset = CGPointMake(previousOffset.x+(previousLocation.x-location.x), previousOffset.y+(previousLocation.y-location.y));
    [self.scrollView setContentOffset:offset animated:NO];  
} else {
    if ( previousScale*sender.scale < 1.15 && previousScale*sender.scale > .85 )
        [self.scrollView setZoomScale:1.0 animated:YES];
}

}

请注意,在此方法中,您必须在视图控制器的类文件中定义许多属性:

  • CGFloat previousScale;
  • CGPoint previousOffset;
  • CGPoint previousLocation;
  • CGPoint location;

好的,就是这样!

不幸的是,我无法让 scrollView 在手势期间显示其滚动条。我尝试了所有这些策略:

//Scroll indicators
self.scrollView.showsVerticalScrollIndicator = YES;
self.scrollView.showsVerticalScrollIndicator = YES;
[self.scrollView flashScrollIndicators];
[self.scrollView setNeedsDisplay];

我真正喜欢的一件事是,如果您查看最后一行,您会注意到它会抓取大约 100% 的任何最终缩放并将其四舍五入。你可以调整你的容忍度;我在 Pages 的缩放行为中看到了这一点,并认为这将是一个不错的选择。

于 2010-05-25T21:06:27.127 回答
1

我把它放在 viewDidLoad 方法中,这完成了处理两个触摸平移行为的滚动视图和另一个处理单点触摸平移行为的平移手势处理程序 -->

scrollView.panGestureRecognizer.minimumNumberOfTouches = 2

let panGR = UIPanGestureRecognizer(target: self, action: #selector(ViewController.handlePan(_:)))
panGR.minimumNumberOfTouches = 1
panGR.maximumNumberOfTouches = 1

scrollView.gestureRecognizers?.append(panGR)

在作为附加到 ViewController 的函数的 handlePan 方法中,只有一个 print 语句来验证是否正在输入该方法 -->

@IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
    print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}

高温高压

于 2017-10-02T02:33:07.583 回答
0

是的,您需要继承UIScrollView并覆盖它的 -touchesBegan:-touchesEnded:方法来“向上”传递触摸。这可能还涉及具有UIView成员变量的子类,以便它知道将触摸传递给的含义。

于 2009-04-24T21:47:09.703 回答
0

查看我的解决方案

#import “JWTwoFingerScrollView.h”

@implementation JWTwoFingerScrollView

- (id)initWithCoder:(NSCoder *)coder {
    self = [super initWithCoder:coder];
    if (self) {
        for (UIGestureRecognizer* r in self.gestureRecognizers) {
            NSLog(@“%@”,[r class]);
            if ([r isKindOfClass:[UIPanGestureRecognizer class]]) {
                [((UIPanGestureRecognizer*)r) setMaximumNumberOfTouches:2];
                [((UIPanGestureRecognizer*)r) setMinimumNumberOfTouches:2];
            }
        }
    }
    return self;
}

-(void)firstTouchTimerFired:(NSTimer*)timer {
    [self setCanCancelContentTouches:NO];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self setCanCancelContentTouches:YES];
    if ([event allTouches].count == 1){
        touchesBeganTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(firstTouchTimerFired:) userInfo: nil repeats:NO];
        [touchesBeganTimer retain];
        [touchFilter touchesBegan:touches withEvent:event];
    }
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    [touchFilter touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@“ended %i”,[event allTouches].count);
    [touchFilter touchesEnded:touches withEvent:event];
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    NSLog(@“canceled %i”,[event allTouches].count);
    [touchFilter touchesCancelled:touches withEvent:event];
}

@end

它不会延迟第一次触摸,也不会在用户使用一个手指后用两根手指触摸时停止。它仍然允许使用计时器取消刚刚开始的一键式事件。

于 2011-12-15T16:39:41.727 回答
0

Kenshi在Swift 4中的回答

for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
    if (gestureRecognizer is UIPanGestureRecognizer) {
        let panGR = gestureRecognizer as? UIPanGestureRecognizer
        panGR?.minimumNumberOfTouches = 2
    }
}
于 2017-12-22T15:25:49.333 回答