iOS perspective gesture support
Drag two fingers upward to tilt the map. Implements #2116
This commit is contained in:
@@ -74,6 +74,13 @@ IB_DESIGNABLE
|
||||
* The default value of this property is `YES`. */
|
||||
@property(nonatomic, getter=isRotateEnabled) BOOL rotateEnabled;
|
||||
|
||||
/** A Boolean value that determines whether the user may change the pitch (tilt) of the map.
|
||||
*
|
||||
* This property controls only user interactions with the map. If you set the value of this property to `NO`, you may still change the pitch of the map programmatically.
|
||||
*
|
||||
* The default value of this property is `YES`. */
|
||||
@property(nonatomic, getter=isPitchEnabled) BOOL pitchEnabled;
|
||||
|
||||
/** The compass image view shown in the upper-right when the map is rotated. */
|
||||
@property (nonatomic, readonly) UIImageView *compassView;
|
||||
|
||||
@@ -173,6 +180,20 @@ IB_DESIGNABLE
|
||||
/** Resets the map rotation to a northern heading. */
|
||||
- (IBAction)resetNorth;
|
||||
|
||||
/** The pitch of the map (measured in degrees).
|
||||
*
|
||||
* The default value `0` shows a completely flat map. Maximum value is `60`. */
|
||||
@property (nonatomic) double pitch;
|
||||
|
||||
/** Changes the pitch of the map.
|
||||
* @param pitch The pitch of the map (measured in degrees) relative to top-down.
|
||||
*
|
||||
* Changing the pitch tilts the map without changing the current center coordinate or zoom level. */
|
||||
- (void)setPitch:(double)pitch;
|
||||
|
||||
/** Resets the map pitch to head-on. */
|
||||
- (IBAction)resetPitch;
|
||||
|
||||
#pragma mark - Converting Map Coordinates
|
||||
|
||||
/** @name Converting Map Coordinates */
|
||||
|
||||
@@ -17,6 +17,7 @@ public:
|
||||
double latitude = 0;
|
||||
double zoom = 0;
|
||||
double bearing = 0;
|
||||
double pitch = 0;
|
||||
|
||||
MGLUserTrackingMode userTrackingMode = MGLUserTrackingModeNone;
|
||||
bool showsUserLocation = false;
|
||||
|
||||
@@ -86,6 +86,7 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
|
||||
settings->latitude = self.mapView.centerCoordinate.latitude;
|
||||
settings->zoom = self.mapView.zoomLevel;
|
||||
settings->bearing = self.mapView.direction;
|
||||
settings->pitch = self.mapView.pitch;
|
||||
settings->debug = self.mapView.isDebugActive;
|
||||
settings->userTrackingMode = self.mapView.userTrackingMode;
|
||||
settings->showsUserLocation = self.mapView.showsUserLocation;
|
||||
@@ -99,6 +100,7 @@ mbgl::Settings_NSUserDefaults *settings = nullptr;
|
||||
settings->load();
|
||||
[self.mapView setCenterCoordinate:CLLocationCoordinate2DMake(settings->latitude, settings->longitude) zoomLevel:settings->zoom animated:NO];
|
||||
self.mapView.direction = settings->bearing;
|
||||
self.mapView.pitch = settings->pitch;
|
||||
self.mapView.userTrackingMode = settings->userTrackingMode;
|
||||
self.mapView.showsUserLocation = settings->showsUserLocation;
|
||||
[self.mapView setDebugActive:settings->debug];
|
||||
|
||||
@@ -11,6 +11,7 @@ Settings_NSUserDefaults::Settings_NSUserDefaults()
|
||||
@"latitude" : @(latitude),
|
||||
@"zoom" : @(zoom),
|
||||
@"bearing" : @(bearing),
|
||||
@"pitch" : @(pitch),
|
||||
@"userTrackingMode" : @(userTrackingMode),
|
||||
@"showsUserLocation" : @(showsUserLocation),
|
||||
@"debug" : @(debug),
|
||||
@@ -26,6 +27,7 @@ void Settings_NSUserDefaults::load()
|
||||
latitude = [settings[@"latitude"] doubleValue];
|
||||
zoom = [settings[@"zoom"] doubleValue];
|
||||
bearing = [settings[@"bearing"] doubleValue];
|
||||
pitch = [settings[@"pitch"] doubleValue];
|
||||
debug = [settings[@"debug"] boolValue];
|
||||
|
||||
unsigned uncheckedTrackingMode = [settings[@"trackingMode"] unsignedIntValue];
|
||||
@@ -44,6 +46,7 @@ void Settings_NSUserDefaults::save()
|
||||
@"latitude" : @(latitude),
|
||||
@"zoom" : @(zoom),
|
||||
@"bearing" : @(bearing),
|
||||
@"pitch" : @(pitch),
|
||||
@"userTrackingMode" : @(userTrackingMode),
|
||||
@"showsUserLocation" : @(showsUserLocation),
|
||||
@"debug" : @(debug),
|
||||
|
||||
@@ -85,6 +85,7 @@ CLLocationDegrees MGLDegreesFromRadians(CGFloat radians)
|
||||
@property (nonatomic) UIPinchGestureRecognizer *pinch;
|
||||
@property (nonatomic) UIRotationGestureRecognizer *rotate;
|
||||
@property (nonatomic) UILongPressGestureRecognizer *quickZoom;
|
||||
@property (nonatomic) UIPanGestureRecognizer *twoFingerDrag;
|
||||
@property (nonatomic) NSMapTable *annotationIDsByAnnotation;
|
||||
@property (nonatomic) NS_MUTABLE_DICTIONARY_OF(NSString *, MGLAnnotationImage *) *annotationImages;
|
||||
@property (nonatomic) std::vector<uint32_t> annotationsNearbyLastTap;
|
||||
@@ -338,6 +339,15 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration)
|
||||
[twoFingerTap requireGestureRecognizerToFail:_pinch];
|
||||
[twoFingerTap requireGestureRecognizerToFail:_rotate];
|
||||
[self addGestureRecognizer:twoFingerTap];
|
||||
|
||||
_twoFingerDrag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTwoFingerDragGesture:)];
|
||||
_twoFingerDrag.minimumNumberOfTouches = 2;
|
||||
_twoFingerDrag.maximumNumberOfTouches = 2;
|
||||
_twoFingerDrag.delegate = self;
|
||||
[_twoFingerDrag requireGestureRecognizerToFail:twoFingerTap];
|
||||
[_twoFingerDrag requireGestureRecognizerToFail:_pan];
|
||||
[self addGestureRecognizer:_twoFingerDrag];
|
||||
_pitchEnabled = YES;
|
||||
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone)
|
||||
{
|
||||
@@ -1326,6 +1336,59 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration)
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTwoFingerDragGesture:(UIPanGestureRecognizer *)twoFingerDrag
|
||||
{
|
||||
if ( ! self.isPitchEnabled) return;
|
||||
|
||||
_mbglMap->cancelTransitions();
|
||||
|
||||
if (twoFingerDrag.state == UIGestureRecognizerStateBegan)
|
||||
{
|
||||
[self trackGestureEvent:MGLEventGesturePitchStart forRecognizer:twoFingerDrag];
|
||||
}
|
||||
else if (twoFingerDrag.state == UIGestureRecognizerStateBegan || twoFingerDrag.state == UIGestureRecognizerStateChanged)
|
||||
{
|
||||
CGFloat gestureDistance = CGPoint([twoFingerDrag translationInView:twoFingerDrag.view]).y;
|
||||
double currentPitch = _mbglMap->getPitch();
|
||||
double minPitch = 0;
|
||||
double maxPitch = 60.0;
|
||||
double slowdown = 20.0;
|
||||
|
||||
double pitchNew = fmax(fmin(currentPitch - (gestureDistance / slowdown), maxPitch), minPitch);
|
||||
|
||||
_mbglMap->setPitch(pitchNew);
|
||||
}
|
||||
else if (twoFingerDrag.state == UIGestureRecognizerStateEnded || twoFingerDrag.state == UIGestureRecognizerStateCancelled)
|
||||
{
|
||||
[self unrotateIfNeededAnimated:YES];
|
||||
|
||||
//[self notifyMapChange:(mbgl::MapChangeRegionDidChange)];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
|
||||
{
|
||||
UIPanGestureRecognizer *panGesture = (UIPanGestureRecognizer *)gestureRecognizer;
|
||||
|
||||
if (panGesture.minimumNumberOfTouches == 2)
|
||||
{
|
||||
CGPoint velocity = [panGesture velocityInView:panGesture.view];
|
||||
double gestureAngle = MGLDegreesFromRadians(atan(velocity.y / velocity.x));
|
||||
double horizontalToleranceDegrees = 20.0;
|
||||
|
||||
// cancel if gesture angle is not 90º±20º (more or less vertical)
|
||||
if ( ! (fabs((fabs(gestureAngle) - 90.0)) < horizontalToleranceDegrees))
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)handleCalloutAccessoryTapGesture:(UITapGestureRecognizer *)tap
|
||||
{
|
||||
if ([self.delegate respondsToSelector:@selector(mapView:annotation:calloutAccessoryControlTapped:)])
|
||||
@@ -1419,6 +1482,11 @@ std::chrono::steady_clock::duration secondsAsDuration(float duration)
|
||||
return [NSSet setWithObject:@"allowsRotating"];
|
||||
}
|
||||
|
||||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingPitchEnabled
|
||||
{
|
||||
return [NSSet setWithObject:@"allowsPitching"];
|
||||
}
|
||||
|
||||
- (void)setDebugActive:(BOOL)debugActive
|
||||
{
|
||||
_mbglMap->setDebug(debugActive);
|
||||
@@ -1617,6 +1685,25 @@ mbgl::LatLngBounds MGLLatLngBoundsFromCoordinateBounds(MGLCoordinateBounds coord
|
||||
[self setDirection:direction animated:NO];
|
||||
}
|
||||
|
||||
- (double)pitch
|
||||
{
|
||||
return _mbglMap->getPitch();
|
||||
}
|
||||
|
||||
- (void)setPitch:(double)pitch
|
||||
{
|
||||
// constrain pitch to between 0º and 60º
|
||||
//
|
||||
_mbglMap->setPitch(fmax(fmin(pitch, 60), 0));
|
||||
|
||||
//[self notifyMapChange:(mbgl::MapChangeRegionDidChange)];
|
||||
}
|
||||
|
||||
- (void)resetPitch
|
||||
{
|
||||
[self setPitch:0];
|
||||
}
|
||||
|
||||
- (CLLocationCoordinate2D)convertPoint:(CGPoint)point toCoordinateFromView:(nullable UIView *)view
|
||||
{
|
||||
CGPoint convertedPoint = [self convertPoint:point fromView:view];
|
||||
@@ -2630,10 +2717,11 @@ CLLocationCoordinate2D MGLLocationCoordinate2DFromLatLng(mbgl::LatLng latLng)
|
||||
{
|
||||
[self updateCompass];
|
||||
|
||||
if (self.pan.state == UIGestureRecognizerStateChanged ||
|
||||
self.pinch.state == UIGestureRecognizerStateChanged ||
|
||||
self.rotate.state == UIGestureRecognizerStateChanged ||
|
||||
self.quickZoom.state == UIGestureRecognizerStateChanged) return;
|
||||
if (self.pan.state == UIGestureRecognizerStateChanged ||
|
||||
self.pinch.state == UIGestureRecognizerStateChanged ||
|
||||
self.rotate.state == UIGestureRecognizerStateChanged ||
|
||||
self.quickZoom.state == UIGestureRecognizerStateChanged ||
|
||||
self.twoFingerDrag.state == UIGestureRecognizerStateChanged) return;
|
||||
|
||||
if (self.isAnimatingGesture) return;
|
||||
|
||||
@@ -3062,6 +3150,21 @@ class MBGLView : public mbgl::View
|
||||
self.rotateEnabled = allowsRotating;
|
||||
}
|
||||
|
||||
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingAllowsPitching
|
||||
{
|
||||
return [NSSet setWithObject:@"pitchEnabled"];
|
||||
}
|
||||
|
||||
- (BOOL)allowsPitching
|
||||
{
|
||||
return self.pitchEnabled;
|
||||
}
|
||||
|
||||
- (void)setAllowsPitching:(BOOL)allowsPitching
|
||||
{
|
||||
self.pitchEnabled = allowsPitching;
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
_mbglMap->onLowMemory();
|
||||
|
||||
@@ -30,6 +30,7 @@ extern NSString *const MGLEventGestureQuickZoom;
|
||||
extern NSString *const MGLEventGesturePanStart;
|
||||
extern NSString *const MGLEventGesturePinchStart;
|
||||
extern NSString *const MGLEventGestureRotateStart;
|
||||
extern NSString *const MGLEventGesturePitchStart;
|
||||
|
||||
typedef NS_DICTIONARY_OF(NSString *, id) MGLMapboxEventAttributes;
|
||||
typedef NS_MUTABLE_DICTIONARY_OF(NSString *, id) MGLMutableMapboxEventAttributes;
|
||||
|
||||
@@ -45,6 +45,7 @@ NSString *const MGLEventGestureQuickZoom = @"QuickZoom";
|
||||
NSString *const MGLEventGesturePanStart = @"Pan";
|
||||
NSString *const MGLEventGesturePinchStart = @"Pinch";
|
||||
NSString *const MGLEventGestureRotateStart = @"Rotation";
|
||||
NSString *const MGLEventGesturePitchStart = @"Pitch";
|
||||
|
||||
const NSUInteger MGLMaximumEventsPerFlush = 20;
|
||||
const NSTimeInterval MGLFlushInterval = 60;
|
||||
|
||||
Reference in New Issue
Block a user