-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
412 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
Sources/MapboxMaps/Foundation/Camera/GestureDecelerationCameraAnimator.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import UIKit | ||
|
||
internal final class GestureDecelerationCameraAnimator: NSObject, CameraAnimatorInterface { | ||
|
||
private var location: CGPoint | ||
private var velocity: CGPoint | ||
private let decelerationRate: CGFloat | ||
private let locationChangeHandler: (CGPoint) -> Void | ||
private var previousDate: Date? | ||
private let dateProvider: DateProvider | ||
internal var completion: (() -> Void)? | ||
|
||
internal init(location: CGPoint, | ||
velocity: CGPoint, | ||
decelerationRate: CGFloat, | ||
locationChangeHandler: @escaping (CGPoint) -> Void, | ||
dateProvider: DateProvider) { | ||
self.location = location | ||
self.velocity = velocity | ||
self.decelerationRate = decelerationRate | ||
self.locationChangeHandler = locationChangeHandler | ||
self.dateProvider = dateProvider | ||
} | ||
|
||
internal private(set) var state: UIViewAnimatingState = .inactive | ||
|
||
internal func cancel() { | ||
stopAnimation() | ||
} | ||
|
||
internal func startAnimation() { | ||
previousDate = dateProvider.now | ||
state = .active | ||
} | ||
|
||
internal func stopAnimation() { | ||
state = .inactive | ||
completion?() | ||
completion = nil | ||
} | ||
|
||
internal func update() { | ||
guard state == .active, let previousDate = previousDate else { | ||
return | ||
} | ||
|
||
let currentDate = dateProvider.now | ||
self.previousDate = currentDate | ||
|
||
let elapsedTime = CGFloat(currentDate.timeIntervalSince(previousDate)) | ||
|
||
// calculate new location showing how far we have traveled | ||
location.x += velocity.x * elapsedTime | ||
location.y += velocity.y * elapsedTime | ||
|
||
locationChangeHandler(location) | ||
|
||
// deceleration rate is a factor that should | ||
// be applied to the velocity once per millisecond | ||
velocity.x *= pow(decelerationRate, (elapsedTime * 1000)) | ||
velocity.y *= pow(decelerationRate, (elapsedTime * 1000)) | ||
|
||
guard abs(velocity.x) >= 1 || abs(velocity.y) >= 1 else { | ||
stopAnimation() | ||
return | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
Tests/MapboxMapsTests/Foundation/Camera/GestureDecelerationCameraAnimatorTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
import XCTest | ||
@testable import MapboxMaps | ||
|
||
final class GestureDecelerationCameraAnimatorTests: XCTestCase { | ||
|
||
var location: CGPoint! | ||
var velocity: CGPoint! | ||
var decelerationRate: CGFloat! | ||
var locationChangeHandler: Stub<CGPoint, Void>! | ||
var dateProvider: MockDateProvider! | ||
var completion: Stub<Void, Void>! | ||
var animator: GestureDecelerationCameraAnimator! | ||
|
||
override func setUp() { | ||
super.setUp() | ||
location = .zero | ||
velocity = CGPoint(x: 1000, y: -1000) | ||
decelerationRate = 0.7 | ||
locationChangeHandler = Stub() | ||
dateProvider = MockDateProvider() | ||
completion = Stub() | ||
animator = GestureDecelerationCameraAnimator( | ||
location: location, | ||
velocity: velocity, | ||
decelerationRate: decelerationRate, | ||
locationChangeHandler: locationChangeHandler.call(with:), | ||
dateProvider: dateProvider) | ||
animator.completion = completion.call | ||
} | ||
|
||
override func tearDown() { | ||
animator = nil | ||
completion = nil | ||
dateProvider = nil | ||
locationChangeHandler = nil | ||
decelerationRate = nil | ||
velocity = nil | ||
location = nil | ||
super.tearDown() | ||
} | ||
|
||
func testStateIsInitiallyInactive() { | ||
XCTAssertEqual(animator.state, .inactive) | ||
} | ||
|
||
func testStartAnimation() { | ||
animator.startAnimation() | ||
|
||
XCTAssertEqual(animator.state, .active) | ||
} | ||
|
||
func testStopAnimation() { | ||
animator.startAnimation() | ||
|
||
animator.stopAnimation() | ||
|
||
XCTAssertEqual(animator.state, .inactive) | ||
XCTAssertEqual(completion.invocations.count, 1) | ||
} | ||
|
||
func testUpdate() { | ||
animator.startAnimation() | ||
|
||
// Simulate advancing by 10 ms | ||
dateProvider.nowStub.defaultReturnValue += 0.01 | ||
animator.update() | ||
|
||
// Expected value is duration * velocity; | ||
XCTAssertEqual(locationChangeHandler.parameters, [CGPoint(x: 10, y: -10)]) | ||
// The previous update() should also have reduced the velocity | ||
// by multiplying it by the decelerationRate once for each elapsed | ||
// millisecond. In this simulateion, 10 ms have elapsed. | ||
let expectedVelocityAdjustmentFactor = pow(decelerationRate, 10) | ||
locationChangeHandler.reset() | ||
// Make sure the animation didn't end yet | ||
XCTAssertEqual(animator.state, .active) | ||
XCTAssertEqual(completion.invocations.count, 0) | ||
|
||
// This time, advance by 20 ms to keep it distinct | ||
// from the first update() call. | ||
dateProvider.nowStub.defaultReturnValue += 0.02 | ||
animator.update() | ||
|
||
// The expected value this time is the previous location + the reduced | ||
// velocity (velocity * expectedVelocityAdjustmentFactor) times the elapsed duration | ||
XCTAssertEqual( | ||
locationChangeHandler.parameters, [ | ||
CGPoint( | ||
x: 10 + (velocity.x * expectedVelocityAdjustmentFactor) * 0.02, | ||
y: -10 + (velocity.y * expectedVelocityAdjustmentFactor) * 0.02)]) | ||
locationChangeHandler.reset() | ||
// After the previous update() call, the velocity should have also been reduced | ||
// to be sufficiently low (< 1 in both x and y) to end the animation. | ||
XCTAssertEqual(animator.state, .inactive) | ||
XCTAssertEqual(completion.invocations.count, 1) | ||
} | ||
} |
Oops, something went wrong.