Skip to content

Commit

Permalink
Rework Gestures (#690)
Browse files Browse the repository at this point in the history
* remove hapticFeedbackEnabled (#663)

* remove hapticFeedbackEnabled`

* clean up changelog

* Make gestureOptions.decelarationRate the source of truth (#662)

* making gesture options decelaration rate the source of truth

* clean up changelog

* fix indentation

* Internalize UIGestureRecognizerDelegate (#669)

* seperated gesture recognizer delegate into a new class and fixed downstream breaks

* adding changelog

* swift lint and code clean up

* adding a comment

* pr comments

* Refactor gestures (#677)

#### Breaking Changes

- Pan deceleration has been temporarily removed
- `TapGestureHandler.init` was previously public by mistake and is now internal
- The behavior of `GestureManager.options` has been updated to better reflect the `isEnabled` state of the associated gesture recognizers
- The gesture recognizer properties of `GestureManager` are no longer `Optional`
- `GestureType` has been redesigned so that its cases have a 1-1 relationship with the built-in gestures

#### Public API Additions

- `CameraState`'s fields are now `var`s instead of `let`s for testing purposes, and a public, memberwise initializer has been added.
- `PanScrollingMode` now conforms to `CaseIterable`
- `GestureType` now conforms to `CaseIterable`

#### Bug fixes

- GestureManager no longer sets itself as the delegate of all gestures in MapView when its options change

#### Internal Refactoring

- Generalizes `CameraAnimatorMapboxMap` by renaming it to `MapboxMapProtocol` so that it can be used throughout the SDK. SDK seems small enough that per-component dependency inversion feels unnecessary and might negatively impact binary size.
- Removes old `GestureHandlerDelegate` by injecting `MapboxMap` and `CameraAnimationsManager` into each gesture handler to allow each handler to manipulate the camera directly.
- Removes unnecessary `GestureContextProvider`
- Refactors handlers and `GestureManager` for dependency injection and more thorough tests
- Tidies up handlers and `GestureManager` and associated tests for greater consistency

* Pan Deceleration (#692)

* update changelog

* fix changelog

* Gesture Options Cleanup (#696)

* wip

* Change double tap to zoom out to "double touch"

* Fix warning

* Lint

* Update changelog

* [run device tests]

* Use explicit self

Co-authored-by: Andrew Hershberger <andrew.hershberger@mapbox.com>
Co-authored-by: Nishant Karajgikar <nishant.karajgikar@mapbox.com>
  • Loading branch information
3 people committed Sep 22, 2021
1 parent ddcf9f2 commit 00a6629
Show file tree
Hide file tree
Showing 64 changed files with 2,389 additions and 1,947 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class CameraAnimatorsExample: UIViewController, ExampleProtocol {
// Start a chain of camera animations
func startCameraAnimations() {
// Declare an animator that changes the map's
var bearingAnimator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in
let bearingAnimator = mapView.camera.makeAnimator(duration: 4, curve: .easeInOut) { (transition) in
transition.bearing.toValue = -45
}

Expand Down
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ Mapbox welcomes participation and contributions from everyone.
* `OfflineRegionGeometryDefinition.geometry` is now of type `Turf.Geometry` instead of `MapboxCommon.Geometry` ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* The `HTTPResponse` init methods that take `MapboxCommon.Expected` instead of `Result` are now correctly marked as refined for Swift. ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* The `DownloadStatus` init methods that take `MapboxCommon.Expected` instead of `Result` and `NSNumber?` instead of `UInt64?` are not correctly marked as refined for Swift. ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* `GestureOptions.hapticFeedbackEnabled` has been removed. ([#663](https://github.com/mapbox/mapbox-maps-ios/pull/663))
* `GestureManager.decelarationRate` has been removed and `GestureOptions.decelerationRate` is the single source of truth. ([#662](https://github.com/mapbox/mapbox-maps-ios/pull/662))
* `GestureManager` no longer conforms to `NSObject` and is not a `UIGestureRecognizerDelegate`. ([#669](https://github.com/mapbox/mapbox-maps-ios/pull/669))
* `TapGestureHandler.init` was previously public by mistake and is now internal. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* The behavior of `GestureManager.options` has been updated to better reflect the `isEnabled` state of the associated gesture recognizers. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* The gesture recognizer properties of `GestureManager` are no longer `Optional`. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* `GestureType` has been redesigned so that its cases have a 1-1 relationship with the built-in gestures. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* `GestureManager.rotationGestureRecognizer` has been removed. Rotation is now handled by `.pinchGestureRecognizer` in addition to its preexisting handling of panning and zooming. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureManager.doubleTapToZoomOutGestureRecognizer` has been replaced with `.doubleTouchToZoomOutGestureRecognizer`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `PanScrollingMode` has been renamed to `PanMode`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureOptions.zoomEnabled` has been replaced by `.doubleTapToZoomInEnabled`, `.doubleTouchToZoomOutEnabled`, and `.quickZoomEnabled`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureOptions.rotateEnabled` has been removed. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureOptions.scrollEnabled` has been renamed to `.panEnabled`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureOptions.scrollingMode` has been renamed to `.panMode`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureOptions.decelerationRate` has been renamed to `.panDecelerationFactor`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureType.doubleTapToZoomOut` has been replaced with `.doubleTouchToZoomOut`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureType.rotate` has been removed. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* `GestureType` cases have been reordered for consistency with `GestureOptions` and `GestureManager`. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))

### Features ✨ and improvements 🏁

Expand All @@ -21,6 +39,10 @@ Mapbox welcomes participation and contributions from everyone.
* Enable instant transitions for data driven symbol layer properties ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* Implement face culling for Metal ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* `HTTPServiceInterface.getInstance()` is now publicly available. ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* `CameraState`'s fields are now `var`s instead of `let`s for testing purposes, and a public, memberwise initializer has been added. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* `PanScrollingMode` now conforms to `CaseIterable`. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* `GestureType` now conforms to `CaseIterable`. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* Pan deceleration has been reimplemented to produce a more natural deceleration effect. ([#692](https://github.com/mapbox/mapbox-maps-ios/pull/692))

### Bug fixes 🐞

Expand All @@ -29,6 +51,10 @@ Mapbox welcomes participation and contributions from everyone.
* Fix rendering artifact for a line layer, when its line-gradient property is set at runtime. ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* Don't draw SDF images in text-field and issue warning for it ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* Fix incorrect return from `StyleManager#getStyleLayerPropertyDefaultValue` for 'text-field'. Now the default value is set to `["format", "" , {}]` ([#689](https://github.com/mapbox/mapbox-maps-ios/pull/689))
* GestureManager no longer sets itself as the delegate of all gestures in MapView when its options change. ([#677](https://github.com/mapbox/mapbox-maps-ios/pull/677))
* Fixes an issue where tapping the compass could fail to set the bearing to 0 if there was already an animation running. Tapping the compass now cancels any existing animations. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* Fixes issues with the pinch gesture when removing and re-adding one of the two required touches. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))
* Fixes an issue where a pan gesture would fail if it interrupted the deceleration from a previous pan gesture. ([#696](https://github.com/mapbox/mapbox-maps-ios/pull/696))

## 10.0.0-rc.8 - Sept 8, 2021

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class BasicCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterf
/// The `CameraView` owned by this animator
private let cameraView: CameraView

private let mapboxMap: CameraAnimatorMapboxMap
private let mapboxMap: MapboxMapProtocol

/// Represents the animation that this animator is attempting to execute
private var animation: ((inout CameraTransition) -> Void)?
Expand Down Expand Up @@ -67,7 +67,7 @@ public class BasicCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterf
// MARK: Initializer
internal init(propertyAnimator: UIViewPropertyAnimator,
owner: AnimationOwner,
mapboxMap: CameraAnimatorMapboxMap,
mapboxMap: MapboxMapProtocol,
cameraView: CameraView) {
self.propertyAnimator = propertyAnimator
self.owner = owner
Expand Down
61 changes: 54 additions & 7 deletions Sources/MapboxMaps/Foundation/Camera/CameraAnimationsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,24 @@ internal protocol CameraAnimatorInterface: CameraAnimator {
func update()
}

internal protocol CameraAnimationsManagerProtocol: AnyObject {
@discardableResult
func ease(to camera: CameraOptions,
duration: TimeInterval,
curve: UIView.AnimationCurve,
completion: AnimationCompletion?) -> Cancelable?

func decelerate(location: CGPoint,
velocity: CGPoint,
decelerationFactor: CGFloat,
locationChangeHandler: @escaping (CGPoint) -> Void,
completion: @escaping () -> Void)

func cancelAnimations()
}

/// An object that manages a camera's view lifecycle.
public class CameraAnimationsManager {
public class CameraAnimationsManager: CameraAnimationsManagerProtocol {

/// Used to set up camera specific configuration
public var options: CameraBoundsOptions {
Expand Down Expand Up @@ -100,9 +116,7 @@ public class CameraAnimationsManager {
cameraAnimatorsSet.add(flyToAnimator)

flyToAnimator.addCompletion { [weak self, weak flyToAnimator] (position) in
if let internalAnimator = self?.internalAnimator,
let animator = flyToAnimator,
internalAnimator === animator {
if self?.internalAnimator === flyToAnimator {
self?.internalAnimator = nil
}
// Call the developer-provided completion (if present)
Expand Down Expand Up @@ -139,9 +153,7 @@ public class CameraAnimationsManager {

// Nil out the `internalAnimator` once the "ease to" finishes
animator.addCompletion { [weak self, weak animator] (position) in
if let internalAnimator = self?.internalAnimator,
let animator = animator,
internalAnimator === animator {
if self?.internalAnimator === animator {
self?.internalAnimator = nil
}
completion?(position)
Expand Down Expand Up @@ -272,4 +284,39 @@ public class CameraAnimationsManager {
cameraViewContainerView.addSubview(cameraView)
return cameraView
}

/// This function will handle the natural decelration of a gesture when there is a velocity provided. A use case for this is the pan gesture.
/// - Parameters:
/// - location: Current location of center coordinate
/// - velocity: The speed at which the map should move over time
/// - decelerationFactor: A multiplication factor that determines the speed at which the velocity should slow down.
/// - locationChangeHandler: Change handler to be passed through to the animator
/// - completion: Completion to be called after animation has finished
internal func decelerate(location: CGPoint,
velocity: CGPoint,
decelerationFactor: CGFloat,
locationChangeHandler: @escaping (CGPoint) -> Void,
completion: @escaping () -> Void) {

// Stop the `internalAnimator` before beginning a deceleration
internalAnimator?.stopAnimation()

let decelerateAnimator = GestureDecelerationCameraAnimator(
location: location,
velocity: velocity,
decelerationFactor: decelerationFactor,
locationChangeHandler: locationChangeHandler,
dateProvider: DefaultDateProvider())

decelerateAnimator.completion = { [weak self, weak decelerateAnimator] in
if self?.internalAnimator === decelerateAnimator {
self?.internalAnimator = nil
}
completion()
}

cameraAnimatorsSet.add(decelerateAnimator)
decelerateAnimator.startAnimation()
internalAnimator = decelerateAnimator
}
}

This file was deleted.

32 changes: 22 additions & 10 deletions Sources/MapboxMaps/Foundation/Camera/CameraState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ import CoreLocation
import UIKit

public struct CameraState: Hashable {
public let center: CLLocationCoordinate2D
public let padding: UIEdgeInsets
public let zoom: CGFloat
public let bearing: CLLocationDirection
public let pitch: CGFloat
public var center: CLLocationCoordinate2D
public var padding: UIEdgeInsets
public var zoom: CGFloat
public var bearing: CLLocationDirection
public var pitch: CGFloat

public init(center: CLLocationCoordinate2D,
padding: UIEdgeInsets,
zoom: CGFloat,
bearing: CLLocationDirection,
pitch: CGFloat) {
self.center = center
self.padding = padding
self.zoom = zoom
self.bearing = bearing
self.pitch = pitch
}

internal init(_ objcValue: MapboxCoreMaps.CameraState) {
self.center = objcValue.center
self.padding = objcValue.padding.toUIEdgeInsetsValue()
self.zoom = CGFloat(objcValue.zoom)
self.bearing = CLLocationDirection(objcValue.bearing)
self.pitch = CGFloat(objcValue.pitch)
self.center = objcValue.center
self.padding = objcValue.padding.toUIEdgeInsetsValue()
self.zoom = CGFloat(objcValue.zoom)
self.bearing = CLLocationDirection(objcValue.bearing)
self.pitch = CGFloat(objcValue.pitch)
}

public static func == (lhs: CameraState, rhs: CameraState) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import UIKit

public class FlyToCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterface {

private let mapboxMap: CameraAnimatorMapboxMap
private let mapboxMap: MapboxMapProtocol

public private(set) var owner: AnimationOwner

Expand All @@ -26,7 +26,7 @@ public class FlyToCameraAnimator: NSObject, CameraAnimator, CameraAnimatorInterf
owner: AnimationOwner,
duration: TimeInterval? = nil,
mapSize: CGSize,
mapboxMap: CameraAnimatorMapboxMap,
mapboxMap: MapboxMapProtocol,
dateProvider: DateProvider = DefaultDateProvider()) {
guard let flyToInterpolator = FlyToInterpolator(from: initial, to: final, cameraBounds: cameraBounds, size: mapSize) else {
return nil
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import UIKit

internal final class GestureDecelerationCameraAnimator: NSObject, CameraAnimatorInterface {

private var location: CGPoint
private var velocity: CGPoint
private let decelerationFactor: CGFloat
private let locationChangeHandler: (CGPoint) -> Void
private var previousDate: Date?
private let dateProvider: DateProvider
internal var completion: (() -> Void)?

internal init(location: CGPoint,
velocity: CGPoint,
decelerationFactor: CGFloat,
locationChangeHandler: @escaping (CGPoint) -> Void,
dateProvider: DateProvider) {
self.location = location
self.velocity = velocity
self.decelerationFactor = decelerationFactor
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 factor should be applied to the velocity once per millisecond
velocity.x *= pow(decelerationFactor, (elapsedTime * 1000))
velocity.y *= pow(decelerationFactor, (elapsedTime * 1000))

guard abs(velocity.x) >= 1 || abs(velocity.y) >= 1 else {
stopAnimation()
return
}
}
}
2 changes: 2 additions & 0 deletions Sources/MapboxMaps/Foundation/MapView+Supportable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ extension MapView: OrnamentSupportableView {
}

internal func compassTapped() {
camera.cancelAnimations()

var animator: BasicCameraAnimator?
animator = camera.makeAnimator(duration: 0.3, curve: .easeOut, animations: { (transition) in
transition.bearing.toValue = 0
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxMaps/Foundation/MapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ open class MapView: UIView {
mapboxMap: mapboxMap)

// Initialize/Configure gesture manager
gestures = GestureManager(view: self, cameraAnimationsManager: camera, mapboxMap: mapboxMap)
gestures = dependencyProvider.makeGestureManager(view: self, mapboxMap: mapboxMap, cameraAnimationsManager: camera)

// Initialize the attribution manager
attributionDialogManager = AttributionDialogManager(dataSource: mapboxMap, delegate: self)
Expand Down
Loading

0 comments on commit 00a6629

Please sign in to comment.