diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 9b0c6ae2fb7..5dc73de6d3c 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -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). diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d242feb38dd..3fc692f4e05 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -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 MGLAnnotationTagContextMap; @@ -267,6 +270,9 @@ @interface MGLMapView () 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; } } @@ -2296,23 +2325,17 @@ - (void)calloutViewDidAppear:(UIView *)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; } } @@ -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 - diff --git a/platform/ios/test/MGLMapViewPitchTests.m b/platform/ios/test/MGLMapViewPitchTests.m index b7b18973a15..3e9311dbd45 100644 --- a/platform/ios/test/MGLMapViewPitchTests.m +++ b/platform/ios/test/MGLMapViewPitchTests.m @@ -2,23 +2,35 @@ #import @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 @@ -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;