开发者

How to cancel button tap if UIGestureRecognizer fires?

Update: The problem seems to be the dependency on another GestureRecognizer to fail. See comments and test project below this question!

In my iPhone app I have a view with multiple UIButtons as subviews. The view also has a UITapGestureRecognizer which is listening for taps with two fingers.

When a two-finger-tap occurs on the view I don't want the buttons to react to the tap, even if one of the fingers was inside the button. I thought this is what "cancelsTouchesInView" is for, but that doesn't work.

My question now is: How to tell my buttons to ignore taps when a gesture is recognized?

Edit: This is my gesture recognizer.

UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTapped:)];
[doubleTap setNumberOfTouchesRequired:2];
[doubleTap setNumberOfTapsRequired:1];
[doubleTap setCancelsTouchesInView:YES];
[doubleTap setDelaysTouchesBegan:YES];
[doubleTap setDelaysTouchesEnded:YES];
[self.view addGestureRecognizer:doubl开发者_StackOverflow中文版eTap];
[doubleTap release];


According to an Apple dev this is a bug. I filed a bug report with Apple. Thanks a lot for your hints, Deepak and gcamp!

Bug report:

Summary: When adding two UITapGestureRecognizers to a view where one requires the other to fail (requiresGestureRecognizerToFail:) the cancelsTouchesInView property of the first gesture recognizer is ignored.

Steps to Reproduce: 1. Create two UITapGestureRecognizers (r1 and r2) 2. Configure r1 to require two touches and one tap and to delay touchesBegan 3. Configure r2 to require two touches and two taps and to delay touchesBegan 4. Configure r1 to require r2 to fail [r1 requiresGestureRecognizerToFail:r2] 5. Add r1 and r2 to a view 6. Place a UIButton in the view 7. Tap with two fingers on the view, one should hit the button.

Expected Results: r1 should be recognized and the button tap should be canceled (cancelsTouchesInView defaults to YES for UITapGestureRecognizers).

Actual Results: r1 is recognized but the button touchedUpInside event is fired, too.

Regression: cancelTouchesInView works fine for r1 once you remove the dependency on r2 (step 4).


There's a method on UIGestureRecognizer that respond to your question

- (void)requireGestureRecognizerToFail:(UIGestureRecognizer *)otherGestureRecognizer

Basically you're requiring that the two tap recognizer fail before accepting the single tap one.

So,

[singleTapRecognizer requireGestureRecognizerToFail:twoTapRecognizer];


Use the delaysTouchesBegan property. Set it to YES.

Alternative

Disable user interaction on the buttons and attach a single finger tap recognizer like mentioned. In the tap handler, check if the tap falls within the bounds of the button. If it is within the bounds of a button, do [theButton sendActionsForControlEvents:UIControlEventTouchUpInside];. This will trigger the touch up event as desired even though the user interaction is disabled.


Mark, I ran into the same bug. If you'll post your radar #, I'll dupe it in bugreporter.

I wrote the following workaround. I subclass UITapGestureRecognizer, then use the subclass to track the touches which triggered the gesture action. If we get the same touches in touchesEnded on the view, we redirect to touchesCancelled. 'immediateTarget' is needed because the touchesEnded call on the view comes after the gesture's touchesEnded but before the gesture's action is called.

RPTapGestureRecognizer.h:

#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>

@interface RPTapGestureRecognizer : UITapGestureRecognizer

@property (nonatomic, retain) NSSet *lastTouches;

- (void)setImmediateTarget:(id)inTarget action:(SEL)inAction;

@end

RPTapGestureRecognizer.m:

#import "RPTapGestureRecognizer.h"

@interface RPTapGestureRecognizer ()
@property (nonatomic) SEL immediateAction;
@property (nonatomic, assign) id immediateTarget;
@end

@implementation RPTapGestureRecognizer

@synthesize lastTouches;
@synthesize immediateAction, immediateTarget;

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    self.lastTouches = nil;

    [super touchesBegan:touches withEvent:event];
}

- (void)setImmediateTarget:(id)inTarget action:(SEL)inAction
{
    self.immediateTarget = inTarget;
    self.immediateAction = inAction;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UIGestureRecognizerState startingState, finalState;

    startingState = self.state;
    [super touchesEnded:touches withEvent:event];
    finalState = self.state;

    if (startingState != finalState &&
        finalState == UIGestureRecognizerStateEnded) {
        /* Must copy; the underlying NSCFSet will be modified by the superclass, removing the touches */
        self.lastTouches = [[touches copy] autorelease];

        if (self.immediateAction)
            [self.immediateTarget performSelector:self.immediateAction
                                       withObject:self];
    }
}

- (void)dealloc
{
    self.lastTouches = nil;
    self.immediateTarget = nil;

    [super dealloc];
}

@end

In the view:

@synthesize lastTapGesture

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (self.lastTapGesture && [self.lastTapGesture.lastTouches isEqualToSet:touches]) {
        [self touchesCancelled:touches withEvent:event];
        self.lastTapGesture = nil;
        return;
    }
 /* touches ended implementation here */
}

 - (void)willHandleMouseTapGesture:(RPTapGestureRecognizer *)tapGesture
{
    /* Because one tap gesture is dependent upon another, the touchesEnded method is still going to be called, instead of touchesCanceled.
     * This is an Apple bug against all versions of iOS at least up to 5.0. It will be called in the run loop before tapGesture's action is called.
     *
     * See http://stackoverflow.com/questions/6188997/how-to-cancel-button-tap-if-uigesturerecognizer-fires/6211922#6211922 for details.
     */
    self.lastTapGesture = tapGesture;
}

- (void)configureGestures
{
    /* Two finger taps */
     RPTapGestureRecognizer *tapGestureOne;
     tapGestureOne = [[[RPTapGestureRecognizer alloc] initWithTarget:self 
                                                              action:@selector(handleMouseTapGesture:)] autorelease];
     tapGestureOne.numberOfTapsRequired = 1;
     tapGestureOne.numberOfTouchesRequired = 2;
     [tapGestureOne setImmediateTarget:self action:@selector(willHandleMouseTapGesture:)];
     [self addGestureRecognizer:tapGestureOne];
     [myGestures addObject:tapGestureOne];

     RPTapGestureRecognizer *tapGestureTwo;
     tapGestureTwo = [[[RPTapGestureRecognizer alloc] initWithTarget:self 
                                                              action:@selector(handleMouseTapGesture:)] autorelease];
     tapGestureTwo.numberOfTapsRequired = 2;
     tapGestureTwo.numberOfTouchesRequired = 2;
     [tapGestureTwo setImmediateTarget:self action:@selector(willHandleMouseTapGesture:)];
     [self addGestureRecognizer:tapGestureTwo];
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