Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[ios] Improve tilt gesture recognizer detection. #15349

Merged
merged 8 commits into from
Aug 24, 2019
4 changes: 4 additions & 0 deletions platform/ios/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Mapbox welcomes participation and contributions from everyone. Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) to get started.

## master

* Fixed an issue that caused the tilt gesture to trigger too easily and conflict with pinch or pan gestures. ([#15349](https://github.com/mapbox/mapbox-gl-native/pull/15349))

## 5.3.0

This release changes how offline tile requests are billed — they are now billed on a pay-as-you-go basis and all developers are able raise the offline tile limit for their users. Offline requests were previously exempt from monthly active user (MAU) billing and increasing the offline per-user tile limit to more than 6,000 tiles required the purchase of an enterprise license. By upgrading to this release, you are opting into the changes outlined in [this blog post](https://blog.mapbox.com/offline-maps-for-all-bb0fc51827be) and [#15380](https://github.com/mapbox/mapbox-gl-native/pull/15380).
Expand Down
93 changes: 61 additions & 32 deletions platform/ios/src/MGLMapView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,9 @@ typedef NS_ENUM(NSUInteger, MGLUserTrackingState) {
/// An indication that the requested annotation was not found or is nonexistent.
enum { MGLAnnotationTagNotFound = UINT32_MAX };

/// The threshold used to consider when a tilt gesture should start.
const CLLocationDegrees MGLHorizontalTiltToleranceDegrees = 45.0;

/// Mapping from an annotation tag to metadata about that annotation, including
/// the annotation itself.
typedef std::unordered_map<MGLAnnotationTag, MGLAnnotationContext> MGLAnnotationTagContextMap;
Expand Down Expand Up @@ -267,6 +270,9 @@ @interface MGLMapView () <UIGestureRecognizerDelegate,
@property (nonatomic) MGLMapDebugMaskOptions residualDebugMask;
@property (nonatomic, copy) NSURL *residualStyleURL;

/// Tilt gesture recognizer helper
friedbunny marked this conversation as resolved.
Show resolved Hide resolved
@property (nonatomic, assign) CGPoint dragGestureMiddlePoint;

- (mbgl::Map &)mbglMap;

@end
Expand Down Expand Up @@ -2139,6 +2145,14 @@ - (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag

if (twoFingerDrag.state == UIGestureRecognizerStateBegan)
{
CGPoint midPoint = [twoFingerDrag translationInView:twoFingerDrag.view];
// In the following if and for the first execution middlePoint
// will be equal to dragGestureMiddlePoint and the resulting
// gestureSlopeAngle will be 0º causing a small delay,
// initializing dragGestureMiddlePoint with the current midPoint
// but substracting one point from 'y' forces an initial 90º angle
// making the gesture avoid the delay
self.dragGestureMiddlePoint = CGPointMake(midPoint.x, midPoint.y-1);
friedbunny marked this conversation as resolved.
Show resolved Hide resolved
initialPitch = *self.mbglMap.getCameraOptions().pitch;
[self notifyGestureDidBegin];
}
Expand All @@ -2150,30 +2164,45 @@ - (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag
twoFingerDrag.state = UIGestureRecognizerStateEnded;
return;
}

CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y;
CGFloat slowdown = 2.0;

CGFloat pitchNew = initialPitch - (gestureDistance / slowdown);

CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag];

MGLMapCamera *oldCamera = self.camera;
MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew];

if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
{
self.mbglMap.jumpTo(mbgl::CameraOptions()

CGPoint leftTouchPoint = [twoFingerDrag locationOfTouch:0 inView:twoFingerDrag.view];
CGPoint rightTouchPoint = [twoFingerDrag locationOfTouch:1 inView:twoFingerDrag.view];
CLLocationDegrees fingerSlopeAngle = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint];

CGPoint middlePoint = [twoFingerDrag translationInView:twoFingerDrag.view];

CLLocationDegrees gestureSlopeAngle = [self angleBetweenPoints:self.dragGestureMiddlePoint endPoint:middlePoint];
self.dragGestureMiddlePoint = middlePoint;
if (fabs(fingerSlopeAngle) < MGLHorizontalTiltToleranceDegrees && fabs(gestureSlopeAngle) > 60.0 ) {

CGFloat gestureDistance = middlePoint.y;
CGFloat slowdown = 2.0;

CGFloat pitchNew = initialPitch - (gestureDistance / slowdown);

CGPoint centerPoint = [self anchorPointForGesture:twoFingerDrag];

MGLMapCamera *oldCamera = self.camera;
MGLMapCamera *toCamera = [self cameraByTiltingToPitch:pitchNew];

if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera])
{
self.mbglMap.jumpTo(mbgl::CameraOptions()
.withPitch(pitchNew)
.withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }));
}

[self cameraIsChanging];

}

