Skip to content

Commit

Permalink
Zoom focal point (#1122)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Hershberger <andrew.hershberger@mapbox.com>
  • Loading branch information
evil159 and macdrevx committed Feb 23, 2022
1 parent 7b9720b commit 1c862c7
Show file tree
Hide file tree
Showing 29 changed files with 234 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Mapbox welcomes participation and contributions from everyone.
* Exposed APIs to allow positioning of other views relative to the logoView, compassView, scaleBarView and attributionButton. ([#1130](https://github.com/mapbox/mapbox-maps-ios/pull/1130))
* Add `GestureOptions.pinchPanEnabled` and `.pinchZoomEnabled`. ([#1092](https://github.com/mapbox/mapbox-maps-ios/pull/1092))
* Fix an issue where pinch gesture emitted superfluous camera changed events. ([#1137](https://github.com/mapbox/mapbox-maps-ios/pull/1137))
* Add focalPoint property to zoom and rotate gestures ([#1122](https://github.com/mapbox/mapbox-maps-ios/pull/1122))
* Expose public initializers for `LayerInfo` and `SourceInfo`. ([#1144](https://github.com/mapbox/mapbox-maps-ios/pull/1144))

## 10.3.0 - February 10, 2022
Expand Down
7 changes: 3 additions & 4 deletions Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco

func makeDoubleTapToZoomInGestureHandler(view: UIView,
mapboxMap: MapboxMapProtocol,
cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler {
cameraAnimationsManager: CameraAnimationsManagerProtocol) -> FocusableGestureHandlerProtocol {
let gestureRecognizer = UITapGestureRecognizer()
view.addGestureRecognizer(gestureRecognizer)
return DoubleTapToZoomInGestureHandler(
Expand All @@ -88,7 +88,7 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco

func makeDoubleTouchToZoomOutGestureHandler(view: UIView,
mapboxMap: MapboxMapProtocol,
cameraAnimationsManager: CameraAnimationsManagerProtocol) -> GestureHandler {
cameraAnimationsManager: CameraAnimationsManagerProtocol) -> FocusableGestureHandlerProtocol {
let gestureRecognizer = UITapGestureRecognizer()
view.addGestureRecognizer(gestureRecognizer)
return DoubleTouchToZoomOutGestureHandler(
Expand All @@ -97,8 +97,7 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco
cameraAnimationsManager: cameraAnimationsManager)
}

func makeQuickZoomGestureHandler(view: UIView,
mapboxMap: MapboxMapProtocol) -> GestureHandler {
func makeQuickZoomGestureHandler(view: UIView, mapboxMap: MapboxMapProtocol) -> FocusableGestureHandlerProtocol {
let gestureRecognizer = UILongPressGestureRecognizer()
view.addGestureRecognizer(gestureRecognizer)
return QuickZoomGestureHandler(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import UIKit

/// `DoubleTapToZoomInGestureHandler` updates the map camera in response
/// to double tap gestures with 1 touch
internal final class DoubleTapToZoomInGestureHandler: GestureHandler {
internal final class DoubleTapToZoomInGestureHandler: GestureHandler, FocusableGestureHandlerProtocol {
internal var focalPoint: CGPoint?

private let mapboxMap: MapboxMapProtocol

private let cameraAnimationsManager: CameraAnimationsManagerProtocol

internal init(gestureRecognizer: UITapGestureRecognizer,
Expand All @@ -26,9 +26,9 @@ internal final class DoubleTapToZoomInGestureHandler: GestureHandler {
delegate?.gestureBegan(for: .doubleTapToZoomIn)
delegate?.gestureEnded(for: .doubleTapToZoomIn, willAnimate: true)

let tapLocation = gestureRecognizer.location(in: view)
let anchor = focalPoint ?? gestureRecognizer.location(in: view)
cameraAnimationsManager.internalEase(
to: CameraOptions(anchor: tapLocation, zoom: mapboxMap.cameraState.zoom + 1),
to: CameraOptions(anchor: anchor, zoom: mapboxMap.cameraState.zoom + 1),
duration: 0.3,
curve: .easeOut) { _ in
self.delegate?.animationEnded(for: .doubleTapToZoomIn)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import UIKit

/// `DoubleTouchToZoomOutGestureHandler` updates the map camera in response
/// to single tap gestures with 2 touches
internal final class DoubleTouchToZoomOutGestureHandler: GestureHandler {
internal final class DoubleTouchToZoomOutGestureHandler: GestureHandler, FocusableGestureHandlerProtocol {
internal var focalPoint: CGPoint?

private let mapboxMap: MapboxMapProtocol

private let cameraAnimationsManager: CameraAnimationsManagerProtocol

internal init(gestureRecognizer: UITapGestureRecognizer,
Expand All @@ -26,9 +26,9 @@ internal final class DoubleTouchToZoomOutGestureHandler: GestureHandler {
delegate?.gestureBegan(for: .doubleTouchToZoomOut)
delegate?.gestureEnded(for: .doubleTouchToZoomOut, willAnimate: true)

let tapLocation = gestureRecognizer.location(in: view)
let anchor = focalPoint ?? gestureRecognizer.location(in: view)
cameraAnimationsManager.internalEase(
to: CameraOptions(anchor: tapLocation, zoom: mapboxMap.cameraState.zoom - 1),
to: CameraOptions(anchor: anchor, zoom: mapboxMap.cameraState.zoom - 1),
duration: 0.3,
curve: .easeOut) { _ in
self.delegate?.animationEnded(for: .doubleTouchToZoomOut)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

internal protocol FocusableGestureHandlerProtocol: GestureHandler {
var focalPoint: CGPoint? { get set }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CoreGraphics

internal final class PanRotatePinchBehavior: PinchBehavior {
private let initialCameraState: CameraState
private let initialPinchMidpoint: CGPoint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CoreGraphics

internal final class PanZoomPinchBehavior: PinchBehavior {
private let initialCameraState: CameraState
private let initialPinchMidpoint: CGPoint
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CoreFoundation
import CoreLocation

internal protocol PinchBehaviorProviderProtocol: AnyObject {
Expand All @@ -7,7 +8,8 @@ internal protocol PinchBehaviorProviderProtocol: AnyObject {
rotateEnabled: Bool,
initialCameraState: CameraState,
initialPinchMidpoint: CGPoint,
initialPinchAngle: CGFloat) -> PinchBehavior
initialPinchAngle: CGFloat,
focalPoint: CGPoint?) -> PinchBehavior
}

internal final class PinchBehaviorProvider: PinchBehaviorProviderProtocol {
Expand All @@ -24,7 +26,8 @@ internal final class PinchBehaviorProvider: PinchBehaviorProviderProtocol {
rotateEnabled: Bool,
initialCameraState: CameraState,
initialPinchMidpoint: CGPoint,
initialPinchAngle: CGFloat) -> PinchBehavior {
initialPinchAngle: CGFloat,
focalPoint: CGPoint?) -> PinchBehavior {
switch (panEnabled, zoomEnabled, rotateEnabled) {
case (true, true, true):
return PanZoomRotatePinchBehavior(
Expand Down Expand Up @@ -53,17 +56,20 @@ internal final class PinchBehaviorProvider: PinchBehaviorProviderProtocol {
initialCameraState: initialCameraState,
initialPinchMidpoint: initialPinchMidpoint,
initialPinchAngle: initialPinchAngle,
focalPoint: focalPoint,
mapboxMap: mapboxMap)
case (false, true, false):
return ZoomPinchBehavior(
initialCameraState: initialCameraState,
initialPinchMidpoint: initialPinchMidpoint,
focalPoint: focalPoint,
mapboxMap: mapboxMap)
case (false, false, true):
return RotatePinchBehavior(
initialCameraState: initialCameraState,
initialPinchMidpoint: initialPinchMidpoint,
initialPinchAngle: initialPinchAngle,
focalPoint: focalPoint,
mapboxMap: mapboxMap)
case (false, false, false):
return EmptyPinchBehavior()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import CoreGraphics
internal final class RotatePinchBehavior: PinchBehavior {
private let initialCameraState: CameraState
private let initialPinchMidpoint: CGPoint
private let initialPinchAngle: CGFloat
private let mapboxMap: MapboxMapProtocol
private let focalPoint: CGPoint?

internal init(initialCameraState: CameraState,
initialPinchMidpoint: CGPoint,
initialPinchAngle: CGFloat,
focalPoint: CGPoint?,
mapboxMap: MapboxMapProtocol) {
self.initialCameraState = initialCameraState
self.initialPinchMidpoint = initialPinchMidpoint
self.initialPinchAngle = initialPinchAngle
self.focalPoint = focalPoint
self.mapboxMap = mapboxMap
}

Expand All @@ -23,7 +27,7 @@ internal final class RotatePinchBehavior: PinchBehavior {
.wrappedAngle(to: pinchAngle)
.toDegrees()
mapboxMap.setCamera(to: CameraOptions(
anchor: initialPinchMidpoint,
anchor: focalPoint ?? initialPinchMidpoint,
bearing: initialCameraState.bearing + CLLocationDirection(bearingIncrement)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ internal final class ZoomPinchBehavior: PinchBehavior {
private let initialCameraState: CameraState
private let initialPinchMidpoint: CGPoint
private let mapboxMap: MapboxMapProtocol
private let focalPoint: CGPoint?

internal init(initialCameraState: CameraState,
initialPinchMidpoint: CGPoint,
focalPoint: CGPoint?,
mapboxMap: MapboxMapProtocol) {
self.initialCameraState = initialCameraState
self.initialPinchMidpoint = initialPinchMidpoint
self.focalPoint = focalPoint
self.mapboxMap = mapboxMap
}

Expand All @@ -16,7 +19,7 @@ internal final class ZoomPinchBehavior: PinchBehavior {
pinchAngle: CGFloat) {
let zoomIncrement = log2(pinchScale)
mapboxMap.setCamera(to: CameraOptions(
anchor: initialPinchMidpoint,
anchor: focalPoint ?? initialPinchMidpoint,
zoom: initialCameraState.zoom + zoomIncrement))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ internal final class ZoomRotatePinchBehavior: PinchBehavior {
private let initialPinchMidpoint: CGPoint
private let initialPinchAngle: CGFloat
private let mapboxMap: MapboxMapProtocol
private let focalPoint: CGPoint?

internal init(initialCameraState: CameraState,
initialPinchMidpoint: CGPoint,
initialPinchAngle: CGFloat,
focalPoint: CGPoint?,
mapboxMap: MapboxMapProtocol) {
self.initialCameraState = initialCameraState
self.initialPinchMidpoint = initialPinchMidpoint
self.initialPinchAngle = initialPinchAngle
self.focalPoint = focalPoint
self.mapboxMap = mapboxMap
}

Expand All @@ -25,7 +28,7 @@ internal final class ZoomRotatePinchBehavior: PinchBehavior {
.wrappedAngle(to: pinchAngle)
.toDegrees()
mapboxMap.setCamera(to: CameraOptions(
anchor: initialPinchMidpoint,
anchor: focalPoint ?? initialPinchMidpoint,
zoom: initialCameraState.zoom + zoomIncrement,
bearing: initialCameraState.bearing + CLLocationDirection(bearingIncrement)))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import UIKit
@_implementationOnly import MapboxCommon_Private

internal protocol PinchGestureHandlerProtocol: GestureHandler {
internal protocol PinchGestureHandlerProtocol: FocusableGestureHandlerProtocol {
var rotateEnabled: Bool { get set }
var zoomEnabled: Bool { get set }
var panEnabled: Bool { get set }
var focalPoint: CGPoint? { get set }
}

/// `PinchGestureHandler` updates the map camera in response to a 2-touch
Expand All @@ -18,6 +20,9 @@ internal final class PinchGestureHandler: GestureHandler, PinchGestureHandlerPro
/// Whether pinch gesture can pan map or not
internal var panEnabled: Bool = true

/// Anchor point for rotating and zooming
internal var focalPoint: CGPoint?

/// The behavior for the current gesture, based on the initial state of the \*Enabled flags.
private var pinchBehavior: PinchBehavior?

Expand Down Expand Up @@ -86,13 +91,21 @@ internal final class PinchGestureHandler: GestureHandler, PinchGestureHandlerPro
guard let view = gestureRecognizer.view else {
return
}

if panEnabled, focalPoint != nil {
Log.warning(
forMessage: "Possible pinch gesture recognizer misconfiguration: the specified focal point will be ignored when pinching. In order for the focal point to work, pinch pan has to be disabled.",
category: "Gestures")
}

pinchBehavior = pinchBehaviorProvider.makePinchBehavior(
panEnabled: panEnabled,
zoomEnabled: zoomEnabled,
rotateEnabled: rotateEnabled,
initialCameraState: mapboxMap.cameraState,
initialPinchMidpoint: gestureRecognizer.location(in: view),
initialPinchAngle: pinchAngle(with: gestureRecognizer))
initialPinchAngle: pinchAngle(with: gestureRecognizer),
focalPoint: focalPoint)
// if this is the first time we started handling the gesture, inform
// the delegate.
if !invokedGestureBegan {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import UIKit

/// `QuickZoomGestureHandler` updates the map camera in response to double tap and drag gestures
internal final class QuickZoomGestureHandler: GestureHandler {
internal final class QuickZoomGestureHandler: GestureHandler, FocusableGestureHandlerProtocol {
private var initialLocation: CGPoint?
private var initialZoom: CGFloat?
internal var focalPoint: CGPoint?
private let mapboxMap: MapboxMapProtocol
private var initialFocalPoint: CGPoint?

internal init(gestureRecognizer: UILongPressGestureRecognizer,
mapboxMap: MapboxMapProtocol) {
Expand All @@ -25,6 +27,7 @@ internal final class QuickZoomGestureHandler: GestureHandler {
delegate?.gestureBegan(for: .quickZoom)
initialLocation = location
initialZoom = mapboxMap.cameraState.zoom
initialFocalPoint = focalPoint
case .changed:
guard let initialLocation = initialLocation,
let initialZoom = initialZoom else {
Expand All @@ -33,10 +36,12 @@ internal final class QuickZoomGestureHandler: GestureHandler {
let distance = location.y - initialLocation.y
// change by 1 zoom level per 75 points of translation
let newZoom = initialZoom + distance / 75
mapboxMap.setCamera(to: CameraOptions(anchor: initialLocation, zoom: newZoom))
let anchor = initialFocalPoint ?? initialLocation
mapboxMap.setCamera(to: CameraOptions(anchor: anchor, zoom: newZoom))
case .ended, .cancelled:
initialLocation = nil
initialZoom = nil
initialFocalPoint = nil
delegate?.gestureEnded(for: .quickZoom, willAnimate: false)
default:
break
Expand Down
17 changes: 11 additions & 6 deletions Sources/MapboxMaps/Gestures/GestureManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ public final class GestureManager: GestureHandlerDelegate {
quickZoomGestureRecognizer.isEnabled = newValue.quickZoomEnabled
panGestureHandler.panMode = newValue.panMode
panGestureHandler.decelerationFactor = newValue.panDecelerationFactor
doubleTapToZoomInGestureHandler.focalPoint = newValue.focalPoint
doubleTouchToZoomOutGestureHandler.focalPoint = newValue.focalPoint
quickZoomGestureHandler.focalPoint = newValue.focalPoint
pinchGestureHandler.focalPoint = newValue.focalPoint
}
get {
var gestureOptions = GestureOptions()
Expand All @@ -43,6 +47,7 @@ public final class GestureManager: GestureHandlerDelegate {
gestureOptions.quickZoomEnabled = quickZoomGestureRecognizer.isEnabled
gestureOptions.panMode = panGestureHandler.panMode
gestureOptions.panDecelerationFactor = panGestureHandler.decelerationFactor
gestureOptions.focalPoint = doubleTapToZoomInGestureHandler.focalPoint
return gestureOptions
}
}
Expand Down Expand Up @@ -95,19 +100,19 @@ public final class GestureManager: GestureHandlerDelegate {
private let panGestureHandler: PanGestureHandlerProtocol
private let pinchGestureHandler: PinchGestureHandlerProtocol
private let pitchGestureHandler: GestureHandler
private let doubleTapToZoomInGestureHandler: GestureHandler
private let doubleTouchToZoomOutGestureHandler: GestureHandler
private let quickZoomGestureHandler: GestureHandler
private let doubleTapToZoomInGestureHandler: FocusableGestureHandlerProtocol
private let doubleTouchToZoomOutGestureHandler: FocusableGestureHandlerProtocol
private let quickZoomGestureHandler: FocusableGestureHandlerProtocol
private let singleTapGestureHandler: GestureHandler
private let anyTouchGestureHandler: GestureHandler
private let mapboxMap: MapboxMapProtocol

internal init(panGestureHandler: PanGestureHandlerProtocol,
pinchGestureHandler: PinchGestureHandlerProtocol,
pitchGestureHandler: GestureHandler,
doubleTapToZoomInGestureHandler: GestureHandler,
doubleTouchToZoomOutGestureHandler: GestureHandler,
quickZoomGestureHandler: GestureHandler,
doubleTapToZoomInGestureHandler: FocusableGestureHandlerProtocol,
doubleTouchToZoomOutGestureHandler: FocusableGestureHandlerProtocol,
quickZoomGestureHandler: FocusableGestureHandlerProtocol,
singleTapGestureHandler: GestureHandler,
anyTouchGestureHandler: GestureHandler,
mapboxMap: MapboxMapProtocol) {
Expand Down
5 changes: 5 additions & 0 deletions Sources/MapboxMaps/Gestures/GestureOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,10 @@ public struct GestureOptions: Equatable {
/// Defaults to `UIScrollView.DecelerationRate.normal.rawValue`
public var panDecelerationFactor: CGFloat = UIScrollView.DecelerationRate.normal.rawValue

/// By default, gestures rotate and zoom around the center of the gesture. Set this property to rotate and zoom around a fixed point instead.
///
/// This property will be ignored by the pinch gesture if ``GestureOptions/pinchPanEnabled`` is set to `true`.
public var focalPoint: CGPoint?

public init() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ final class MockMapViewDependencyProvider: MapViewDependencyProviderProtocol {
gestureRecognizer: UIGestureRecognizer()),
pinchGestureHandler: MockPinchGestureHandler(gestureRecognizer: UIGestureRecognizer()),
pitchGestureHandler: makeGestureHandler(),
doubleTapToZoomInGestureHandler: makeGestureHandler(),
doubleTouchToZoomOutGestureHandler: makeGestureHandler(),
quickZoomGestureHandler: makeGestureHandler(),
doubleTapToZoomInGestureHandler: MockFocusableGestureHandler(
gestureRecognizer: UIGestureRecognizer()),
doubleTouchToZoomOutGestureHandler: MockFocusableGestureHandler(
gestureRecognizer: UIGestureRecognizer()),
quickZoomGestureHandler: MockFocusableGestureHandler(gestureRecognizer: UIGestureRecognizer()),
singleTapGestureHandler: makeGestureHandler(),
anyTouchGestureHandler: makeGestureHandler(),
mapboxMap: mapboxMap)
Expand Down
Loading

0 comments on commit 1c862c7

Please sign in to comment.