diff options
author | Julian Rex <julian.rex@gmail.com> | 2018-03-21 13:51:10 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-03-21 13:51:10 -0400 |
commit | d4f2bc07ca424fe6c4a596dac91ab247634e7aab (patch) | |
tree | c41ac7519c8c0ddca43faa61b345a56b0fa32058 /platform/ios/src | |
parent | 4b3d0638b8029421c0fb3f380f4d482cfd74f00e (diff) | |
download | qtlocation-mapboxgl-d4f2bc07ca424fe6c4a596dac91ab247634e7aab.tar.gz |
[ios,macos] Selecting offscreen annotation pans map to fit annotation & callout view (#3249, #9790)
Diffstat (limited to 'platform/ios/src')
-rw-r--r-- | platform/ios/src/MGLCalloutView.h | 25 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.h | 23 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 101 |
3 files changed, 126 insertions, 23 deletions
diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h index 0481a39680..7e7cf2d02e 100644 --- a/platform/ios/src/MGLCalloutView.h +++ b/platform/ios/src/MGLCalloutView.h @@ -41,6 +41,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; + +/** + Presents a callout view by adding it to `view` and pointing at the given rect + of `view`’s bounds. Constrains the callout to the rect in the space of `view`. + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToRect:(CGRect)constrainedRect animated:(BOOL)animated; + /** Dismisses the callout view. */ @@ -49,6 +56,24 @@ NS_ASSUME_NONNULL_BEGIN @optional /** + If implemented, should provide margins to expand the rect the callout is presented from. + + These are used to determine positioning. Currently only the top and bottom properties of the return + value are used. For example, `{ .top = -50.0, .left = -10.0, .bottom = 0.0, .right = -10.0 }` indicates + a 50 point margin above the presentation origin rect (and 10 point margins to the left and the right) + in which the callout is assumed to be displayed. + + There are no assumed defaults for these margins, as they should be calculated from the callout that + is to be presented. For example, `SMCalloutView` generates the top margin from the callout height, but + the left and right margins from a minimum width that the callout should have. + + @param rect Rect that the callout is presented from. This should be the same as the one passed in + `-[MGLCalloutView presentCalloutFromRect:inView:constrainedToRect:animated:]` + @return `UIEdgeInsets` representing the margins. Values should be negative. + */ +- (UIEdgeInsets)marginInsetsHintForPresentationFromRect:(CGRect)rect NS_SWIFT_NAME(marginInsetsHintForPresentation(from:)); + +/** A Boolean value indicating whether the callout view should be anchored to the corresponding annotation. You can adjust the callout view’s precise location by overriding -[UIView setCenter:]. The callout view will not be anchored to the diff --git a/platform/ios/src/MGLMapView.h b/platform/ios/src/MGLMapView.h index 52d28d871c..2d566f26a0 100644 --- a/platform/ios/src/MGLMapView.h +++ b/platform/ios/src/MGLMapView.h @@ -1200,17 +1200,32 @@ MGL_EXPORT IB_DESIGNABLE Assigning a new array to this property selects only the first annotation in the array. + + If the annotation is of type `MGLPointAnnotation` and is offscreen, the camera + will animate to bring the annotation and its callout just on screen. If you + need finer control, consider using `-selectAnnotation:animated:`. + + @note In versions prior to `4.0.0` if the annotation was offscreen it was not + selected. */ @property (nonatomic, copy) NS_ARRAY_OF(id <MGLAnnotation>) *selectedAnnotations; /** - Selects an annotation and displays a callout view for it. + Selects an annotation and displays its callout view. - If the given annotation is not visible within the current viewport, this - method has no effect. + The `animated` parameter determines whether the map is panned to bring the + annotation on-screen, specifically: + + | `animated` parameter | Effect | + |------------------|--------| + | `NO` | The annotation is selected, and the callout is presented. However the map is not panned to bring the annotation or callout onscreen. The presentation of the callout is animated. | + | `YES` | The annotation is selected, and the callout is presented. If the annotation is offscreen *and* is of type `MGLPointAnnotation`, the map is panned so that the annotation and its callout are brought just onscreen. The annotation is *not* centered within the viewport. | @param annotation The annotation object to select. - @param animated If `YES`, the callout view is animated into position. + @param animated If `YES`, the annotation and callout view are animated on-screen. + + @note In versions prior to `4.0.0` selecting an offscreen annotation did not + change the camera. */ - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated; diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 6b47dc05f7..3daea33367 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -144,6 +144,9 @@ const CGFloat MGLAnnotationImagePaddingForCallout = 1; const CGSize MGLAnnotationAccessibilityElementMinimumSize = CGSizeMake(10, 10); +/// Padding to edge of view that an offscreen annotation must have when being brought onscreen (by being selected) +const UIEdgeInsets MGLMapViewOffscreenAnnotationPadding = UIEdgeInsetsMake(-20.0f, -20.0f, -20.0f, -20.0f); + /// An indication that the requested annotation was not found or is nonexistent. enum { MGLAnnotationTagNotFound = UINT32_MAX }; @@ -272,7 +275,7 @@ public: CADisplayLink *_displayLink; BOOL _needsDisplayRefresh; - NSUInteger _changeDelimiterSuppressionDepth; + NSInteger _changeDelimiterSuppressionDepth; /// Center coordinate of the pinch gesture on the previous iteration of the gesture. CLLocationCoordinate2D _previousPinchCenterCoordinate; @@ -1625,7 +1628,7 @@ public: { CGPoint calloutPoint = [singleTap locationInView:self]; CGRect positionRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:calloutPoint]; - [self selectAnnotation:annotation animated:YES calloutPositioningRect:positionRect]; + [self selectAnnotation:annotation moveOnscreen:YES animateSelection:YES calloutPositioningRect:positionRect]; } else if (self.selectedAnnotation) { @@ -4234,6 +4237,12 @@ public: }); } + +- (BOOL)isBringingAnnotationOnscreenSupportedForAnnotation:(id<MGLAnnotation>)annotation animated:(BOOL)animated { + // Consider delegating + return animated && [annotation isKindOfClass:[MGLPointAnnotation class]]; +} + - (id <MGLAnnotation>)selectedAnnotation { if (_userLocationAnnotationIsSelected) @@ -4274,20 +4283,16 @@ public: if ([firstAnnotation isKindOfClass:[MGLMultiPoint class]]) return; - // Select the annotation if it’s visible. - if (MGLCoordinateInCoordinateBounds(firstAnnotation.coordinate, self.visibleCoordinateBounds)) - { - [self selectAnnotation:firstAnnotation animated:NO]; - } + [self selectAnnotation:firstAnnotation animated:YES]; } - (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated { CGRect positioningRect = [self positioningRectForAnnotation:annotation defaultCalloutPoint:CGPointZero]; - [self selectAnnotation:annotation animated:animated calloutPositioningRect:positioningRect]; + [self selectAnnotation:annotation moveOnscreen:animated animateSelection:YES calloutPositioningRect:positioningRect]; } -- (void)selectAnnotation:(id <MGLAnnotation>)annotation animated:(BOOL)animated calloutPositioningRect:(CGRect)calloutPositioningRect +- (void)selectAnnotation:(id <MGLAnnotation>)annotation moveOnscreen:(BOOL)moveOnscreen animateSelection:(BOOL)animateSelection calloutPositioningRect:(CGRect)calloutPositioningRect { if ( ! annotation) return; @@ -4311,22 +4316,34 @@ public: MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); annotationView = annotationContext.annotationView; if (annotationView && annotationView.enabled) { - // Annotations represented by views use the view frame as the positioning rect. - calloutPositioningRect = annotationView.frame; - [annotationView.superview bringSubviewToFront:annotationView]; - [annotationView setSelected:YES animated:animated]; + // Annotations represented by views use the view frame as the positioning rect. + calloutPositioningRect = annotationView.frame; + [annotationView.superview bringSubviewToFront:annotationView]; + + [annotationView setSelected:YES animated:animateSelection]; } } self.selectedAnnotation = annotation; + // Determine if we're allowed to move this offscreen annotation on screen, even though we've asked it to + if (moveOnscreen) { + moveOnscreen = [self isBringingAnnotationOnscreenSupportedForAnnotation:annotation animated:animateSelection]; + } + + CGRect expandedPositioningRect = UIEdgeInsetsInsetRect(calloutPositioningRect, MGLMapViewOffscreenAnnotationPadding); + + // Used for callout positioning, and moving offscreen annotations onscreen. + CGRect constrainedRect = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + UIView <MGLCalloutView> *calloutView = nil; + if ([annotation respondsToSelector:@selector(title)] && annotation.title && [self.delegate respondsToSelector:@selector(mapView:annotationCanShowCallout:)] && [self.delegate mapView:self annotationCanShowCallout:annotation]) { // build the callout - UIView <MGLCalloutView> *calloutView; if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)]) { id providedCalloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation]; @@ -4384,13 +4401,51 @@ public: // set annotation delegate to handle taps on the callout view calloutView.delegate = self; - // present popup - [calloutView presentCalloutFromRect:calloutPositioningRect - inView:self.glView - constrainedToView:self.glView - animated:animated]; + // If the callout view provides inset (outset) information, we can use it to expand our positioning + // rect, which we then use to help move the annotation on-screen if want need to. + if (moveOnscreen && [calloutView respondsToSelector:@selector(marginInsetsHintForPresentationFromRect:)]) { + UIEdgeInsets margins = [calloutView marginInsetsHintForPresentationFromRect:calloutPositioningRect]; + expandedPositioningRect = UIEdgeInsetsInsetRect(expandedPositioningRect, margins); + } + } + + if (moveOnscreen) + { + moveOnscreen = NO; + + // Need to consider the content insets. + CGRect bounds = UIEdgeInsetsInsetRect(self.bounds, self.contentInset); + + // Any one of these cases should trigger a move onscreen + if (CGRectGetMinX(calloutPositioningRect) < CGRectGetMinX(bounds)) + { + constrainedRect.origin.x = expandedPositioningRect.origin.x; + moveOnscreen = YES; + } + else if (CGRectGetMaxX(calloutPositioningRect) > CGRectGetMaxX(bounds)) + { + constrainedRect.origin.x = CGRectGetMaxX(expandedPositioningRect) - constrainedRect.size.width; + moveOnscreen = YES; + } + + if (CGRectGetMinY(calloutPositioningRect) < CGRectGetMinY(bounds)) + { + constrainedRect.origin.y = expandedPositioningRect.origin.y; + moveOnscreen = YES; + } + else if (CGRectGetMaxY(calloutPositioningRect) > CGRectGetMaxY(bounds)) + { + constrainedRect.origin.y = CGRectGetMaxY(expandedPositioningRect) - constrainedRect.size.height; + moveOnscreen = YES; + } } + // Remember, calloutView can be nil here. + [calloutView presentCalloutFromRect:calloutPositioningRect + inView:self.glView + constrainedToRect:constrainedRect + animated:animateSelection]; + // notify delegate if ([self.delegate respondsToSelector:@selector(mapView:didSelectAnnotation:)]) { @@ -4401,6 +4456,13 @@ public: { [self.delegate mapView:self didSelectAnnotationView:annotationView]; } + + if (moveOnscreen) + { + CGPoint center = CGPointMake(CGRectGetMidX(constrainedRect), CGRectGetMidY(constrainedRect)); + CLLocationCoordinate2D centerCoord = [self convertPoint:center toCoordinateFromView:self]; + [self setCenterCoordinate:centerCoord animated:animateSelection]; + } } - (MGLCompactCalloutView *)calloutViewForAnnotation:(id <MGLAnnotation>)annotation @@ -4591,6 +4653,7 @@ public: animated:animated]; } + #pragma mark Annotation Image Delegate - (void)annotationImageNeedsRedisplay:(MGLAnnotationImage *)annotationImage |