3
votes

I'm trying to create a gesture recognizer able to detect the rotation of 4 fingers (similar when you rotate a volume knob).
The main idea was to create a subclass of UIRotateGestureRecognizer and override its method. In the -touchesBegan I detect the number of touches, if the number is lower than 4 the state of the gesture is fail.
After that I pass the location point to an algorithm that find the diameter of a convex hull. If you think about it, your fingers are the vertices and I just need to find the two vertices with the max distance. Obtained these two points I reference them as ivar and I pass them to the superclass as it is a simple rotation with just two fingers.
It doesn't work:

  1. the detection of the touches seems pretty hard
  2. very rarely the -touchesHasMoved is called
  3. when its called it hangs the most of time

Can someone help me?

Here is the code:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (touches.count<4) {
        //FAIL
        self.state = UIGestureRecognizerStateFailed;
        return;
    }

    //Find the diameter of the convex hull
    NSArray * touchesArray = [touches allObjects];
    NSMutableArray * pointsArray = @[].mutableCopy;
    for (UITouch * touch in touchesArray) {
        [pointsArray addObject:[NSValue valueWithCGPoint:[touch locationInView:touch.view]]];
    }
    DiameterType convexHullDiameter = getDiameterFromPoints(pointsArray);
    CGPoint firstPoint =  convexHullDiameter.firstPoint;
    CGPoint secondPoint = convexHullDiameter.secondPoint;
    for (UITouch * touch in touchesArray) {
        if (CGPointEqualToPoint([touch locationInView:touch.view], firstPoint) ) {
            self.fistTouch = touch;
        }
        else if (CGPointEqualToPoint([touch locationInView:touch.view], secondPoint)){
            self.secondTouch = touch;
        }
    }
    //Calculating the rotation center as a mid point between the diameter vertices
    CGPoint rotationCenter = (CGPoint) {
        .x = (convexHullDiameter.firstPoint.x + convexHullDiameter.secondPoint.x)/2,
        .y = (convexHullDiameter.firstPoint.y + convexHullDiameter.secondPoint.y)/2
    };
    self.rotationCenter = rotationCenter;
    //Passing touches to super as a fake rotation gesture
    NSSet * touchesSet = [[NSSet alloc] initWithObjects:self.fistTouch, self.secondTouch, nil];
    [super touchesBegan:touchesSet withEvent:event];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    if (touches.count<4) {
        self.state = UIGestureRecognizerStateFailed;
        return;
    }

    [super touchesMoved:[[NSSet alloc] initWithObjects:self.fistTouch, self.secondTouch, nil] withEvent:event];
}

- (void) touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event  {
    [super touchesCancelled:[[NSSet alloc] initWithObjects:self.fistTouch, self.secondTouch, nil] withEvent:event];
}

- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event  {
    [super touchesEnded:[[NSSet alloc] initWithObjects:self.fistTouch, self.secondTouch, nil] withEvent:event];
}
3
Don't you can use 2 finger only and use the rotation/pitch gesture (default from ios)? I'm saing that because I use two finger to change volume level in a sound system, not 4. Probably I'll use more fingers on heavy things, like a hard tap water, p.ex.Ratata Tata
If I use just 2 fingers it wouldn't be the same thing from user perspective.Andrea
Maybe using "[[event allTouches] anyObject];" instead of "[touches allObjects];" helps a bit, I couldn't understand what your problem is exactly though.Mert Buran

3 Answers

2
votes

The reason initial detection is hard is that all the touches may not start at the same time. touchesBegan will likely be called multiple times as separate touches land on the screen. You can use the event parameter to query all of the current touches with event.allTouches. So your current approach for triggering the gesture to fail will not work. You should not set state to fail if touches.count is < 4 but instead just return if event.allTouches.count < 4. You could use a timer to set the state to fail if the fourth touch does not happen within a certain time from the first.

touchesMoved likely has problems because the touches in the event object do not match up with those in the set that you pass to super.

1
votes

If you think about it, your fingers are the vertices and I just need to find the two vertices with the max distance.

I don't think this will work in practice, even if you are able to trick the UIGestureRecognizer.

This is how I would implement the algorithm in the 'correct' way:

  1. Remember the 'old' touches.
  2. When you're given 'new' touches, try to match each finger to the previous touch. If you can't, fail.
  3. Compute the center of 'new' + 'old' touches.
  4. For each of 4 fingers identified two steps ago, compute angle traveled in radians, approximated as

    new(i) - old(i) divided by distance to center

  5. If any angle is too big (> 0.5), fail.

  6. This guarantees that approximation is valid.
  7. Now compute the average of 4 angles.

Congratulations, you now have the rotation angle (measured in radians).

0
votes

I would put this in a comment if I had enough Rep.

[super touchesMoved:[[NSSet alloc] initWithObjects:self.fistTouch, self.secondTouch, nil] withEvent:event];

You're using something called fistTouch, which doesn't sound like what you want. My guess is you want firstTouch.

Additionally there are possible collisions between gestures going on that may be overriding each other. Did you know there is a 4-finger zoom-out in iOS7 that is a system-wide gesture? Also, a 4-finger zoom-in during an app will close it.