diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f324755b70..62781118cd5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Mapbox welcomes participation and contributions from everyone. * Set `MapboxMap` flags during gestures and animations. ([#754](https://github.com/mapbox/mapbox-maps-ios/pull/754)) * Treat anchor as constant for `ease(to:)` animations. ([#772](https://github.com/mapbox/mapbox-maps-ios/pull/772)) * Fix experimental snapshot API for iOS 15. ([#760](https://github.com/mapbox/mapbox-maps-ios/pull/760)) +* Decelerate more quickly (or not at all) on pitched maps. ([#773](https://github.com/mapbox/mapbox-maps-ios/pull/773)) ## 10.0.1 - October 15, 2021 diff --git a/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift b/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift index d5967d9d6424..4210be6fb82a 100644 --- a/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift +++ b/Sources/MapboxMaps/Gestures/GestureHandlers/PanGestureHandler.swift @@ -74,17 +74,21 @@ internal final class PanGestureHandler: GestureHandler, PanGestureHandlerProtoco // it without further dragging. This specific time interval // is just the result of manual tuning. let decelerationTimeout: TimeInterval = 1.0 / 30.0 + let maxPitchForDeceleration: CGFloat = 60 guard let initialTouchLocation = initialTouchLocation, let initialCameraState = initialCameraState, let lastChangedDate = lastChangedDate, - dateProvider.now.timeIntervalSince(lastChangedDate) < decelerationTimeout else { + dateProvider.now.timeIntervalSince(lastChangedDate) < decelerationTimeout, + mapboxMap.cameraState.pitch <= maxPitchForDeceleration else { delegate?.gestureEnded(for: .pan, willAnimate: false) return } cameraAnimationsManager.decelerate( location: touchLocation, velocity: gestureRecognizer.velocity(in: view), - decelerationFactor: decelerationFactor, + // Decelerate more quickly when pitched. This is a workaround for an issue + // in the drag API that we hope to fix in MapboxCoreMaps in a future release. + decelerationFactor: decelerationFactor * (1 - mapboxMap.cameraState.pitch / 5000), locationChangeHandler: { (touchLocation) in // here we capture the initial state so that we can clear // it immediately after starting the animation diff --git a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift index dbaa8f2cb298..53600fcd35f7 100644 --- a/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift +++ b/Tests/MapboxMapsTests/Gestures/GestureHandlers/PanGestureHandlerTests.swift @@ -97,7 +97,7 @@ final class PanGestureHandlerTests: XCTestCase { clampedTouchLocation: clampedTouchLocation) } - func testHandlePanChangedWithHorizontalPanScrollingMode() throws { + func testHandlePanChangedWithHorizontalPanMode() throws { let initialTouchLocation = CGPoint.random() let changedTouchLocation = CGPoint.random() let clampedTouchLocation = CGPoint( @@ -111,7 +111,7 @@ final class PanGestureHandlerTests: XCTestCase { clampedTouchLocation: clampedTouchLocation) } - func testHandlePanChangedWithVerticalPanScrollingMode() throws { + func testHandlePanChangedWithVerticalPanMode() throws { let initialTouchLocation = CGPoint.random() let changedTouchLocation = CGPoint.random() let clampedTouchLocation = CGPoint( @@ -143,7 +143,8 @@ final class PanGestureHandlerTests: XCTestCase { line: UInt = #line) throws { panGestureHandler.panMode = panMode panGestureHandler.decelerationFactor = .random(in: 0.1...0.99) - let initialCameraState = CameraState.random() + var initialCameraState = CameraState.random() + initialCameraState.pitch = initialCameraState.pitch.clamped(to: 0...60) mapboxMap.cameraState = initialCameraState let endedTouchLocation = CGPoint.random() gestureRecognizer.locationStub.returnValueQueue = [ @@ -172,7 +173,7 @@ final class PanGestureHandlerTests: XCTestCase { let decelerateParams = cameraAnimationsManager.decelerateStub.parameters.first XCTAssertEqual(decelerateParams?.location, endedTouchLocation, line: line) XCTAssertEqual(decelerateParams?.velocity, velocity, line: line) - XCTAssertEqual(decelerateParams?.decelerationFactor, panGestureHandler.decelerationFactor, line: line) + XCTAssertEqual(decelerateParams?.decelerationFactor, panGestureHandler.decelerationFactor * (1 - initialCameraState.pitch / 5000), line: line) let locationChangeHandler = try XCTUnwrap(decelerateParams?.locationChangeHandler) locationChangeHandler(interpolatedTouchLocation) XCTAssertEqual(mapboxMap.dragStartStub.parameters, [initialTouchLocation], line: line) @@ -202,7 +203,7 @@ final class PanGestureHandlerTests: XCTestCase { clampedTouchLocation: clampedTouchLocation) } - func testHandlePanEndedWithHorizontalPanScrollingMode() throws { + func testHandlePanEndedWithHorizontalPanMode() throws { let initialTouchLocation = CGPoint.random() let interpolatedTouchLocation = CGPoint.random() let clampedTouchLocation = CGPoint( @@ -216,7 +217,7 @@ final class PanGestureHandlerTests: XCTestCase { clampedTouchLocation: clampedTouchLocation) } - func testHandlePanEndedWithVerticalPanScrollingMode() throws { + func testHandlePanEndedWithVerticalPanMode() throws { let initialTouchLocation = CGPoint.random() let interpolatedTouchLocation = CGPoint.random() let clampedTouchLocation = CGPoint( @@ -230,6 +231,23 @@ final class PanGestureHandlerTests: XCTestCase { clampedTouchLocation: clampedTouchLocation) } + func testHandlePanEndedWithPitchGreaterThan60DoesNotDecelerate() throws { + mapboxMap.cameraState = .random() + mapboxMap.cameraState.pitch = 61 + gestureRecognizer.getStateStub.defaultReturnValue = .began + gestureRecognizer.sendActions() + gestureRecognizer.getStateStub.defaultReturnValue = .changed + gestureRecognizer.sendActions() + + gestureRecognizer.getStateStub.defaultReturnValue = .ended + gestureRecognizer.sendActions() + + XCTAssertEqual(delegate.gestureEndedStub.invocations.count, 1) + XCTAssertEqual(delegate.gestureEndedStub.parameters.first?.gestureType, .pan) + let willAnimate = try XCTUnwrap(delegate.gestureEndedStub.parameters.first?.willAnimate) + XCTAssertFalse(willAnimate) + } + func testHandlePanCancelledDoesNotTriggerDecelerationAnimation() throws { gestureRecognizer.getStateStub.defaultReturnValue = .cancelled