我有一个应用程序,我的主视图接受touchesBegan
和touchesMoved
,因此接受单指触摸和拖动。我想实现一个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];
}
你需要子类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类;它对用户来说并不好,但对程序员来说确实感觉很好。
这似乎是互联网上这个问题的最佳资源。另一个接近解决方案can be found here。
我以一种非常令人满意的方式以不同的方式解决了这个问题,主要是通过将我自己的手势识别器取代到等式中。我强烈建议任何试图达到原始海报要求的效果的人都会考虑使用UIScrollView
的侵略性子类化。
以下过程将提供:
UIScrollView
UIPinchGestureRecognizer
)首先,假设您有一个视图控制器及其视图。在IB中,使视图成为scrollView的子视图,并调整视图的调整大小规则,以便它不会调整大小。在scrollview的属性中,打开任何显示“bounce”并关闭“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的缩放行为中看到了这一点,并认为这将是一个很好的接触。
看看我的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
它不会延迟第一次触摸,并且在用户使用后用两个手指触摸时不会停止。它仍允许使用计时器取消刚开始的一触式事件。
是的,您需要继承UIScrollView
并覆盖其-touchesBegan:
和-touchesEnded:
方法以传递“up”。这可能还涉及具有UIView
成员变量的子类,以便它知道将触摸传递给它的意义。
我把它放在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的函数,只有一个print语句来验证是否正在输入该方法 - >
@IBAction func handlePan(_ sender: UIPanGestureRecognizer) {
print("Entered handlePan numberOfTuoches: \(sender.numberOfTouches)")
}
HTH
Kenshi在Swift 4中的回答
for gestureRecognizer: UIGestureRecognizer in self.gestureRecognizers! {
if (gestureRecognizer is UIPanGestureRecognizer) {
let panGR = gestureRecognizer as? UIPanGestureRecognizer
panGR?.minimumNumberOfTouches = 2
}
}
在SDK 3.2中,UIScrollView的触摸处理使用Gesture Recognizers处理。
如果要进行双指平移而不是默认的单指平移,可以使用以下代码:
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];
这个答案很乱,因为你只能通过阅读所有其他答案和评论找到正确的答案(最接近的答案得到了倒退的问题)。接受的答案太模糊而无用,并提出了一种不同的方法。
合成,这是有效的
// 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也可以进行缩放,那么这可能会变得非常混乱......手势无法正常工作,因为捏缩放和滚动对抗它。如果我找到合适的答案,我会更新。
我们通过子类化UIScrollView并根据触摸次数以简单粗暴的方式过滤事件,设法在我们的iPhone绘图应用程序中实现类似的功能:
//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设置为相同的值来锁定缩放,计时器将触发。
一个非常奇怪的行为是当一个手指事件在允许的时间段内被双指手势取消时,计时器将被延迟。在调用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
坏消息: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:
消息。