iOS perspective gesture support

Drag two fingers upward to tilt the map.

Implements #2116
This commit is contained in:
Jason Wray
2015-08-18 16:25:40 -04:00
parent d8ed33c7d4
commit 42dc5336db
7 changed files with 136 additions and 4 deletions

View File

@@ -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 */

View File

@@ -17,6 +17,7 @@ public:
double latitude = 0;
double zoom = 0;
double bearing = 0;
double pitch = 0;
MGLUserTrackingMode userTrackingMode = MGLUserTrackingModeNone;
bool showsUserLocation = false;

View File

@@ -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];

View File

@@ -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),

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;