6

I want to make a custom gesture recognizer with three fingers. Which is similar to unpinch gesture recognizer.

All I need is an idea about how to recognize it.

My gesture needs to recognize three fingers with three directions. For example:

enter image description here

enter image description here

enter image description here

I hope images makes sense. I need to make it flexible for any three opposite directions. Thanks in advance. Any help would be appreciated.

I am aware about the subclass methods and I've created custom gestures already with single finger like semicircle, full circle. I need a coding idea about how to handle that.

Dinesh Raja
  • 8,501
  • 5
  • 42
  • 81
  • 2
    pinch gesture recognizer doesn't do what you want? – nielsbot May 11 '14 at 11:38
  • But, client needs it to work with 3 fingers. – Dinesh Raja May 11 '14 at 16:55
  • 5
    Do you create app for Teenage Mutant Ninja Turtles? – sage444 May 30 '14 at 09:14
  • @sage444 No. I am not. – Dinesh Raja May 30 '14 at 09:38
  • You do not explain if you **also** want to recognise **at the same time**, translation and/or rotation. – Fattie Jun 01 '14 at 16:38
  • Yes I want to prevent rotation and panning of three fingers in same direction too. – Dinesh Raja Jun 02 '14 at 03:44
  • "Yes I want to prevent rotation and panning of three fingers in same direction too" that's totally bizarre, but Sandy has explained how to do it. – Fattie Jun 02 '14 at 05:31
  • He explained with 'canPreventGestureRecognizer' subclass method. But I don't have a rotation gesture recognizer with my view. I need to prevent detection of rotation touches in this custom gesture. – Dinesh Raja Jun 02 '14 at 05:33
  • 1
    @DineshRaja, what you can do in this case is make two more recognizers: a rotation recognizer and a pan recognizer. Then use -shouldBeRequiredToFailByGestureRecognizer: on your pinch recognizer to ensure that both the pan and rotation are failing before the pinch can be used. – Sandy Chapman Jun 02 '14 at 12:11
  • Hi Dinesh -- exactly as Sandy explains totally in that comment (and has been mentioned, I counted it!! some eleven times!!) you have to use shouldBeRequiredToFailByGestureRecognizer; i.e., you have to make two (or more) gesture recognisers and then use shouldBeRequiredToFailByGestureRecognizer. (Again just **for the record**, it's utterly, totally, bizarre that the client would want to "exlude" rotation/translation -- but **IF THAT IS THE GOAL**, you must, to repeat, do precisely what Sandy has explained. There is (effectively) no other way to do it. – Fattie Jun 03 '14 at 09:44
  • Did I mention, to get this done, do what Sandy has explained ;-) – Fattie Jun 03 '14 at 09:44
  • @JoeBlow Thanks for that clarification. I agreed already that "Rotation after a pinch is also a pinch". So I don't care about rotation anymore. FYI, client seeing this thread and I need to let you know that excluding of rotation is not his requirement. Only thing I need to prevent is panning and I will follow your suggestions. – Dinesh Raja Jun 03 '14 at 11:56

4 Answers4

4

