diff options
author | Minh Nguyễn <mxn@1ec5.org> | 2015-05-16 11:23:45 -0700 |
---|---|---|
committer | Minh Nguyễn <mxn@1ec5.org> | 2016-04-25 00:18:57 -0700 |
commit | 7cc3928a19ffc40e2835264f1349dc7a07fd4017 (patch) | |
tree | 38cfa365bc77196be45787cbff7be555b4994031 | |
parent | 9d3269eaa36d929de6191e60d7b332919ae02cab (diff) | |
download | qtlocation-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.m | 5 | ||||
-rw-r--r-- | platform/ios/app/MBXViewController.m | 18 | ||||
-rw-r--r-- | platform/ios/src/MGLCalloutView.h | 7 | ||||
-rw-r--r-- | platform/ios/src/MGLMapView.mm | 133 | ||||
-rw-r--r-- | platform/ios/src/MGLUserLocationAnnotationView.m | 6 |
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]; |