-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[ios] keep callout view open when panning #6676
Conversation
@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]; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
974fb16
to
7a6733c
Compare
|| self.userTrackingMode == MGLUserTrackingModeNone | ||
|| self.userTrackingState != MGLUserTrackingStateChanged) | ||
{ | ||
[self deselectAnnotation:self.selectedAnnotation animated:NO]; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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).
👍 |
There was a problem hiding this 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.
Bumpin’ — I think this is good to go, once there’s a changelog entry? |
Come to think of it, we should probably still deselect the annotation it goes off-screen. We do that for the user location annotation. |
Good catch @friedbunny. It only works for annotation views so far. |
232817a fixes #6676 (comment) but the implementation is broken. The callout view shouldn't be positioned in |
3511e71
to
46afa93
Compare
UIView <MGLCalloutView> *calloutView = self.calloutViewForSelectedAnnotation; | ||
if (calloutView && calloutView.representedObject == annotationContext.annotation) { | ||
CGPoint point = annotationView.center; | ||
point.y -= annotationView.bounds.size.height/2.0f; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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)
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:)]; |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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:
.
16df3aa
to
5bffd38
Compare
if (image) | ||
{ | ||
CGRect rect = [self positioningRectForCalloutForAnnotationWithTag:tag]; | ||
CGRect insetRect = UIEdgeInsetsInsetRect(rect, image.image.alignmentRectInsets); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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:)]) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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:
.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this 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 |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
89f29f4
to
47a57f6
Compare
47a57f6
to
307b4d5
Compare
Thanks for iterating with me on this one. I hope one final glance is sufficient. |
Fixes #3154
An open callout view now stays open while panning around.
Tapping the map view will dismiss the callout view.