Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

[ios] keep callout view open when panning #6676

Merged
merged 1 commit into from
Dec 5, 2016

Conversation

frederoni
Copy link
Contributor

@frederoni frederoni commented Oct 12, 2016

Fixes #3154
An open callout view now stays open while panning around.
Tapping the map view will dismiss the callout view.

animation

@frederoni frederoni added iOS Mapbox Maps SDK for iOS needs discussion labels Oct 12, 2016
@frederoni frederoni self-assigned this Oct 12, 2016
@mention-bot
Copy link

@frederoni, thanks for your PR! By analyzing the history of the files in this pull request, we identified @1ec5, @incanus and @boundsj to be potential reviewers.

|| self.userTrackingMode == MGLUserTrackingModeNone
|| self.userTrackingState != MGLUserTrackingStateChanged)
{
[self deselectAnnotation:self.selectedAnnotation animated:NO];
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was dismissing the callout the single purpose of deselecting this annotation?

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe so. That's what it was for in the raster SDK...

Copy link
Contributor

Choose a reason for hiding this comment

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

The annotation still needs to be deselected if it lies outside the viewport. See -selectAnnotation:animated:.

UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
if (calloutView && calloutView.representedObject == annotationContext.annotation) {
CGPoint point = annotationView.center;
point.y -= annotationView.bounds.size.height/2.0f;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is center-top alignment appropriate for every case?

Copy link
Contributor

@incanus incanus Oct 12, 2016

Choose a reason for hiding this comment

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

If we want it to look like MapKit did pre-iOS 10 (when it got rid of callouts of this style), then yes. But we don't have now / this PR still doesn't provide a facility for placement which moves the map before presentation the way SMCalloutView does (see here).

Copy link
Contributor

Choose a reason for hiding this comment

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

Eventually we’ll support shifting the viewport to fit the callout (#1089) and it might be nice to support alternative popup directions (bottom, side, etc), but let’s not worry about those until #4392.

Copy link
Contributor

Choose a reason for hiding this comment

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

Eventually we’ll support shifting the viewport to fit the callout (#1089)

More specifically, #3249.

@frederoni frederoni force-pushed the 3154-keep-callout-open branch 2 times, most recently from 974fb16 to 7a6733c Compare October 12, 2016 13:52
|| self.userTrackingMode == MGLUserTrackingModeNone
|| self.userTrackingState != MGLUserTrackingStateChanged)
{
[self deselectAnnotation:self.selectedAnnotation animated:NO];
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe so. That's what it was for in the raster SDK...

UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
if (calloutView && calloutView.representedObject == annotationContext.annotation) {
CGPoint point = annotationView.center;
point.y -= annotationView.bounds.size.height/2.0f;
Copy link
Contributor

@incanus incanus Oct 12, 2016

Choose a reason for hiding this comment

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

If we want it to look like MapKit did pre-iOS 10 (when it got rid of callouts of this style), then yes. But we don't have now / this PR still doesn't provide a facility for placement which moves the map before presentation the way SMCalloutView does (see here).

@incanus
Copy link
Contributor

incanus commented Oct 12, 2016

👍

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

Please update the iOS changelog.

@friedbunny
Copy link
Contributor

Bumpin’ — I think this is good to go, once there’s a changelog entry?

@friedbunny friedbunny added feature annotations Annotations on iOS and macOS or markers on Android and removed needs discussion labels Oct 17, 2016
@friedbunny friedbunny added this to the ios-v3.4.0 milestone Oct 17, 2016
@1ec5
Copy link
Contributor

1ec5 commented Oct 17, 2016

Come to think of it, we should probably still deselect the annotation it goes off-screen. We do that for the user location annotation.

@friedbunny
Copy link
Contributor

I’m not seeing the callout move with the annotation:

asdf

@frederoni
Copy link
Contributor Author

Good catch @friedbunny. It only works for annotation views so far.

@frederoni frederoni added ⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold needs discussion labels Oct 20, 2016
@frederoni
Copy link
Contributor Author

232817a fixes #6676 (comment) but the implementation is broken. The callout view shouldn't be positioned in -[MGLMapView updateAnnotationViews], partially because it's not related but also because it gets interrupted if the delegate doesn't respond to -[MGLMapViewDelegate mapView:viewForAnnotation:]

@frederoni frederoni changed the base branch from master to release-ios-v3.4.0 November 16, 2016 11:45
@frederoni frederoni force-pushed the 3154-keep-callout-open branch 2 times, most recently from 3511e71 to 46afa93 Compare November 22, 2016 16:07
@frederoni frederoni added ✓ ready for review and removed needs discussion ⚠️ DO NOT MERGE Work in progress, proof of concept, or on hold labels Nov 22, 2016
UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
if (calloutView && calloutView.representedObject == annotationContext.annotation) {
CGPoint point = annotationView.center;
point.y -= annotationView.bounds.size.height/2.0f;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn’t account for any centerOffset on the annotation view.

if (image)
{
CGPoint point = [self convertCoordinate:annotation.coordinate toPointToView:self];
point.y -= image.image.size.height/2.0f;
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn’t account for the annotation image’s alignment rect insets.

CGPoint point = annotationView.center;
point.x += annotationView.centerOffset.dx;
point.y += annotationView.centerOffset.dy;
point.y -= annotationView.bounds.size.height/2.0f;
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be simpler to calculate the point based on the annotation view's frame, i.e., CGGetMinY(annotationView.frame).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MinY from the frame is not what I'm trying to achieve here but MidY from the bounds would work.

Copy link
Contributor

@1ec5 1ec5 Dec 1, 2016

Choose a reason for hiding this comment

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

My point was that all four of these lines could become something like:

CGPoint point = CGPointMake(CGGetMidX(annotationView.frame), CGGetMinY(annotationView.frame));

Also, looking back at this, I’m not sure if center offset is already accounted for in the center and frame. I think it may already be.

UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
if (calloutView && calloutView.representedObject == annotationContext.annotation) {
NSObject<MGLAnnotation> *annotation = annotationContext.annotation;
BOOL implementsImageForAnnotation = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
Copy link
Contributor

Choose a reason for hiding this comment

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

Move this outside the loop.

NSObject<MGLAnnotation> *annotation = annotationContext.annotation;
BOOL implementsImageForAnnotation = [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)];
if (implementsImageForAnnotation) {
MGLAnnotationImage *image = [self.delegate mapView:self imageForAnnotation:annotation];
Copy link
Contributor

Choose a reason for hiding this comment

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

Use -imageOfAnnotationWithTag: to avoid asking the delegate for information the map view already has.

}
} else {
// Pin the callout view to the gl annotation
UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation;
Copy link
Contributor

Choose a reason for hiding this comment

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

Like -selectAnnotation:animated:, this method should use -positioningRectForCalloutForAnnotationWithTag:, which accounts for the padding around the annotation image and avoids half-pixel issues. To convert from the positioning rect to an anchor point, use CGRectGetMidX() and CGRectGetMinY.

|| self.userTrackingMode == MGLUserTrackingModeNone
|| self.userTrackingState != MGLUserTrackingStateChanged)
{
[self deselectAnnotation:self.selectedAnnotation animated:NO];
Copy link
Contributor

Choose a reason for hiding this comment

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

The annotation still needs to be deselected if it lies outside the viewport. See -selectAnnotation:animated:.

if (image)
{
CGRect rect = [self positioningRectForCalloutForAnnotationWithTag:tag];
CGRect insetRect = UIEdgeInsetsInsetRect(rect, image.image.alignmentRectInsets);
Copy link
Contributor

Choose a reason for hiding this comment

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

-positioningRectForCalloutForAnnotationWithTag: already calls -frameOfImage:centeredAtCoordinate:, so it already accounts for the alignment rect insets.

image = [self dequeueReusableAnnotationImageWithIdentifier:MGLDefaultStyleMarkerSymbolName];
}

if (image)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it necessary to guard for the presence of an image here? -positioningRectForCalloutForAnnotationWithTag: already looks up the annotation’s image.

{
MGLAnnotationImage *image = [self imageOfAnnotationWithTag:tag];

if (!image && [self.delegate respondsToSelector:@selector(mapView:imageForAnnotation:)]) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think -imageOfAnnotationWithTag: should be good enough. We shouldn't be overusing this delegate method.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, image is unused now that we're getting the positioning rect below.

CGPoint point = annotationView.center;
point.x += annotationView.centerOffset.dx;
point.y += annotationView.centerOffset.dy;
point.y -= CGRectGetMidY(annotationView.bounds);
Copy link
Contributor

Choose a reason for hiding this comment

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

At this point, I think you can pretty much collapse the view and non-view cases together: for view-backed annotations, rect is the view's frame; for GL annotations, rect comes from -positioningRectForCalloutForAnnotationWithTag:.

Copy link
Contributor

Choose a reason for hiding this comment

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

The center (and thus the bounds and frame) already account for the center offset. Sorry for being unclear about that previously. This does mean you can combine the view and non-view cases, just like you do in the MapChangeRegionWillChange handler above.

@@ -4513,7 +4513,26 @@ - (void)notifyMapChange:(mbgl::MapChange)change
|| self.userTrackingMode == MGLUserTrackingModeNone
|| self.userTrackingState != MGLUserTrackingStateChanged)
{
[self deselectAnnotation:self.selectedAnnotation animated:NO];
// Deselect annotation if it lies outside the viewport
Copy link
Contributor

Choose a reason for hiding this comment

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

This is solving for something similar to -[MGLMapView updateAnnotationViews]. I wonder if checking if the -array -[MGLMapView visibleAnnotations] contains the selected annotation would be fast enough. If it is, it would simplify this implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

We already call -visibleAnnotations soon after this in order to reposition annotation views. We could refactor this code to call `-visibleAnnotations upfront and reuse the results for placement of annotation views, the callout view, and (on macOS) tooltip and cursor rects.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think -visibleAnnotations is fine but -[NSArray containsObject:] is O(n2) and takes 2ms with 100 annotations. Not sure if it's in the beginning or the end but it would decrease the performance while panning around.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. Ideally visibleAnnotations would be a set, but unfortunately the way we compute it (using feature querying) means it’s always going to be O(n) regardless. (Not O(n²), because we’re only checking whether one particular annotation is visible.)

Copy link
Contributor

@1ec5 1ec5 left a comment

Choose a reason for hiding this comment

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

A couple remaining things to take care of, but I’ll remove my senatorial hold now. 😉

@@ -4513,7 +4513,26 @@ - (void)notifyMapChange:(mbgl::MapChange)change
|| self.userTrackingMode == MGLUserTrackingModeNone
|| self.userTrackingState != MGLUserTrackingStateChanged)
{
[self deselectAnnotation:self.selectedAnnotation animated:NO];
// Deselect annotation if it lies outside the viewport
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. Ideally visibleAnnotations would be a set, but unfortunately the way we compute it (using feature querying) means it’s always going to be O(n) regardless. (Not O(n²), because we’re only checking whether one particular annotation is visible.)

@@ -4631,6 +4646,9 @@ - (void)updateAnnotationViews

if (!delegateImplementsViewForAnnotation)
{
[CATransaction begin];
[self updateCalloutView];
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you considered moving the calls to -updateCalloutView to directly within the MapChangeDidFinishRenderingFrame handler, after the call to -updateAnnotationViews? That would avoid the double call here, and I think it’d be more intuitive, since we need to reposition the callout view even if there are no view-backed annotations.

For reference, the macOS implementation calls -updateAnnotationCallouts within the MapChangeRegionIsChanging and MapChangeRegionDidChange handlers, which may fire less frequently. It makes sense that we’d want to keep the callout view in sync with any annotation views we update in MapChangeDidFinishRenderingFrame, though.

CGPoint point = annotationView.center;
point.x += annotationView.centerOffset.dx;
point.y += annotationView.centerOffset.dy;
point.y -= CGRectGetMidY(annotationView.bounds);
Copy link
Contributor

Choose a reason for hiding this comment

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

The center (and thus the bounds and frame) already account for the center offset. Sorry for being unclear about that previously. This does mean you can combine the view and non-view cases, just like you do in the MapChangeRegionWillChange handler above.

@frederoni
Copy link
Contributor Author

Thanks for iterating with me on this one. I hope one final glance is sufficient.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
annotations Annotations on iOS and macOS or markers on Android feature iOS Mapbox Maps SDK for iOS
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants