summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMinh Nguyễn <mxn@1ec5.org>2015-05-16 11:23:45 -0700
committerMinh Nguyễn <mxn@1ec5.org>2016-04-25 00:18:57 -0700
commit7cc3928a19ffc40e2835264f1349dc7a07fd4017 (patch)
tree38cfa365bc77196be45787cbff7be555b4994031
parent9d3269eaa36d929de6191e60d7b332919ae02cab (diff)
downloadqtlocation-mapboxgl-7cc3928a19ffc40e2835264f1349dc7a07fd4017.tar.gz
[ios] Made annotation callouts accessible
Via nfarina/calloutview#84, SMCalloutView is now accessible. Activating a focused annotation now shows its callout view and focuses its left accessory view, if present, or the title view. There is a “return to map” accessibility element for dismissing the callout view and restoring focus to the annotation on the map.
-rw-r--r--platform/ios/app/MBXCustomCalloutView.m5
-rw-r--r--platform/ios/app/MBXViewController.m18
-rw-r--r--platform/ios/src/MGLCalloutView.h7
-rw-r--r--platform/ios/src/MGLMapView.mm133
-rw-r--r--platform/ios/src/MGLUserLocationAnnotationView.m6
5 files changed, 142 insertions, 27 deletions
diff --git a/platform/ios/app/MBXCustomCalloutView.m b/platform/ios/app/MBXCustomCalloutView.m
index 11ce86e76a..9edc00f6e9 100644
--- a/platform/ios/app/MBXCustomCalloutView.m
+++ b/platform/ios/app/MBXCustomCalloutView.m
@@ -59,6 +59,11 @@ static CGFloat const tipWidth = 10.0;
CGFloat frameOriginY = rect.origin.y - frameHeight;
self.frame = CGRectMake(frameOriginX, frameOriginY,
frameWidth, frameHeight);
+
+ if ([self.delegate respondsToSelector:@selector(calloutViewDidAppear:)])
+ {
+ [self.delegate performSelector:@selector(calloutViewDidAppear:) withObject:self];
+ }
}
- (void)dismissCalloutAnimated:(BOOL)animated
diff --git a/platform/ios/app/MBXViewController.m b/platform/ios/app/MBXViewController.m
index 8f628e8126..098fc7b744 100644
--- a/platform/ios/app/MBXViewController.m
+++ b/platform/ios/app/MBXViewController.m
@@ -739,6 +739,24 @@ static const CLLocationCoordinate2D WorldTourDestinations[] = {
return nil;
}
+- (UIView *)mapView:(__unused MGLMapView *)mapView leftCalloutAccessoryViewForAnnotation:(__unused id<MGLAnnotation>)annotation
+{
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
+ button.frame = CGRectZero;
+ [button setTitle:@"Left" forState:UIControlStateNormal];
+ [button sizeToFit];
+ return button;
+}
+
+- (UIView *)mapView:(__unused MGLMapView *)mapView rightCalloutAccessoryViewForAnnotation:(__unused id<MGLAnnotation>)annotation
+{
+ UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
+ button.frame = CGRectZero;
+ [button setTitle:@"Right" forState:UIControlStateNormal];
+ [button sizeToFit];
+ return button;
+}
+
- (void)mapView:(MGLMapView *)mapView tapOnCalloutForAnnotation:(id <MGLAnnotation>)annotation
{
if ( ! [annotation isKindOfClass:[MGLPointAnnotation class]])
diff --git a/platform/ios/src/MGLCalloutView.h b/platform/ios/src/MGLCalloutView.h
index 59f52adb6d..641976dfee 100644
--- a/platform/ios/src/MGLCalloutView.h
+++ b/platform/ios/src/MGLCalloutView.h
@@ -67,6 +67,11 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (void)calloutViewWillAppear:(UIView<MGLCalloutView> *)calloutView;
+/**
+ Called after the callout view appears on screen, or after the appearance animation is complete.
+ */
+- (void)calloutViewDidAppear:(UIView<MGLCalloutView> *)calloutView;
+
@end
-NS_ASSUME_NONNULL_END \ No newline at end of file
+NS_ASSUME_NONNULL_END
diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index 72f3bc915b..f6a0cb4904 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -148,6 +148,26 @@ public:
MGLAnnotationAccessibilityElement *accessibilityElement;
};
+/** An accessibility element representing the MGLMapView at large. */
+@interface MGLMapViewProxyAccessibilityElement : UIAccessibilityElement
+
+@end
+
+@implementation MGLMapViewProxyAccessibilityElement
+
+- (instancetype)initWithAccessibilityContainer:(id)container
+{
+ if (self = [super initWithAccessibilityContainer:container])
+ {
+ self.accessibilityTraits = UIAccessibilityTraitButton;
+ self.accessibilityLabel = self.accessibilityLabel;
+ self.accessibilityHint = @"Returns to the map";
+ }
+ return self;
+}
+
+@end
+
#pragma mark - Private -
@interface MGLMapView () <UIGestureRecognizerDelegate,
@@ -187,6 +207,7 @@ public:
@property (nonatomic) CGFloat quickZoomStart;
@property (nonatomic, getter=isDormant) BOOL dormant;
@property (nonatomic, readonly, getter=isRotationAllowed) BOOL rotationAllowed;
+@property (nonatomic) UIAccessibilityElement *mapViewProxyAccessibilityElement;
@end
@@ -1316,6 +1337,22 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
return;
}
[self trackGestureEvent:MGLEventGestureSingleTap forRecognizer:singleTap];
+
+ if (self.mapViewProxyAccessibilityElement.accessibilityElementIsFocused)
+ {
+ id nextElement;
+ if (_userLocationAnnotationIsSelected)
+ {
+ nextElement = self.userLocationAnnotationView;
+ }
+ else
+ {
+ nextElement = _annotationContextsByAnnotationTag[_selectedAnnotationTag].accessibilityElement;
+ }
+ [self deselectAnnotation:self.selectedAnnotation animated:YES];
+ UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nextElement);
+ return;
+ }
CGPoint tapPoint = [singleTap locationInView:self];
@@ -1534,6 +1571,12 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
}
}
+- (void)calloutViewDidAppear:(UIView<MGLCalloutView> *)calloutView
+{
+ UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil);
+ UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, calloutView);
+}
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
NSArray *validSimultaneousGestures = @[ self.pan, self.pinch, self.rotate ];
@@ -1789,25 +1832,53 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
UIViewController *viewController = self.viewControllerForLayoutGuides;
if (viewController)
{
- UIView *compassContainer = self.compassView.superview;
- CGFloat topInset = compassContainer.frame.origin.y + compassContainer.frame.size.height + 5;
+ CGFloat topInset = viewController.topLayoutGuide.length;
frame.origin.y += topInset;
- frame.size.height -= topInset;
-
- CGFloat bottomInset = MIN(self.logoView.frame.origin.y, self.attributionButton.frame.origin.y) - 8;
- frame.size.height = bottomInset - frame.origin.y;
+ frame.size.height -= topInset + viewController.bottomLayoutGuide.length;
}
return frame;
}
+- (UIBezierPath *)accessibilityPath
+{
+ UIBezierPath *path = [UIBezierPath bezierPathWithRect:self.accessibilityFrame];
+
+ // Exclude any visible annotation callout view.
+ if (self.calloutViewForSelectedAnnotation)
+ {
+ UIBezierPath *calloutViewPath = [UIBezierPath bezierPathWithRect:self.calloutViewForSelectedAnnotation.frame];
+ [path appendPath:calloutViewPath];
+ }
+
+ return path;
+}
+
- (NSInteger)accessibilityElementCount
{
+ if (self.calloutViewForSelectedAnnotation)
+ {
+ return 2 /* selectedAnnotationCalloutView, mapViewProxyAccessibilityElement */;
+ }
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
return visibleAnnotations.size() + 3 /* compass, userLocationAnnotationView, attributionButton */;
}
- (id)accessibilityElementAtIndex:(NSInteger)index
{
+ if (self.calloutViewForSelectedAnnotation)
+ {
+ if (index == 0)
+ {
+ return self.calloutViewForSelectedAnnotation;
+ }
+ if (index == 1)
+ {
+ self.mapViewProxyAccessibilityElement.accessibilityFrame = self.accessibilityFrame;
+ self.mapViewProxyAccessibilityElement.accessibilityPath = self.accessibilityPath;
+ return self.mapViewProxyAccessibilityElement;
+ }
+ return nil;
+ }
std::vector<MGLAnnotationTag> visibleAnnotations = [self annotationTagsInRect:self.bounds];
// Ornaments
@@ -1881,6 +1952,11 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
- (NSInteger)indexOfAccessibilityElement:(id)element
{
+ if (self.calloutViewForSelectedAnnotation)
+ {
+ return [@[self.calloutViewForSelectedAnnotation, self.mapViewProxyAccessibilityElement]
+ indexOfObject:element];
+ }
if (element == self.compassView)
{
return 0;
@@ -1907,6 +1983,15 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
else return std::distance(visibleAnnotations.begin(), foundElement) + 2 /* compass, userLocationAnnotationView */;
}
+- (UIAccessibilityElement *)mapViewProxyAccessibilityElement
+{
+ if ( ! _mapViewProxyAccessibilityElement)
+ {
+ _mapViewProxyAccessibilityElement = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self];
+ }
+ return _mapViewProxyAccessibilityElement;
+}
+
#pragma mark - Geography -
+ (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate
@@ -3062,14 +3147,16 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
[self.delegate mapView:self annotationCanShowCallout:annotation])
{
// build the callout
+ UIView <MGLCalloutView> *calloutView;
if ([self.delegate respondsToSelector:@selector(mapView:calloutViewForAnnotation:)])
{
- self.calloutViewForSelectedAnnotation = [self.delegate mapView:self calloutViewForAnnotation:annotation];
+ calloutView = [self.delegate mapView:self calloutViewForAnnotation:annotation];
}
- if (!self.calloutViewForSelectedAnnotation)
+ if (!calloutView)
{
- self.calloutViewForSelectedAnnotation = [self calloutViewForAnnotation:annotation];
+ calloutView = [self calloutViewForAnnotation:annotation];
}
+ self.calloutViewForSelectedAnnotation = calloutView;
if (_userLocationAnnotationIsSelected)
{
@@ -3084,41 +3171,38 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
// consult delegate for left and/or right accessory views
if ([self.delegate respondsToSelector:@selector(mapView:leftCalloutAccessoryViewForAnnotation:)])
{
- self.calloutViewForSelectedAnnotation.leftAccessoryView =
- [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];
+ calloutView.leftAccessoryView = [self.delegate mapView:self leftCalloutAccessoryViewForAnnotation:annotation];
- if ([self.calloutViewForSelectedAnnotation.leftAccessoryView isKindOfClass:[UIControl class]])
+ if ([calloutView.leftAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleCalloutAccessoryTapGesture:)];
- [self.calloutViewForSelectedAnnotation.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
+ [calloutView.leftAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
}
if ([self.delegate respondsToSelector:@selector(mapView:rightCalloutAccessoryViewForAnnotation:)])
{
- self.calloutViewForSelectedAnnotation.rightAccessoryView =
- [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];
+ calloutView.rightAccessoryView = [self.delegate mapView:self rightCalloutAccessoryViewForAnnotation:annotation];
- if ([self.calloutViewForSelectedAnnotation.rightAccessoryView isKindOfClass:[UIControl class]])
+ if ([calloutView.rightAccessoryView isKindOfClass:[UIControl class]])
{
UITapGestureRecognizer *calloutAccessoryTap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleCalloutAccessoryTapGesture:)];
- [self.calloutViewForSelectedAnnotation.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
+ [calloutView.rightAccessoryView addGestureRecognizer:calloutAccessoryTap];
}
}
// set annotation delegate to handle taps on the callout view
- self.calloutViewForSelectedAnnotation.delegate = self;
+ calloutView.delegate = self;
// present popup
- [self.calloutViewForSelectedAnnotation presentCalloutFromRect:positioningRect
- inView:self.glView
- constrainedToView:self.glView
- animated:animated];
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
+ [calloutView presentCalloutFromRect:positioningRect
+ inView:self.glView
+ constrainedToView:self.glView
+ animated:animated];
}
// notify delegate
@@ -3200,8 +3284,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
self.calloutViewForSelectedAnnotation = nil;
self.selectedAnnotation = nil;
- UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil);
-
// notify delegate
if ([self.delegate respondsToSelector:@selector(mapView:didDeselectAnnotation:)])
{
@@ -3391,7 +3473,6 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration)
self.userLocationAnnotationView = [[MGLUserLocationAnnotationView alloc] initInMapView:self];
self.userLocationAnnotationView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
- self.userLocationAnnotationView.isAccessibilityElement = YES;
[self validateLocationServices];
}
diff --git a/platform/ios/src/MGLUserLocationAnnotationView.m b/platform/ios/src/MGLUserLocationAnnotationView.m
index 74908ec4e5..d4f4a23fbd 100644
--- a/platform/ios/src/MGLUserLocationAnnotationView.m
+++ b/platform/ios/src/MGLUserLocationAnnotationView.m
@@ -57,6 +57,7 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
self.annotation = [[MGLUserLocation alloc] initWithMapView:mapView];
_mapView = mapView;
[self setupLayers];
+ self.isAccessibilityElement = YES;
self.accessibilityTraits = UIAccessibilityTraitButton;
_accessibilityCoordinateFormatter = [[MGLCoordinateFormatter alloc] init];
@@ -97,6 +98,11 @@ const CGFloat MGLUserLocationAnnotationArrowSize = MGLUserLocationAnnotationPuck
return [_accessibilityCoordinateFormatter stringFromCoordinate:self.mapView.centerCoordinate];
}
+- (CGRect)accessibilityFrame
+{
+ return CGRectInset(self.frame, -15, -15);
+}
+
- (UIBezierPath *)accessibilityPath
{
return [UIBezierPath bezierPathWithOvalInRect:self.frame];