使用UIScrollView用两根手指滚动

时间:2009-04-24 19:23:05

标签: iphone objective-c uiscrollview

我有一个应用,我的主视图同时接受touchesBegantouchesMoved,因此需要单手触摸和拖动。我想实现一个UIScrollView,我让它工作,但它覆盖了drags,因此我的contentView永远不会收到它们。我想实现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];
}

14 个答案:

答案 0 :(得分:64)

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

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

for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) {     
    if ([gestureRecognizer  isKindOfClass:[UIPanGestureRecognizer class]]) {
        UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer;
        panGR.minimumNumberOfTouches = 2;               
    }
}

答案 1 :(得分:35)

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

self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;

panGestureRecognizer会忽略一个手指拖动,因此单指拖动事件将传递到内容视图。

答案 2 :(得分:14)

在iOS 3.2+中,您现在可以轻松实现双指滚动。只需在滚动视图中添加一个平移手势识别器,并将其maximumNumberOfTouches设置为1.它将声明所有单指滚动,但允许2个手指滚动将链向上传递到滚动视图的内置平移手势识别器(从而允许正常的滚动行为。)

UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)];
panGestureRecognizer.maximumNumberOfTouches = 1;
[scrollView addGestureRecognizer:panGestureRecognizer];
[panGestureRecognizer release];

答案 3 :(得分: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来处理未真正发生的触摸。

您可能希望阅读my article on advanced UIScrollView tricks(并在那里查看一些完全不相关的UIScrollView示例代码)。

当然,如果你无法使它工作,总是可以选择手动控制UIScrollView的移动,或使用完全自定义编写的滚动视图。 Three20 library中有TTScrollView类;对用户来说感觉不太好,但对程序员感觉很好。

答案 4 :(得分: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.gestureRecognizers替换为myScrollView.gestureRecognizers,就可以了。

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

如果您希望UIScrollView也可以进行缩放,这样可以解决问题...手势无法正常工作,因为捏缩放和滚动可以解决问题。如果我找到合适的答案,我会更新。

答案 5 :(得分: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

答案 6 :(得分: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

#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

答案 7 :(得分:1)

坏消息:iPhone SDK 3.0及更高版本,不再向-touchesBegan:和 - touchesEnded: ** UIScrollview **子类方法传递触摸。您可以使用不同的touchesShouldBegintouchesShouldCancelInContentView方法。

如果你真的想要接触这个,请有一个允许这样做的 hack

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超类的触摸。

答案 8 :(得分: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;
}

如果第二次触摸开始:事件带有多个手指,则允许滚动视图取消触摸。因此,如果用户使用两根手指进行平移,则此视图将获得touchesCanceled:消息。

答案 9 :(得分:1)

这似乎是互联网上这个问题的最佳资源。另一个近似解决方案can be found here

我以一种非常令人满意的方式以不同的方式解决了这个问题,主要是通过将我自己的手势识别器取代到等式中。我强烈建议任何试图达到原始海报所要求的效果的人都要考虑这种替代方法,而不是UIScrollView的侵略性子类化。

以下过程将提供:

  • 包含自定义视图的UIScrollView

  • 用两根手指(通过UIPinchGestureRecognizer

  • 进行缩放和平移
  • 您的视图所有其他触摸的事件处理

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

创建UIScrollView的自定义子类,并使此scrollview成为自定义子类。在视图控制器中为插件添加插座并连接它们。你现在完全配置好了。

您需要将以下代码添加到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的缩放行为中看到了这一点,并认为这将是一个很好的接触。

答案 10 :(得分:0)

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

答案 11 :(得分:0)

查看我的solution

#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

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

答案 12 :(得分:0)

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

scrollView.panGestureRecognizer.minimumNumberOfTouches = 2

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

scrollView.gestureRecognizers?.append(panGR)

并且在handlePan方法中,这是一个附加到ViewController的函数,只有一个print语句来验证是否正在输入该方法 - &gt;

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

HTH

答案 13 :(得分:0)

Kenshi Swift 4

中的回答
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
    if (gestureRecognizer is UIPanGestureRecognizer) {
        let panGR = gestureRecognizer as? UIPanGestureRecognizer
        panGR?.minimumNumberOfTouches = 2
    }
}