diff options
author | Jordan Kiley <jmkiley@users.noreply.github.com> | 2019-08-20 22:12:08 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-20 22:12:08 -0700 |
commit | 6f78560f249ef057f95d16fb1f55de6d97295681 (patch) | |
tree | 88b6e22d176dba3b8ccc7f48bfe134cb25bb8b5e | |
parent | 2f43a7b87b7d12298318cb6611ac9a7ef8868b09 (diff) | |
download | qtlocation-mapboxgl-6f78560f249ef057f95d16fb1f55de6d97295681.tar.gz |
[ios] Add threshold for triggering the rotate gesture recognizer (#14929)
-rw-r--r-- | platform/ios/ios.xcodeproj/project.pbxproj | 16 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 147 | ||||
-rw-r--r-- | platform/ios/test/MGLMapViewDirectionTests.mm | 7 | ||||
-rw-r--r-- | platform/ios/test/MGLMapViewZoomTests.mm (renamed from platform/ios/test/MGLMapViewZoomTests.m) | 68 | ||||
-rw-r--r-- | platform/ios/test/MGLMockGestureRecognizers.h | 10 | ||||
-rw-r--r-- | platform/ios/test/MGLMockGestureRecognizers.m | 11 |
6 files changed, 229 insertions, 30 deletions
diff --git a/platform/ios/ios.xcodeproj/project.pbxproj b/platform/ios/ios.xcodeproj/project.pbxproj index 49043bdeee..5a5cb00b49 100644 --- a/platform/ios/ios.xcodeproj/project.pbxproj +++ b/platform/ios/ios.xcodeproj/project.pbxproj @@ -388,7 +388,7 @@ 9680274022653B84006BA4A1 /* MBXSKUToken.h in Headers */ = {isa = PBXBuildFile; fileRef = 9680273E22653B84006BA4A1 /* MBXSKUToken.h */; }; 9680276422655696006BA4A1 /* libmbxaccounts.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9680274122653C3E006BA4A1 /* libmbxaccounts.a */; }; 96802766226556C5006BA4A1 /* libmbxaccounts.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9680274122653C3E006BA4A1 /* libmbxaccounts.a */; }; - 9686D1BD22D9357700194EA0 /* MGLMapViewZoomTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.m */; }; + 9686D1BD22D9357700194EA0 /* MGLMapViewZoomTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.mm */; }; 968F36B51E4D128D003A5522 /* MGLDistanceFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3557F7AE1E1D27D300CCA5E6 /* MGLDistanceFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96E027231E57C76E004B8E66 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 96E027251E57C76E004B8E66 /* Localizable.strings */; }; 96E516DC2000547000A02306 /* MGLPolyline_Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9654C1251FFC1AB900DB6A19 /* MGLPolyline_Private.h */; }; @@ -481,6 +481,8 @@ 9C6E284522A982670056B7BE /* MMEUINavigation.h in Headers */ = {isa = PBXBuildFile; fileRef = 406E99B31FFEFED600D9FFCC /* MMEUINavigation.h */; }; 9C6E284622A982670056B7BE /* MMEUniqueIdentifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 40834BBF1FE05D6E00C1BD0D /* MMEUniqueIdentifier.h */; }; 9C6E284722A982670056B7BE /* MMEDispatchManager.h in Headers */ = {isa = PBXBuildFile; fileRef = ACA65F552140696B00537748 /* MMEDispatchManager.h */; }; + A4DE3DCB23038C98005B3473 /* MGLMockGestureRecognizers.h in Sources */ = {isa = PBXBuildFile; fileRef = A4DE3DCA23038A7F005B3473 /* MGLMockGestureRecognizers.h */; }; + A4DE3DCC23038CCA005B3473 /* MGLMockGestureRecognizers.m in Sources */ = {isa = PBXBuildFile; fileRef = A4DE3DC823038A07005B3473 /* MGLMockGestureRecognizers.m */; }; A4F3FB1D2254865900A30170 /* missing_icon.json in Resources */ = {isa = PBXBuildFile; fileRef = A4F3FB1C2254865900A30170 /* missing_icon.json */; }; AC46EB59225E600A0039C013 /* MMECertPin.h in Headers */ = {isa = PBXBuildFile; fileRef = AC46EB57225E60090039C013 /* MMECertPin.h */; }; AC46EB5A225E600A0039C013 /* MMECertPin.h in Headers */ = {isa = PBXBuildFile; fileRef = AC46EB57225E60090039C013 /* MMECertPin.h */; }; @@ -1129,7 +1131,7 @@ 967C864A210A9D3C004DF794 /* UIDevice+MGLAdditions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+MGLAdditions.m"; sourceTree = "<group>"; }; 9680273E22653B84006BA4A1 /* MBXSKUToken.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MBXSKUToken.h; path = "../vendor/mapbox-accounts-ios/MBXSKUToken.h"; sourceTree = "<group>"; }; 9680274122653C3E006BA4A1 /* libmbxaccounts.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmbxaccounts.a; path = "vendor/mapbox-accounts-ios/libmbxaccounts.a"; sourceTree = SOURCE_ROOT; }; - 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MGLMapViewZoomTests.m; sourceTree = "<group>"; }; + 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MGLMapViewZoomTests.mm; sourceTree = "<group>"; }; 968F36B41E4D0FC6003A5522 /* ja */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; 96E027241E57C76E004B8E66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = "<group>"; }; 96E027271E57C77A004B8E66 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; @@ -1173,6 +1175,8 @@ 9C6E286522A9849E0056B7BE /* release-notes-jazzy.md.ejs */ = {isa = PBXFileReference; lastKnownFileType = text; path = "release-notes-jazzy.md.ejs"; sourceTree = "<group>"; }; 9C6E286622A9849E0056B7BE /* deploy-snapshot.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "deploy-snapshot.sh"; sourceTree = "<group>"; }; 9C6E286722A9849E0056B7BE /* release-notes.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "release-notes.js"; sourceTree = "<group>"; }; + A4DE3DC823038A07005B3473 /* MGLMockGestureRecognizers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MGLMockGestureRecognizers.m; sourceTree = "<group>"; }; + A4DE3DCA23038A7F005B3473 /* MGLMockGestureRecognizers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MGLMockGestureRecognizers.h; sourceTree = "<group>"; }; A4F3FB1C2254865900A30170 /* missing_icon.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = missing_icon.json; sourceTree = "<group>"; }; AC46EB57225E60090039C013 /* MMECertPin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MMECertPin.h; sourceTree = "<group>"; }; AC46EB58225E60090039C013 /* MMECertPin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMECertPin.m; sourceTree = "<group>"; }; @@ -1744,6 +1748,8 @@ children = ( CA5E5042209BDC5F001A8A81 /* MGLTestUtility.h */, 4031ACFE1E9FD29F00A3EA26 /* MGLSDKTestHelpers.swift */, + A4DE3DCA23038A7F005B3473 /* MGLMockGestureRecognizers.h */, + A4DE3DC823038A07005B3473 /* MGLMockGestureRecognizers.m */, ); name = "Test Helpers"; sourceTree = "<group>"; @@ -2057,7 +2063,7 @@ 96381C0122C6F3950053497D /* MGLMapViewPitchTests.m */, 9658C154204761FC00D8A674 /* MGLMapViewScaleBarTests.m */, 076171C22139C70900668A35 /* MGLMapViewTests.m */, - 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.m */, + 9686D1BC22D9357700194EA0 /* MGLMapViewZoomTests.mm */, 1F95931C1E6DE2E900D5B294 /* MGLNSDateAdditionsTests.mm */, 96036A0520059BBA00510F3D /* MGLNSOrthographyAdditionsTests.m */, DAE7DEC11E245455007505A6 /* MGLNSStringAdditionsTests.m */, @@ -3236,6 +3242,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A4DE3DCC23038CCA005B3473 /* MGLMockGestureRecognizers.m in Sources */, + A4DE3DCB23038C98005B3473 /* MGLMockGestureRecognizers.h in Sources */, 6407D6701E0085FD00F6A9C3 /* MGLDocumentationExampleTests.swift in Sources */, DA2E88631CC0382C00F24E7B /* MGLOfflineRegionTests.m in Sources */, 409F43FD1E9E781C0048729D /* MGLMapViewDelegateIntegrationTests.swift in Sources */, @@ -3273,7 +3281,7 @@ 3575798B1D502B0C000B822E /* MGLBackgroundStyleLayerTests.mm in Sources */, 9658C155204761FC00D8A674 /* MGLMapViewScaleBarTests.m in Sources */, 409D0A0D1ED614CE00C95D0C /* MGLAnnotationViewIntegrationTests.swift in Sources */, - 9686D1BD22D9357700194EA0 /* MGLMapViewZoomTests.m in Sources */, + 9686D1BD22D9357700194EA0 /* MGLMapViewZoomTests.mm in Sources */, DA2E88621CC0382C00F24E7B /* MGLOfflinePackTests.m in Sources */, 55E2AD131E5B125400E8C587 /* MGLOfflineStorageTests.mm in Sources */, 07D8C6FF1F67562C00381808 /* MGLComputedShapeSourceTests.m in Sources */, diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index d77f94d8ba..51102233cf 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -128,6 +128,9 @@ const CLLocationDirection MGLToleranceForSnappingToNorth = 7; /// Distance threshold to stop the camera while animating. const CLLocationDistance MGLDistanceThresholdForCameraPause = 500; +/// Rotation threshold while a pinch gesture is occurring. +static NSString * const MGLRotationThresholdWhileZoomingKey = @"MGLRotationThresholdWhileZooming"; + /// Reuse identifier and file name of the default point annotation image. static NSString * const MGLDefaultStyleMarkerSymbolName = @"default_marker"; @@ -240,6 +243,10 @@ public: @property (nonatomic) CGFloat quickZoomStart; @property (nonatomic, getter=isDormant) BOOL dormant; @property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed; +@property (nonatomic) CGFloat rotationThresholdWhileZooming; +@property (nonatomic) CGFloat rotationBeforeThresholdMet; +@property (nonatomic) BOOL isZooming; +@property (nonatomic) BOOL isRotating; @property (nonatomic) BOOL shouldTriggerHapticFeedbackForCompass; @property (nonatomic) MGLMapViewProxyAccessibilityElement *mapViewProxyAccessibilityElement; @property (nonatomic) MGLAnnotationContainerView *annotationContainerView; @@ -1622,6 +1629,9 @@ public: { self.scale = powf(2, [self zoomLevel]); + if (abs(pinch.velocity) > abs(self.rotate.velocity)) { + self.isZooming = YES; + } [self notifyGestureDidBegin]; } else if (pinch.state == UIGestureRecognizerStateChanged) @@ -1697,6 +1707,7 @@ public: } } + self.isZooming = NO; [self notifyGestureDidEndWithDrift:drift]; [self unrotateIfNeededForGesture]; } @@ -1709,6 +1720,106 @@ public: { if ( ! self.isRotateEnabled) return; + if ([[NSUserDefaults standardUserDefaults] objectForKey:MGLRotationThresholdWhileZoomingKey]) { + [self handleRotateGestureRecognizerWithThreshold:rotate]; + } else { + [self cancelTransitions]; + + CGPoint centerPoint = [self anchorPointForGesture:rotate]; + MGLMapCamera *oldCamera = self.camera; + + self.cameraChangeReasonBitmask |= MGLCameraChangeReasonGestureRotate; + + if (rotate.state == UIGestureRecognizerStateBegan) + { + self.angle = MGLRadiansFromDegrees(*self.mbglMap.getCameraOptions().bearing) * -1; + + self.isRotating = YES; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + self.userTrackingMode = MGLUserTrackingModeFollow; + } + + self.shouldTriggerHapticFeedbackForCompass = NO; + [self notifyGestureDidBegin]; + } + if (rotate.state == UIGestureRecognizerStateChanged) + { + CGFloat newDegrees = MGLDegreesFromRadians(self.angle + rotate.rotation) * -1; + + // constrain to +/-30 degrees when merely rotating like Apple does + // + if ( ! self.isRotationAllowed && std::abs(self.pinch.scale) < 10) + { + newDegrees = fminf(newDegrees, 30); + newDegrees = fmaxf(newDegrees, -30); + } + + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + self.mbglMap.jumpTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); + } + + [self cameraIsChanging]; + + // Trigger a light haptic feedback event when the user rotates to due north. + if (@available(iOS 10.0, *)) + { + if (self.isHapticFeedbackEnabled && fabs(newDegrees) <= 1 && self.shouldTriggerHapticFeedbackForCompass) + { + UIImpactFeedbackGenerator *hapticFeedback = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight]; + [hapticFeedback impactOccurred]; + + self.shouldTriggerHapticFeedbackForCompass = NO; + } + else if (fabs(newDegrees) > 1) + { + self.shouldTriggerHapticFeedbackForCompass = YES; + } + } + } + else if ((rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled)) + { + CGFloat velocity = rotate.velocity; + CGFloat decelerationRate = self.decelerationRate; + if (decelerationRate != MGLMapViewDecelerationRateImmediate && fabs(velocity) > 3) + { + CGFloat radians = self.angle + rotate.rotation; + CGFloat newRadians = radians + velocity * decelerationRate * 0.1; + CGFloat newDegrees = MGLDegreesFromRadians(newRadians) * -1; + + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; + + if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) + { + self.mbglMap.easeTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), + MGLDurationFromTimeInterval(decelerationRate)); + + [self notifyGestureDidEndWithDrift:YES]; + __weak MGLMapView *weakSelf = self; + + [self animateWithDelay:decelerationRate animations:^ + { + [weakSelf unrotateIfNeededForGesture]; + }]; + } + } + else + { + [self notifyGestureDidEndWithDrift:NO]; + [self unrotateIfNeededForGesture]; + } + } + } +} + +- (void)handleRotateGestureRecognizerWithThreshold:(UIRotationGestureRecognizer *)rotate { [self cancelTransitions]; CGPoint centerPoint = [self anchorPointForGesture:rotate]; @@ -1716,20 +1827,29 @@ public: self.cameraChangeReasonBitmask |= MGLCameraChangeReasonGestureRotate; - if (rotate.state == UIGestureRecognizerStateBegan) + _rotationThresholdWhileZooming = [[[NSUserDefaults standardUserDefaults] objectForKey:MGLRotationThresholdWhileZoomingKey] floatValue]; + + // Check whether a zoom triggered by a pinch gesture is occurring and if the rotation threshold has been met. + if (MGLDegreesFromRadians(self.rotationBeforeThresholdMet) < self.rotationThresholdWhileZooming && self.isZooming && !self.isRotating) { + self.rotationBeforeThresholdMet += fabs(rotate.rotation); + rotate.rotation = 0; + return; + } + + if (rotate.state == UIGestureRecognizerStateBegan || ! self.isRotating) { self.angle = MGLRadiansFromDegrees(*self.mbglMap.getCameraOptions().bearing) * -1; + self.isRotating = YES; if (self.userTrackingMode != MGLUserTrackingModeNone) { self.userTrackingMode = MGLUserTrackingModeFollow; } self.shouldTriggerHapticFeedbackForCompass = NO; - [self notifyGestureDidBegin]; } - else if (rotate.state == UIGestureRecognizerStateChanged) + if (rotate.state == UIGestureRecognizerStateChanged) { CGFloat newDegrees = MGLDegreesFromRadians(self.angle + rotate.rotation) * -1; @@ -1740,14 +1860,14 @@ public: newDegrees = fminf(newDegrees, 30); newDegrees = fmaxf(newDegrees, -30); } - + MGLMapCamera *toCamera = [self cameraByRotatingToDirection:newDegrees aroundAnchorPoint:centerPoint]; if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { - self.mbglMap.jumpTo(mbgl::CameraOptions() - .withBearing(newDegrees) - .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); + self.mbglMap.jumpTo(mbgl::CameraOptions() + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y})); } [self cameraIsChanging]; @@ -1768,8 +1888,12 @@ public: } } } - else if (rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled) + else if ((rotate.state == UIGestureRecognizerStateEnded || rotate.state == UIGestureRecognizerStateCancelled)) { + self.rotationBeforeThresholdMet = 0; + if (! self.isRotating) { return; } + self.isRotating = NO; + CGFloat velocity = rotate.velocity; CGFloat decelerationRate = self.decelerationRate; if (decelerationRate != MGLMapViewDecelerationRateImmediate && fabs(velocity) > 3) @@ -1783,14 +1907,13 @@ public: if ([self _shouldChangeFromCamera:oldCamera toCamera:toCamera]) { self.mbglMap.easeTo(mbgl::CameraOptions() - .withBearing(newDegrees) - .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), + .withBearing(newDegrees) + .withAnchor(mbgl::ScreenCoordinate { centerPoint.x, centerPoint.y }), MGLDurationFromTimeInterval(decelerationRate)); [self notifyGestureDidEndWithDrift:YES]; - __weak MGLMapView *weakSelf = self; - + [self animateWithDelay:decelerationRate animations:^ { [weakSelf unrotateIfNeededForGesture]; diff --git a/platform/ios/test/MGLMapViewDirectionTests.mm b/platform/ios/test/MGLMapViewDirectionTests.mm index 8a724a06bc..81e169432b 100644 --- a/platform/ios/test/MGLMapViewDirectionTests.mm +++ b/platform/ios/test/MGLMapViewDirectionTests.mm @@ -1,5 +1,6 @@ #import <Mapbox/Mapbox.h> #import <XCTest/XCTest.h> +#import "MGLMockGestureRecognizers.h" #import <mbgl/math/wrap.hpp> @@ -8,12 +9,6 @@ - (void)resetNorthAnimated:(BOOL)animated; @end -@interface UIRotationGestureRecognizerMock : UIRotationGestureRecognizer -@end - -@implementation UIRotationGestureRecognizerMock -- (CGPoint)locationInView:(nullable UIView*)view { return view.center; } -@end @interface MGLMapViewDirectionTests : XCTestCase @property (nonatomic) MGLMapView *mapView; diff --git a/platform/ios/test/MGLMapViewZoomTests.m b/platform/ios/test/MGLMapViewZoomTests.mm index bd617857fd..360af72d02 100644 --- a/platform/ios/test/MGLMapViewZoomTests.m +++ b/platform/ios/test/MGLMapViewZoomTests.mm @@ -1,16 +1,14 @@ #import <Mapbox/Mapbox.h> #import <XCTest/XCTest.h> +#import "MGLMockGestureRecognizers.h" + +#import <mbgl/math/wrap.hpp> @interface MGLMapView (MGLMapViewZoomTests) +@property (nonatomic) BOOL isZooming; +@property (nonatomic) CGFloat rotationThresholdWhileZooming; - (void)handlePinchGesture:(UIPinchGestureRecognizer *)pinch; -@end - -@interface UIPinchGestureRecognizerMock : UIPinchGestureRecognizer -@property (nonatomic) CGPoint locationInViewOverride; -@end - -@implementation UIPinchGestureRecognizerMock -- (CGPoint)locationInView:(nullable UIView *)view { return self.locationInViewOverride; } +- (void)handleRotateGesture:(UIRotationGestureRecognizer *)rotate; @end @interface MGLMapViewZoomTests : XCTestCase @@ -129,6 +127,60 @@ XCTAssertEqualWithAccuracy(centerCoordinateBeforeReset.longitude, self.mapView.centerCoordinate.longitude, 0.0000001, @"Map center coordinate longitude should remain constant after resetting to north."); } +- (void)testPinchAndZoom { + + [[NSUserDefaults standardUserDefaults] setObject:@3 forKey:@"MGLRotationThresholdWhileZooming"]; + self.mapView.rotationThresholdWhileZooming = 3; + self.mapView.zoomLevel = 15; + UIPinchGestureRecognizerMock *pinch = [[UIPinchGestureRecognizerMock alloc] initWithTarget:self.mapView action:nil]; + [self.mapView addGestureRecognizer:pinch]; + pinch.state = UIGestureRecognizerStateBegan; + pinch.velocity = 5.0; + pinch.locationInViewOverride = CGPointMake(0, 0); + [self.mapView handlePinchGesture:pinch]; + + XCTAssertTrue(self.mapView.isZooming); + + UIRotationGestureRecognizerMock *rotate = [[UIRotationGestureRecognizerMock alloc] initWithTarget:self.mapView action:nil]; + rotate.state = UIGestureRecognizerStateBegan; + rotate.rotation = MGLRadiansFromDegrees(1); + [self.mapView addGestureRecognizer:rotate]; + [self.mapView handleRotateGesture:rotate]; + + // Both the rotation and direction should be zero since the rotation threshold hasn't been met. + XCTAssertEqual(rotate.rotation, 0); + XCTAssertEqual(self.mapView.direction, 0); + + // The direction should be `0`. The default rotation threshold is `3`. + XCTAssertEqual(self.mapView.direction, 0); + rotate.state = UIGestureRecognizerStateChanged; + rotate.rotation = MGLRadiansFromDegrees(2); + [self.mapView handleRotateGesture:rotate]; + + // The direction should be `0`. The default rotation threshold is `3`. + XCTAssertEqual(self.mapView.direction, 0); + + for (NSNumber *degrees in @[@-90, @-10, @10, @10, @30, @90, @180, @240, @460, @500, @590, @800]) { + rotate.state = UIGestureRecognizerStateChanged; + rotate.rotation = MGLRadiansFromDegrees([degrees doubleValue]); + [self.mapView handleRotateGesture:rotate]; + + CGFloat wrappedRotation = mbgl::util::wrap(-MGLDegreesFromRadians(rotate.rotation), 0., 360.); + + + // Check that the direction property now matches the gesture's rotation. + XCTAssertEqualWithAccuracy(self.mapView.direction, wrappedRotation, 0.001, @"Map direction should match gesture rotation for input of %@°.", degrees); + } + + rotate.state = UIGestureRecognizerStateEnded; + pinch.state = UIGestureRecognizerStateEnded; + + [self.mapView handleRotateGesture:rotate]; + [self.mapView handlePinchGesture:pinch]; + + XCTAssertFalse(self.mapView.isZooming); +} + NS_INLINE CGFloat MGLScaleFromZoomLevel(double zoom) { return pow(2, zoom); } diff --git a/platform/ios/test/MGLMockGestureRecognizers.h b/platform/ios/test/MGLMockGestureRecognizers.h new file mode 100644 index 0000000000..aa5fbec494 --- /dev/null +++ b/platform/ios/test/MGLMockGestureRecognizers.h @@ -0,0 +1,10 @@ + +#import <UIKit/UIKit.h> + +@interface UIPinchGestureRecognizerMock : UIPinchGestureRecognizer +@property (nonatomic, readwrite) CGFloat velocity; +@property (nonatomic) CGPoint locationInViewOverride; +@end + +@interface UIRotationGestureRecognizerMock : UIRotationGestureRecognizer +@end diff --git a/platform/ios/test/MGLMockGestureRecognizers.m b/platform/ios/test/MGLMockGestureRecognizers.m new file mode 100644 index 0000000000..89df6750a9 --- /dev/null +++ b/platform/ios/test/MGLMockGestureRecognizers.m @@ -0,0 +1,11 @@ + +#import "MGLMockGestureRecognizers.h" + +@implementation UIPinchGestureRecognizerMock +@synthesize velocity; +- (CGPoint)locationInView:(nullable UIView *)view { return self.locationInViewOverride; } +@end + +@implementation UIRotationGestureRecognizerMock +- (CGPoint)locationInView:(nullable UIView*)view { return view.center; } +@end |