You need to create a UIGestureRecognizer subclass of your own (let's call it DRThreeFingerPinchGestureRecognizer) and in it to implement:

– touchesBegan:withEvent:
– touchesMoved:withEvent:
– touchesEnded:withEvent:
– touchesCancelled:withEvent:

These methods are called when touches are accepted by the system and possibly before they are sent to the view itself (depending on how you setup the gesture recognizer). Each of these methods will give you a set of touches, for which you can check the current location in your view and previous location. Since pinch gesture is relatively very simple, this information is enough for you to test if the user is performing a pinch, and fail the test (UIGestureRecognizerStateFailed). If state was not failed by – touchesEnded:withEvent:, you can recognize the gesture.

I say pinch gestures are simple, because you can easily track each touch and see how it moves compared to other touches and itself. If a threshold of an angle is passed and broken, you fail the test, otherwise you allow it to continue. If touches do not move in separate angles to each other, you fail the test. You will have to play with what angles of the vectors are acceptable, because 120 degrees are not optimal for the three most common fingers (thumb + index + middle fingers). You may just want to check that the vectors are not colliding.

Make sure to read the UIGestureRecognizer documentation for an in-depth look at the various methods, as well as subclassing notes.

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • Hey Leo, Thanks for the link. I am aware about the subclass methods and I've created custom gestures already with single finger like semicircle, full circle. I need a coding idea about how to handle that. – Dinesh Raja May 30 '14 at 09:30
  • I cannot use UIPinchGestureRecognizer subclass, since it won't work with 3 fingers. For UIGestureRecognizer subclass, As you said we need to handle the degrees. But when handling the degree, if the user try to rotate, then same degrees will be maintained between touches and we end up start recognizing the wrong one. – Dinesh Raja May 30 '14 at 09:37
  • @DineshRaja, No, you can track angles yourself. The touch objects are persistent from start to finish, so you can create a mapping of touch and previous angle. If new angle is a steep change, you can fail the test. If one touch disappears in the middle (user lifted finger), you can fail the test. – Léo Natan May 30 '14 at 09:42
  • @DineshRaja You can calculate angles using a normalized touch vector and another vector pointing up. – Léo Natan May 30 '14 at 09:44
  • I have added comment in the other answer. Let me know if I am in a wrong path. Thanks – Dinesh Raja May 31 '14 at 07:11
  • Just FYI angles are not relevant to a pinch - it's just the distance. A three-finger pinch is a commonplace gesture. You can do a three-finger pinch with your three fingers in a perfectly straight line. Or you can have two right next to each other and one far away. Angles are completely unrelated. – Fattie Jun 02 '14 at 05:05
  • 1
    @JoeBlow That's incorrect. If you start the gesture, and then rotate the fingers, the vectors change angles. This change of angle has to be prevented. – Léo Natan Jun 02 '14 at 05:19
  • Hi Leo - a pinch is a pinch - try it on an iPhone. It has absolutely nothing to do with rotation, or translation. – Fattie Jun 02 '14 at 05:26
  • "vectors" are uninvolved with pinch gestures. pinch gestures are distance based. (or, velocity). – Fattie Jun 02 '14 at 05:28
  • 1
    @Joe, "Vectors" are indeed involved. Every two measure points give you a vector. These vectors have an angle relative to something. If these angles change, the pinch gesture should not be accepted. "Prevent" means failing the test, just like Apple does it in their own implementation. When pinch fails, the user will prevent rotation by himself. – Léo Natan Jun 02 '14 at 05:32
  • (a vector includes distance and direction; "distance" is just a scalar, not a vector. "5 meters" or "3 inches" is not a vector. a vector is a direction+distance (or indeed, a set of >1 scalars). in the scheme i was explaining, you use only distance, discarding the direction.) – Fattie Jun 03 '14 at 09:47
  • Uhm, there is a direction. oldPoint -> newPoint, it defines a vector. Direction is important in pinch gesture, as well as speed. – Léo Natan Jun 03 '14 at 10:03
3

I think track angles is leading you down the wrong path. I think it's likely a more flexible and intuitive gesture if you don't constrain it based on the angles between the fingers. It'll be less error prone if you just deal with it as a three-fingered pinch regardless of how the fingers move relative to each other. This is what I'd do:

if(presses != 3) {
     state = UIGestureRecognizerStateCancelled;
     return;
}

// After three fingers are detected, begin tracking the gesture.
state =  UIGestureRecognizerStateBegan; 
central_point_x = (point1.x + point2.x + point3.x) / 3;
central_point_y = (point1.y + point2.y + point3.y) / 3;

// Record the central point and the average finger distance from it.
central_point = make_point(central_point_x, central_point_y);
initial_pinch_amount = (distance_between(point1, central_point) + distance_between(point2, central_point) + distance_between(point3, central_point)) / 3;

Then on each update for touches moved:

if(presses != 3) {
     state = UIGestureRecognizerStateEnded;
     return;
}

// Get the new central point
central_point_x = (point1.x + point2.x + point3.x) / 3;
central_point_y = (point1.y + point2.y + point3.y) / 3;
central_point = make_point(central_point_x, central_point_y);

// Find the new average distance
pinch_amount = (distance_between(point1, central_point) + distance_between(point2, central_point) + distance_between(point3, central_point)) / 3;

// Determine the multiplicative factor between them.
difference_factor = pinch_amount / initial_pinch_amount

Then you can do whatever you want with the difference_factor. If it's greater than 1, then the pinch has moved away from the center. If it's less than one, it's moved towards the center. This will also give the user the ability to hold two fingers stationary and only move a third to perform your gesture. This will address certain accessibility issues that your users may encounter.

Also, you could always track the incremental change between touch move events, but they won't be equally spaced in time and I suspect you'll have more troubles dealing with it.

I also apologize for the pseudo-code. If something isn't clear I can look at doing up a real example.

Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
  • That's not correct. If a user rotates his fingers, your math will not take into account that rotation occurred, which does not answer the "pinch" requirement. Angles are very simple to calculate, so there is little reason not to. – Léo Natan May 30 '14 at 17:46
  • 1
    @LeoNatan, if the use rotates their fingers it will be still be recognized as valid. The only thing my solution does is to detect how far the user moves their fingers from a central point. There was nothing in the question that stated rotations needed to be detected and disallowed. – Sandy Chapman May 30 '14 at 18:23
  • I don't think you understand what "pinch" means. It's not a pan gesture, where rotation is allowed. Pinch is defined by either fingers moving toward a center or away from it, on the same angle they started the motion vector. – Léo Natan May 30 '14 at 18:50
  • Thank you @SandyChapman. This will absolutely help me to find if the user pinches or unpinch the screen. I am working on Leo's answer to prevent the rotation detection and gonna include yours to find the pinch and unpinch. – Dinesh Raja May 31 '14 at 07:07
  • @SandyChapman One more thing, we need to take into account is "User should not allowed to pan the screen with three fingers". I think we can handle that with drastic change in the center point. – Dinesh Raja May 31 '14 at 07:08
  • I will summarize both of your answers and will work on the code and post you updates. – Dinesh Raja May 31 '14 at 07:09
  • Honestly, this is wrong. You simply calculate the total distance (all three distances) for a three-finger pinch. You don't need the center for any reason, nor any angles. 'dun done it many times. Cheers! – Fattie Jun 01 '14 at 16:26
  • @JoeBlow, I appreciate your solution, but the fact that there are other solutions doesn't make this one wrong. – Sandy Chapman Jun 01 '14 at 19:05
  • @DineshRaja, if you want to prevent panning with three fingers you should check out the -canPreventGestureRecognizer: method on the UIGestureRecognizer. You can use this to block you panning gesture when your pinch recognizer detects a valid gesture. – Sandy Chapman Jun 01 '14 at 19:05
  • @SandyChapman I didn't mean the UIPanGestureRecognizer. I meant if user moves all three fingers in same direction in this gesture. Like like we need to prevent rotation and panning of three fingers in same direction – Dinesh Raja Jun 02 '14 at 03:41
  • Ssndy "canPreventGestureRecognizer" --- **exactly** – Fattie Jun 02 '14 at 04:47
  • Just to be utterly clear, **Sandy's solution will work perfectly**, it's just that it is "unnecessary work" and (not that this matters) it's "not how you do it", ie programming a pinch is commonplace and you just do it with the distance sum. – Fattie Jun 02 '14 at 04:55
3

Quick note for future readers: the way you do an unpinch/pinch with three fingers is add the distances ab,bc,ac.

However if your graphics package just happens to have on hand "area of a triangle" - simply use that. ("It saves one whole line of code!")

Hope it helps.


All you need to do is track:

the distance between the three fingers!

Simply add up "every" permutation

(Well, there's three .. ab, ac and cb. Just add those; that's all there is to it!)

When that value, say, triples from the start value, that's an "outwards triple unpinch".

... amazingly it's that simple.

Angles are irrelevant.


Footnote if you want to be a smartass: this applies to any pinch/unpinch gesture, 2, 3 fingers, whatever:

track the derivative of the sum-distance (I mean to say, the velocity) rather than the distance. (Bizarrely this is often EASIER TO DO! because it is stateless! you need only look at the previous frame!!!!)

So in other words, the gesture is trigger when the expansion/contraction VELOCITY of the fingers reaches a certain value, rather than a multiple of the start value.


More interesting footnote!

However there is a subtle problem here: whenever you do anything like this (any platform) you have to be careful to measure "on the glass".

IF You are just doing distance (ie, my first solution above) of course everything cancels out and you can just say "if it doubles" (in pixels -- points -- whatever the hell). BUT if you are doing velocity as part of the calculation in any gesture, then somewhat surprisingly, you have to literally find the velocity in meters per second in the real world, which sounds weird at first! Of course you can't do this exactly (particularly with android) coz glass sizes vary somewhat, but you have to get close to it. Here is a long post discussing this problem http://answers.unity3d.com/questions/292333/how-to-calculate-swipe-speed-on-ios.html In practice you usually have to make do with "screen-widths-per-second" which is pretty good. (But this may be vastly different on phones, large tablets, and these days "surface" type things. on your whole iMac screen, 0.1 screenwidthspersecond may be fast, but on an iPhone that is nothing, not a gesture.)


Final footnote! I simply don't know if Apple use "distance multiple" or "glass velocity" in their gesture recognition, or also likely is some subtle mix. I've never read an article from them commenting on it.


Another footnote! -- if for whatever reason you do want to find the "center" of the triangle (I mean the center of the three fingers). This is a well-travelled problem for game programmers because, after all, all 3D mesh is triangles.

Fortunately it's trivial to find the center of three points, just add the three vectors and divide by three! (Confusingly this even works in higher dimensions!!)

You can see endless posts on this issue...

http://answers.unity3d.com/questions/445442/calculate-uv-at-center-of-triangle.html

http://answers.unity3d.com/questions/424950/mid-point-of-a-triangle.html

Conceivably, if you were incredibly anal, you would want the "barycenter" which is more like the center of mass, just google if you want that.

Community
  • 1
  • 1
Fattie
  • 27,874
  • 70
  • 431
  • 719
  • Note. Leo raised the point that you may **also** want to recognise simultaneous 3F translation and/or 3F rotation. If so, you simply make **SEPARATE** gesture recognisers for those, it's that easy. – Fattie Jun 01 '14 at 16:40
  • Interesting solution. I think you'll get somewhat different changes in magnitude vs my solution, but I think it'd work just about the same without having to calculate the center value (which, although fairly trivial to do, is still more work than not doing it). – Sandy Chapman Jun 01 '14 at 19:00
  • Thanks @JoeBlow I am not sure how we can prevent rotation and panning with this. Anyway I will try your solution and get back to you. – Dinesh Raja Jun 02 '14 at 03:43
  • Hey Joe, A pinch is a pinch is a pinch. Well, Rotation is not a pinch. So you need to prevent rotation to get detected in a pinch gesture. Which means calculating distance will lead to auto detect the rotation as a PINCH gesture. Pinch is not a rotation and rotation is not a pinch. So you need to PREVENT detection of ROTATION as PINCH. Clear???? – Dinesh Raja Jun 02 '14 at 05:29
  • 1
    Prevention is a relative term. Failing the test is prevention enough. If the pinch gesture is failed, the user will learn not to rotate but keep the vectors straight enough. – Léo Natan Jun 02 '14 at 05:34
  • It's completely bizarre the client would want to exclude rotation/translation from a pinch. (So, the app will behave like no app has ever behaved!) BUT if you want exclude one gesture from another, no problem, Sandy has pointed the way .. canPreventGestureRecognizer. – Fattie Jun 02 '14 at 05:36
  • @JoeBlow Thanks. Now I understand what you meant. "If you put three fingers on the screen, and pinch, and at the same time happen to rotate, that is a pinch." Thanks for your patience and help. – Dinesh Raja Jun 02 '14 at 05:37
  • Ok. and do we need to fail the gesture, if user pans the screens with three fingers in same direction? I think that's not a pinch. So I am asking. – Dinesh Raja Jun 02 '14 at 05:40
  • 1
    "Same time happen to rotate" -> not pinch gesture. "Fingers moving in the same direction (=panning)" -> not pinch gesture. – Léo Natan Jun 02 '14 at 05:50
1

Simple subclass of UIGestureRecognizer. It calculates the relative triangular center of 3 points, and then calculates the average distance from that center, angle is not important. You then check the average distance in your Gesture Handler.

.h

#import <UIKit/UIKit.h>
#import <UIKit/UIGestureRecognizerSubclass.h>
@interface UnPinchGestureRecognizer : UIGestureRecognizer
 @property CGFloat averageDistanceFromCenter;
@end

.m

#import "UnPinchGestureRecognizer.h"
@implementation UnPinchGestureRecognizer

-(CGPoint)centerOf:(CGPoint)pnt1 pnt2:(CGPoint)pnt2 pnt3:(CGPoint)pnt3
{
    CGPoint center;
    center.x = (pnt1.x + pnt2.x + pnt3.x) / 3;
    center.y = (pnt1.y + pnt2.y + pnt3.y) / 3;
    return center;
}
-(CGFloat)averageDistanceFromCenter:(CGPoint)center pnt1:(CGPoint)pnt1 pnt2:(CGPoint)pnt2 pnt3:(CGPoint)pnt3
{
    CGFloat distance;
    distance = (sqrt(fabs(pnt1.x-center.x)*fabs(pnt1.x-center.x)+fabs(pnt1.y-center.y)*fabs(pnt1.y-center.y))+
                sqrt(fabs(pnt2.x-center.x)*fabs(pnt2.x-center.x)+fabs(pnt2.y-center.y)*fabs(pnt2.y-center.y))+
                sqrt(fabs(pnt3.x-center.x)*fabs(pnt3.x-center.x)+fabs(pnt3.y-center.y)*fabs(pnt3.y-center.y)))/3;
    return distance;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    if ([touches count] == 3) {
        [super touchesBegan:touches withEvent:event];
        NSArray *touchObjects = [touches allObjects];
        CGPoint pnt1 = [[touchObjects objectAtIndex:0] locationInView:self.view];
        CGPoint pnt2 = [[touchObjects objectAtIndex:1] locationInView:self.view];
        CGPoint pnt3 = [[touchObjects objectAtIndex:2] locationInView:self.view];

        CGPoint center = [self centerOf:pnt1 pnt2:pnt2 pnt3:pnt3];
        self.averageDistanceFromCenter = [self averageDistanceFromCenter:center pnt1:pnt1 pnt2:pnt2 pnt3:pnt3];
        self.state = UIGestureRecognizerStateBegan;
    }
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {

    if ([touches count] == 3)
    {
        NSArray *touchObjects = [touches allObjects];
        CGPoint pnt1 = [[touchObjects objectAtIndex:0] locationInView:self.view];
        CGPoint pnt2 = [[touchObjects objectAtIndex:1] locationInView:self.view];
        CGPoint pnt3 = [[touchObjects objectAtIndex:2] locationInView:self.view];

        CGPoint center = [self centerOf:pnt1 pnt2:pnt2 pnt3:pnt3];
        self.averageDistanceFromCenter = [self averageDistanceFromCenter:center pnt1:pnt1 pnt2:pnt2 pnt3:pnt3];
        self.state = UIGestureRecognizerStateChanged;
        return;
    }

}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    self.state = UIGestureRecognizerStateEnded;
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    self.state = UIGestureRecognizerStateFailed;
}

@end

implementation of Gesture, I have a max avg distance set to start, and then a minimum to end, you can also check during changed as well:

-(IBAction)handleUnPinch:(UnPinchGestureRecognizer *)sender
{
    switch (sender.state) {
        case UIGestureRecognizerStateBegan:
            //If you want a maximum starting distance
            self.validPinch = (sender.averageDistanceFromCenter<75);
            break;
        case UIGestureRecognizerStateEnded:
            //Minimum distance from relative center
            if (self.validPinch && sender.averageDistanceFromCenter >=150) {
                NSLog(@"successful unpinch");
            }
            break;
        default:
            break;
    }
}
hybrdthry911
  • 1,259
  • 7
  • 12
  • This is as pointless as calculating the area of a square ... by first calculating the diagonal! It would be pointless. Distances from center is the same calculation as just sum of the distances. By the way in 3D packages you often have **area of a triangle**. Once again, that's the same thing. You can simply use "area of the triangle" and check if that is expanding. – Fattie Jun 05 '14 at 06:15
  • Definitely not pointless, it works! Not even close to the same calculation. Distance from center does not correlate to the area at all. You can have the same distance from center, and have VERY different areas. In this case, I think average distance from center point is the best way to go, and not area. – hybrdthry911 Jun 05 '14 at 11:42
  • No problem, don't worry about it. Regarding YOUR calculation. I encourage you to try it simply calculating the sum of the three distances, which is what is normally done to find pinches. Please try it and let me know. – Fattie Jun 05 '14 at 12:27
  • Regarding AREA. I encourage you to try it and let me know. (BTW "correlates" mean they change together. If the distance from the centre gets bigger, the area will get bigger.) – Fattie Jun 05 '14 at 12:29
  • "In this case, I think average distance from center point is the best way to go, and not area." You are wrong. All three methods **will perfectly detect a pinch**. There will be **utterly no difference** in the user experience. All three methods are trivial. Adding the distances is used because, "Of three extremely simple methods, it's the simplest!" Heh. **IF YOU HAPPEN TO HAVE** an area function on hand already (common in graphics/games situations), just use that - it will save typing seven characters on your keyboard. – Fattie Jun 05 '14 at 12:35
  • Purely for what it's worth. In gesture recognition when YOU DO HAVE TO track distance of a finger "from the center" (for some reason -- like with 3D shape formation apps). It's a little tricky, you must track it from **what 'was' the center** or it makes no sense to the user. {Also, obviously, static fingers will be "thought" by your app to be moving if the centre moves.} It's quite difficult to do and requires some good heuristics about when yoU "swap over." Purely FWIW. – Fattie Jun 05 '14 at 12:39
  • If my code will perfectly detect a pinch like you are stating, why are you arguing here. Not even arguing with me, you seem to be arguing with yourself. My code will detect an unpinch, and I prefer this method. I posted simple code, let Dinesh decide if it is what he needs. – hybrdthry911 Jun 05 '14 at 16:17