[self cameraIsChanging];

}
else if (twoFingerDrag.state == UIGestureRecognizerStateEnded || twoFingerDrag.state == UIGestureRecognizerStateCancelled)
{
[self notifyGestureDidEndWithDrift:NO];
[self unrotateIfNeededForGesture];
self.dragGestureMiddlePoint = CGPointZero;
}

}
Expand Down Expand Up @@ -2296,23 +2325,17 @@ - (void)calloutViewDidAppear:(UIView<MGLCalloutView> *)calloutView

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
if (gestureRecognizer == _twoFingerDrag)
{
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;

if (panGesture.minimumNumberOfTouches == 2)
{
CGPoint west = [panGesture locationOfTouch:0 inView:panGesture.view];
CGPoint east = [panGesture locationOfTouch:1 inView:panGesture.view];

if (west.x > east.x) {
CGPoint swap = west;
west = east;
east = swap;
}
CGPoint leftTouchPoint = [panGesture locationOfTouch:0 inView:panGesture.view];
CGPoint rightTouchPoint = [panGesture locationOfTouch:1 inView:panGesture.view];

CLLocationDegrees horizontalToleranceDegrees = 60.0;
if ([self angleBetweenPoints:west east:east] > horizontalToleranceDegrees) {
CLLocationDegrees degrees = [self angleBetweenPoints:leftTouchPoint endPoint:rightTouchPoint];
if (fabs(degrees) > MGLHorizontalTiltToleranceDegrees) {
return NO;
}
}
Expand All @@ -2334,18 +2357,24 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ];

return ([validSimultaneousGestures containsObject:gestureRecognizer] && [validSimultaneousGestures containsObject:otherGestureRecognizer]);
}

- (CLLocationDegrees)angleBetweenPoints:(CGPoint)west east:(CGPoint)east
- (CLLocationDegrees)angleBetweenPoints:(CGPoint)originPoint endPoint:(CGPoint)endPoint
{
CGFloat slope = (west.y - east.y) / (west.x - east.x);
if (originPoint.x > endPoint.x) {
CGPoint swap = originPoint;
originPoint = endPoint;
endPoint = swap;
}

CGFloat x = (endPoint.x - originPoint.x);
CGFloat y = (endPoint.y - originPoint.y);

CGFloat angle = atan(fabs(slope));
CLLocationDegrees degrees = MGLDegreesFromRadians(angle);
CGFloat angleInRadians = atan2(y, x);
CLLocationDegrees angleInDegrees = MGLDegreesFromRadians(angleInRadians);

return degrees;
return angleInDegrees;
}

#pragma mark - Attribution -
Expand Down
39 changes: 35 additions & 4 deletions platform/ios/test/MGLMapViewPitchTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,35 @@
#import <XCTest/XCTest.h>

@interface MockUIPanGestureRecognizer : UIPanGestureRecognizer
@property CGFloat mbx_tiltGestureYTranslation;
@property NSUInteger mbx_numberOfFingersForGesture;
@property CGPoint mbx_middlePoint;
@property CGPoint mbx_westPoint;
@property CGPoint mbx_eastPoint;
@end

@implementation MockUIPanGestureRecognizer
- (instancetype)initWithTarget:(id)target action:(SEL)action {
if (self = [super initWithTarget:target action:action]) {
self.mbx_tiltGestureYTranslation = 0;
self.mbx_numberOfFingersForGesture = 2;
self.mbx_westPoint = CGPointMake(100, 0);
self.mbx_eastPoint = CGPointMake(200, 0);
}
return self;
}
- (NSUInteger)numberOfTouches { return self.mbx_numberOfFingersForGesture; }
- (CGPoint)translationInView:(UIView *)view { return CGPointMake(0, self.mbx_tiltGestureYTranslation); }
- (CGPoint)translationInView:(UIView *)view { return self.mbx_middlePoint; }
- (CGPoint)locationOfTouch:(NSUInteger)touchIndex inView:(UIView *)view {
if (touchIndex == 0) {
return self.mbx_westPoint;
}
return self.mbx_eastPoint;
}
- (void)setTiltGestureYTranslationForPitchDegrees:(CGFloat)degrees {
// The tilt gesture takes the number of screen points the fingers have moved and then divides them by a "slowdown" factor, which happens to be set to 2.0 in -[MGLMapView handleTwoFingerDragGesture:].
self.mbx_tiltGestureYTranslation = -(degrees * 2.0);
CGFloat mbx_tiltGestureYTranslation = -(degrees * 2.0);
self.mbx_westPoint = CGPointMake(self.mbx_westPoint.x, mbx_tiltGestureYTranslation);
self.mbx_eastPoint = CGPointMake(self.mbx_eastPoint.x, mbx_tiltGestureYTranslation);
self.mbx_middlePoint = CGPointMake(self.mbx_middlePoint.x, mbx_tiltGestureYTranslation);
}
@end

Expand Down Expand Up @@ -112,6 +124,25 @@ - (void)testTiltGesture {
}
}

- (void)testHorizontalTiltGesture {
MockUIPanGestureRecognizer *gesture = [[MockUIPanGestureRecognizer alloc] initWithTarget:self.mapView action:nil];
gesture.state = UIGestureRecognizerStateBegan;
[self.mapView handleTwoFingerDragGesture:gesture];
XCTAssertEqual(self.mapView.camera.pitch, 0, @"Pitch should initially be set to 0°.");

// Tilt gestures should not be triggered on horizontal dragging.
for (NSInteger deltaX = 0; deltaX < 5; deltaX++) {
gesture.mbx_westPoint = CGPointMake(100 - deltaX, 100);
gesture.mbx_eastPoint = CGPointMake(100 - deltaX, 50);
gesture.mbx_middlePoint = CGPointMake((gesture.mbx_westPoint.x + gesture.mbx_westPoint.x)/2, (gesture.mbx_westPoint.y + gesture.mbx_westPoint.y)/2);

gesture.state = UIGestureRecognizerStateChanged;

[self.mapView handleTwoFingerDragGesture:gesture];
XCTAssertEqual(self.mapView.camera.pitch, 0, @"Horizontal dragging should not tilt the map.");
}
}

- (void)testTiltGestureFromInitialTilt {
CGFloat initialTilt = 20;
CGFloat additionalTilt = 30;
Expand Down