未验证 提交 343af333 编写于 作者: D Dan Field 提交者: GitHub

Live region announcements for iOS (#18798)

上级 a95882ba
......@@ -483,9 +483,12 @@ class SemanticsFlag {
/// Platforms may use this information to make polite announcements to the
/// user to inform them of updates to this node.
///
/// An example of a live region is a [SnackBar] widget. On Android, A live
/// region causes a polite announcement to be generated automatically, even
/// if the user does not have focus of the widget.
/// An example of a live region is a [SnackBar] widget. On Android and iOS,
/// live region causes a polite announcement to be generated automatically,
/// even if the widget does not have accessibility focus. This announcement
/// may not be spoken if the OS accessibility services are already
/// announcing something else, such as reading the label of a focused
/// widget or providing a system announcement.
static const SemanticsFlag isLiveRegion = SemanticsFlag._(_kIsLiveRegionIndex);
/// The semantics node has the quality of either being "on" or "off".
......
......@@ -91,6 +91,7 @@ constexpr int32_t kRootNodeId = 0;
uid:(int32_t)uid NS_DESIGNATED_INITIALIZER;
- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node;
- (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node;
- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges;
- (NSString*)routeName;
- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action;
......
......@@ -180,6 +180,25 @@ flutter::SemanticsAction GetSemanticsActionForScrollDirection(
[self node].scrollPosition != node->scrollPosition;
}
/**
* Whether calling `setSemanticsNode:` with `node` should trigger an
* announcement.
*/
- (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node {
// The node dropped the live region flag, if it ever had one.
if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
return NO;
}
// The node has gained a new live region flag, always announce.
if (![self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
return YES;
}
// The label has updated, and the new node has a live region flag.
return [self node].label != node->label;
}
- (BOOL)hasChildren {
if (_node.IsPlatformViewNode()) {
return YES;
......
......@@ -64,4 +64,42 @@ class MockAccessibilityBridge : public AccessibilityBridgeIos {
XCTAssertEqualObjects(parent.children, @[ child2 ]);
}
- (void)testShouldTriggerAnnouncement {
fml::WeakPtrFactory<flutter::AccessibilityBridgeIos> factory(
new flutter::MockAccessibilityBridge());
fml::WeakPtr<flutter::AccessibilityBridgeIos> bridge = factory.GetWeakPtr();
SemanticsObject* object = [[SemanticsObject alloc] initWithBridge:bridge uid:0];
// Handle nil with no node set.
XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);
// Handle initial setting of node with liveRegion
flutter::SemanticsNode node;
node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsLiveRegion);
node.label = "foo";
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);
// Handle nil with node set.
[object setSemanticsNode:&node];
XCTAssertFalse([object nodeShouldTriggerAnnouncement:nil]);
// Handle new node, still has live region, same label.
XCTAssertFalse([object nodeShouldTriggerAnnouncement:&node]);
// Handle update node with new label, still has live region.
flutter::SemanticsNode updatedNode;
updatedNode.flags = static_cast<int32_t>(flutter::SemanticsFlags::kIsLiveRegion);
updatedNode.label = "bar";
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&updatedNode]);
// Handle dropping the live region flag.
updatedNode.flags = 0;
XCTAssertFalse([object nodeShouldTriggerAnnouncement:&updatedNode]);
// Handle adding the flag when the label has not changed.
updatedNode.label = "foo";
[object setSemanticsNode:&updatedNode];
XCTAssertTrue([object nodeShouldTriggerAnnouncement:&node]);
}
@end
......@@ -84,6 +84,7 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
flutter::CustomAccessibilityActionUpdates actions) {
BOOL layoutChanged = NO;
BOOL scrollOccured = NO;
BOOL needsAnnouncement = NO;
for (const auto& entry : actions) {
const flutter::CustomAccessibilityAction& action = entry.second;
actions_[action.id] = action;
......@@ -93,6 +94,7 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
SemanticsObject* object = GetOrCreateObject(node.id, nodes);
layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
needsAnnouncement = [object nodeShouldTriggerAnnouncement:&node];
[object setSemanticsNode:&node];
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren =
......@@ -133,6 +135,26 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
} else if (object.platformViewSemanticsContainer) {
object.platformViewSemanticsContainer = nil;
}
if (needsAnnouncement) {
// Try to be more polite - iOS 11+ supports
// UIAccessibilitySpeechAttributeQueueAnnouncement which should avoid
// interrupting system notifications or other elements.
// Expectation: roughly match the behavior of polite announcements on
// Android.
NSString* announcement =
[[[NSString alloc] initWithUTF8String:object.node.label.c_str()] autorelease];
if (@available(iOS 11.0, *)) {
UIAccessibilityPostNotification(
UIAccessibilityAnnouncementNotification,
[[[NSAttributedString alloc]
initWithString:announcement
attributes:@{
UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
}] autorelease]);
} else {
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, announcement);
}
}
}
SemanticsObject* root = objects_.get()[@(kRootNodeId)];
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册