Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom focal point #1122

Merged
merged 18 commits into from
Feb 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I'm not familiar with swift, but is it needed as there's no other change in this class besides this import.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Xcode has started auto-importing frameworks which is leading to a lot of things like this happening. Probably there were other changes to this file in a previous iteration of this PR which led to this import which was then not reverted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@macdrevx exactly! I'm willing to keep it, Xcode has a point - we do use CoreGrapics types here.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import can be removed now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one stays, but the one from GestureOptions goes :)


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
macdrevx marked this conversation as resolved.
Show resolved Hide resolved
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.
macdrevx marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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