用两个手指用UIScrollView滚动
我有一个应用程序,我的主视图接受touchesBegan
和touchesMoved
,因此采取单手指触摸,拖动。 我想实现一个UIScrollView
,我有它的工作,但它覆盖拖动,因此我的contentView从来没有收到它们。 我想实现一个UIScrollview
,其中双指拖动指示滚动,单指拖动事件传递给我的内容视图,所以它正常执行。 我是否需要创build我自己的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]; }
你需要inheritanceUIScrollView(当然!)。 那么你需要:
-
使单指事件进入你的内容视图(简单),和
-
使双指事件滚动滚动视图(可能很容易,可能很难,可能是不可能的)。
帕特里克的build议通常是好的:让你的UIScrollView子类知道你的内容视图,然后触摸事件处理程序检查手指的数量并相应地转发事件。 只要确保(1)你发送给内容视图的事件不会通过响应者链回滚到UIScrollView(即确保全部处理),(2)尊重通常的触摸事件stream程(即touchesBegan, touchesBegan,touchesMoved,touchesEnded}的一些数字,完成touchesEnded或touchesCancelled),尤其是在处理UIScrollView时。 #2可能会很棘手。
如果你决定这个事件是针对UIScrollView的,另一个技巧是让UIScrollView相信你的双指手势实际上是一个手指的手势(因为UIScrollView不能用两个手指滚动)。 尝试只将一个手指的数据传递给超级(通过过滤(NSSet *)touches
参数 – 注意它只包含更改的触摸 – 并忽略错误的手指事件)。
如果不行的话,你就麻烦了。 理论上,您可以尝试通过创build类似于UITouch的类来创build仿真触摸来提供给UIScrollView。 基础的C代码不检查types,所以也许将(YourTouch *)转换为(UITouch *)可以工作,并且可以欺骗UIScrollView来处理实际上并没有发生的事情。
您可能想阅读我关于高级UIScrollView技巧的文章 (并在那里看到一些完全不相关的UIScrollView示例代码)。
当然,如果你不能工作,总是有一个选项可以手动控制UIScrollView的移动,或者使用一个完全自定义的滚动视图。 Three20库中有TTScrollView类; 用户感觉不好,但对程序员感觉良好。
在SDK 3.2中,使用手势识别器处理UIScrollView的触摸处理。
如果您想要执行双指平移而不是默认的单指平移,则可以使用以下代码:
for (UIGestureRecognizer *gestureRecognizer in scrollView.gestureRecognizers) { if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) { UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *) gestureRecognizer; panGR.minimumNumberOfTouches = 2; } }
对于iOS 5+,设置此属性与Mike Laurence的答案效果相同:
self.scrollView.panGestureRecognizer.minimumNumberOfTouches = 2;
一个手指拖动被panGestureRecognizer忽略,所以单指拖动事件被传递给内容视图。
在iOS 3.2 +中,您现在可以非常轻松地实现双指滚动。 只需在滚动视图中添加一个平移手势识别器,并将其maximumNumberOfTouches设置为1.它将要求所有单指滚动,但允许2+手指滚动向上传递给滚动视图的内置平移手势识别器(因此允许正常的滚动行为)。
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(recognizePan:)]; panGestureRecognizer.maximumNumberOfTouches = 1; [scrollView addGestureRecognizer:panGestureRecognizer]; [panGestureRecognizer release];
这个答案是一团糟,因为你只能通过阅读所有其他的答案和评论(最接近的答案得到的问题倒退)才能find正确的答案。 被接受的答案太模糊不实,并提出了一个不同的方法。
合成,这工作
// 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; } }
这需要两个手指进行滚动。 我已经在一个子类中完成了这个,但是如果没有,只需用myScrollView.gestureRecognizers
代替self.gestureRecognizers
,你就可以开始。
我唯一添加的是使用id
来避免丑陋的演员:)
这个工程,但可以得到相当凌乱,如果你想让你的UIScrollView也做缩放…手势不能正常工作,因为捏到缩放和滚动打出来。 我会更新这个,如果我find一个合适的答案。
我们设法在我们的iPhone绘图应用程序中实现类似的function,通过inheritanceUIScrollView并根据触摸次数以简单粗暴的方式过滤事件:
//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
我对上面的代码有了进一步的改进。 问题是,即使在我们设置setCanCancelContentTouches:NO
我们有问题,缩放手势会中断内容。 它不会取消内容的触摸,但允许同时放大。 为了防止这种情况,我通过每次将minimumZoomScale和maximumZoomScale设置为相同的值来locking缩放,定时器会触发。
一个非常奇怪的行为是,当一个手指事件在允许的时间段内被双手手势取消时,计时器将被延迟。 touchCanceled事件被调用后会被触发。 所以我们遇到这个问题,我们试图locking缩放,尽pipe事件已经被取消,因此禁止缩放下一个事件。 为了处理这种行为,定时器callback方法检查是否之前调用了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
坏消息:iPhone SDK 3.0及更高版本,不要通过触摸-touchesBegan:
和 – touchesEnded:
** UIScrollview
**子类的方法了。 你可以使用touchesShouldBegin
和touchesShouldCancelInContentView
方法是不一样的。
如果你真的想得到这个接触,有一个黑客 ,允许这个。
在你的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
超类。
我所做的是让我的视图控制器设置滚动视图:
[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:
消息。
这似乎是在互联网上这个问题的最佳资源。 在这里可以find另一个closures的解
我用一种不同的方式以非常令人满意的方式解决了这个问题,主要是通过把我自己的手势识别器代入方程。 我强烈build议任何试图达到原始海报要求的效果的人都会考虑使用UIScrollView
powershell子类。
以下过程将提供:
-
包含您的自定义视图的
UIScrollView
-
用两个手指缩放和平移(通过
UIPinchGestureRecognizer
) -
您的视图的所有其他触摸事件处理
首先,我们假设你有一个视图控制器及其视图。 在IB中,使视图成为scrollView的子视图,并调整视图的resize规则,使其不resize。 在滚动视图的属性中,打开任何说“反弹”并closures “ delaysContentTouches
”。 此外,您必须将缩放最小值和最大值设置为默认值1.0以外的值,正如Apple的文档所述,这是缩放工作所必需的。
创build一个UIScrollView
的自定义子类,并使这个滚动视图的自定义子类。 添加一个sockets到您的视图控制器的滚动视图,并连接起来。 你现在完全configuration。
您将需要添加下面的代码到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的缩放行为中看到过这一点,并认为这将是一个很好的接触。
看看我的解决scheme :
#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
它不会延迟第一次触摸,并且在用户使用两根手指触摸后不会停止。 它仍然允许使用计时器取消刚开始的单触事件。
是的,你需要-touchesEnded:
UIScrollView
子类并覆盖它的touchesBegan:
和-touchesEnded:
方法来传递“up”。 这可能还会涉及具有UIView
成员variables的子类,以便它知道传递给它的意思。
我把它放在viewDidLoad方法中,这就完成了处理两个触摸平移行为的滚动视图和处理一个触摸平移行为的另一个平移手势处理器 – >
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的函数,只需要一个打印语句来validation方法是否正在进入 – >
@IBAction func handlePan(_ sender: UIPanGestureRecognizer) { print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)") }
HTH