From babc43ea6289a5ec6eee9dbf80057ab2de742dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Mon, 11 May 2015 03:16:37 -0700 Subject: [PATCH] [ios] Made annotations accessible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazily create and cache accessibility elements for visible annotations as UIAccessibility asks about them. Sort the annotations’ accessibility elements by the annotations’ distance from the logical center (the center after accounting for padding, or the user dot when user tracking mode is on). Updated the changelog. Fixes #1493. --- platform/ios/CHANGELOG.md | 1 + platform/ios/src/MGLMapView.mm | 112 +++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index 2377da7cf..e36fd4629 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -9,6 +9,7 @@ Mapbox welcomes participation and contributions from everyone. Please read [CON - The user dot now moves smoothly between user location updates while user location tracking is disabled. ([#1582](https://github.com/mapbox/mapbox-gl-native/pull/1582)) - An MGLAnnotation can be relocated by changing its `coordinate` property in a KVO-compliant way. An MGLMultiPoint cannot be relocated. ([#3835](https://github.com/mapbox/mapbox-gl-native/pull/3835)) - Setting the `image` property of an MGLAnnotationImage to `nil` resets it to the default red pin image and reclaims resources that can be used to customize additional annotations. ([#3835](https://github.com/mapbox/mapbox-gl-native/pull/3835)) +- The compass, user dot, and visible annotations are now accessible to VoiceOver users. ([#1496](https://github.com/mapbox/mapbox-gl-native/pull/1496)) - The SDK is now localizable. No localizations are currently provided, other than English, but if you need a particular localization, you can install the SDK manually and drop a .lproj folder into the framework. ([#4783](https://github.com/mapbox/mapbox-gl-native/pull/4783)) - Fixed an issue preventing KVO change notifications from being generated on MGLMapView’s `userTrackingMode` key path when `-setUserTrackingMode:animated:` is called. ([#4724](https://github.com/mapbox/mapbox-gl-native/pull/4724)) - Rendering now occurs on the main thread, fixing a hang when calling `-[MGLMapView styleURL]` before the map view has fully loaded or while the application is in the background. ([#2909](https://github.com/mapbox/mapbox-gl-native/pull/2909)) diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm index 5b429d9d4..222bd8f22 100644 --- a/platform/ios/src/MGLMapView.mm +++ b/platform/ios/src/MGLMapView.mm @@ -129,12 +129,23 @@ mbgl::Color MGLColorObjectFromUIColor(UIColor *color) return {{ (float)r, (float)g, (float)b, (float)a }}; } +@interface MGLAnnotationAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) MGLAnnotationTag tag; + +@end + +@implementation MGLAnnotationAccessibilityElement + +@end + /// Lightweight container for metadata about an annotation, including the annotation itself. class MGLAnnotationContext { public: id annotation; /// The annotation’s image’s reuse identifier. NSString *imageReuseIdentifier; + MGLAnnotationAccessibilityElement *accessibilityElement; }; #pragma mark - Private - @@ -1789,6 +1800,105 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) return frame; } +- (NSInteger)accessibilityElementCount +{ + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + return visibleAnnotations.size() + 2 /* compass, attributionButton */; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index +{ + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + + // Ornaments + if (index == 0) + { + return self.compassView; + } + if (index > 0 && (NSUInteger)index == visibleAnnotations.size() + 1 /* compass */) + { + return self.attributionButton; + } + + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + CGPoint centerPoint = self.contentCenter; + if (self.userTrackingMode != MGLUserTrackingModeNone) + { + centerPoint = self.userLocationAnnotationViewCenter; + } + CLLocationCoordinate2D currentCoordinate = [self convertPoint:centerPoint toCoordinateFromView:self]; + std::sort(visibleAnnotations.begin(), visibleAnnotations.end(), [&](const MGLAnnotationTag tagA, const MGLAnnotationTag tagB) { + CLLocationCoordinate2D coordinateA = [[self annotationWithTag:tagA] coordinate]; + CLLocationCoordinate2D coordinateB = [[self annotationWithTag:tagB] coordinate]; + CLLocationDegrees deltaA = hypot(coordinateA.latitude - currentCoordinate.latitude, + coordinateA.longitude - currentCoordinate.longitude); + CLLocationDegrees deltaB = hypot(coordinateB.latitude - currentCoordinate.latitude, + coordinateB.longitude - currentCoordinate.longitude); + return deltaA < deltaB; + }); + + NSUInteger annotationIndex = MGLAnnotationTagNotFound; + if (index >= 0 && (NSUInteger)index < visibleAnnotations.size()) + { + annotationIndex = index - 1 /* compass */; + } + MGLAnnotationTag annotationTag = visibleAnnotations[annotationIndex]; + NSAssert(annotationTag != MGLAnnotationTagNotFound, @"Can’t get accessibility element for nonexistent or invisible annotation at index %li.", index); + NSAssert(_annotationContextsByAnnotationTag.count(annotationTag), @"Missing annotation for tag %u.", annotationTag); + MGLAnnotationContext &annotationContext = _annotationContextsByAnnotationTag.at(annotationTag); + id annotation = annotationContext.annotation; + MGLAnnotationAccessibilityElement *element = annotationContext.accessibilityElement; + + // Lazily create an accessibility element for the found annotation. + if ( ! element) + { + element = [[MGLAnnotationAccessibilityElement alloc] initWithAccessibilityContainer:self]; + element.tag = annotationTag; + element.accessibilityTraits = UIAccessibilityTraitButton; + if ([annotation respondsToSelector:@selector(title)]) + { + element.accessibilityLabel = annotation.title; + } + if ([annotation respondsToSelector:@selector(subtitle)]) + { + element.accessibilityValue = annotation.subtitle; + } + annotationContext.accessibilityElement = element; + } + + // Update the accessibility element’s frame. + MGLAnnotationImage *annotationImage = [self imageOfAnnotationWithTag:annotationTag]; + CGRect annotationFrame = [self frameOfImage:annotationImage.image centeredAtCoordinate:annotation.coordinate]; + CGRect screenRect = UIAccessibilityConvertFrameToScreenCoordinates(annotationFrame, self); + element.accessibilityFrame = screenRect; + + return element; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element +{ + if (element == self.compassView) + { + return 0; + } + if ( ! [element isKindOfClass:[MGLAnnotationAccessibilityElement class]] && + element != self.attributionButton) + { + return NSNotFound; + } + + std::vector visibleAnnotations = [self annotationTagsInRect:self.bounds]; + if (element == self.attributionButton) + { + return visibleAnnotations.size(); + } + std::sort(visibleAnnotations.begin(), visibleAnnotations.end()); + auto foundElement = std::find(visibleAnnotations.begin(), visibleAnnotations.end(), + ((MGLAnnotationAccessibilityElement *)element).tag); + if (foundElement == visibleAnnotations.end()) return NSNotFound; + else return std::distance(visibleAnnotations.begin(), foundElement) + 1; +} + #pragma mark - Geography - + (NS_SET_OF(NSString *) *)keyPathsForValuesAffectingCenterCoordinate @@ -2551,6 +2661,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) } [self didChangeValueForKey:@"annotations"]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } /// Initialize and return a default annotation image that depicts a round pin @@ -2685,6 +2796,7 @@ mbgl::Duration MGLDurationInSeconds(NSTimeInterval duration) [self willChangeValueForKey:@"annotations"]; _mbglMap->removeAnnotations(annotationTagsToRemove); [self didChangeValueForKey:@"annotations"]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } }