9 #include "flutter/fml/logging.h"
15 #pragma GCC diagnostic error "-Wundeclared-selector"
22 constexpr int32_t kSemanticObjectIdInvalid = -1;
24 class DefaultIosDelegate :
public AccessibilityBridge::IosDelegate {
26 bool IsFlutterViewControllerPresentingModalViewController(
28 if (view_controller) {
29 return view_controller.isPresentingViewController;
35 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
36 id argument)
override {
37 UIAccessibilityPostNotification(notification, argument);
45 std::shared_ptr<PlatformViewsController> platform_views_controller,
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
49 platform_views_controller_(std::move(platform_views_controller)),
50 last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
51 objects_([[NSMutableDictionary alloc] init]),
53 ios_delegate_(ios_delegate ? std::move(ios_delegate)
54 : std::make_unique<DefaultIosDelegate>()),
57 initWithName:
@"flutter/accessibility"
58 binaryMessenger:
platform_view->GetOwnerViewController().get().engine.binaryMessenger
60 [accessibility_channel_.get() setMessageHandler:^(
id message,
FlutterReply reply) {
66 [accessibility_channel_.get() setMessageHandler:nil];
68 view_controller_.viewIfLoaded.accessibilityElements = nil;
76 last_focused_semantics_object_id_ = id;
77 [accessibility_channel_.get() sendMessage:@{
@"type" :
@"didGainFocus",
@"nodeId" : @(id)}];
81 if (last_focused_semantics_object_id_ ==
id) {
82 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
87 flutter::SemanticsNodeUpdates nodes,
88 const flutter::CustomAccessibilityActionUpdates& actions) {
89 BOOL layoutChanged = NO;
90 BOOL scrollOccured = NO;
91 BOOL needsAnnouncement = NO;
92 for (
const auto& entry : actions) {
93 const flutter::CustomAccessibilityAction& action = entry.second;
94 actions_[action.id] = action;
96 for (
const auto& entry : nodes) {
97 const flutter::SemanticsNode& node = entry.second;
99 layoutChanged = layoutChanged || [
object nodeWillCauseLayoutChange:&node];
100 scrollOccured = scrollOccured || [
object nodeWillCauseScroll:&node];
101 needsAnnouncement = [
object nodeShouldTriggerAnnouncement:&node];
102 [
object setSemanticsNode:&node];
103 NSUInteger newChildCount = node.childrenInTraversalOrder.size();
104 NSMutableArray* newChildren = [[NSMutableArray alloc] initWithCapacity:newChildCount];
105 for (NSUInteger i = 0; i < newChildCount; ++i) {
106 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
107 [newChildren addObject:child];
109 NSMutableArray* newChildrenInHitTestOrder =
110 [[NSMutableArray alloc] initWithCapacity:newChildCount];
111 for (NSUInteger i = 0; i < newChildCount; ++i) {
112 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
113 [newChildrenInHitTestOrder addObject:child];
115 object.children = newChildren;
116 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
117 if (!node.customAccessibilityActions.empty()) {
118 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
119 [[NSMutableArray alloc] init];
120 for (int32_t action_id : node.customAccessibilityActions) {
121 flutter::CustomAccessibilityAction& action = actions_[action_id];
122 if (action.overrideId != -1) {
127 NSString* label = @(action.label.data());
128 SEL selector =
@selector(onCustomAccessibilityAction:);
133 customAction.
uid = action_id;
134 [accessibilityCustomActions addObject:customAction];
136 object.accessibilityCustomActions = accessibilityCustomActions;
139 if (needsAnnouncement) {
145 NSString* announcement = [[NSString alloc] initWithUTF8String:
object.node.label.c_str()];
146 UIAccessibilityPostNotification(
147 UIAccessibilityAnnouncementNotification,
148 [[NSAttributedString alloc] initWithString:announcement
150 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
157 bool routeChanged =
false;
161 if (!view_controller_.view.accessibilityElements) {
162 view_controller_.view.accessibilityElements =
163 @[ [root accessibilityContainer] ?: [NSNull
null] ];
165 NSMutableArray<SemanticsObject*>* newRoutes = [[NSMutableArray alloc] init];
166 [root collectRoutes:newRoutes];
169 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
170 previous_routes_.end()) {
175 if (lastAdded == nil && [newRoutes count] > 0) {
176 int index = [newRoutes count] - 1;
177 lastAdded = [newRoutes objectAtIndex:index];
186 if (lastAdded != nil &&
187 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
188 previous_route_id_ = [lastAdded uid];
191 previous_routes_.clear();
193 previous_routes_.push_back([route uid]);
196 view_controller_.viewIfLoaded.accessibilityElements = nil;
199 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_ allKeys]];
201 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
203 [objects_ removeObjectsForKeys:doomed_uids];
206 [
object accessibilityBridgeDidFinishUpdate];
209 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
210 layoutChanged = layoutChanged || [doomed_uids count] > 0;
213 NSString* routeName = [lastAdded routeName];
214 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
221 [objects_.get() objectForKey:@(last_focused_semantics_object_id_)];
225 ios_delegate_->PostAccessibilityNotification(
226 UIAccessibilityLayoutChangedNotification,
228 }
else if (scrollOccured) {
232 ios_delegate_->PostAccessibilityNotification(
233 UIAccessibilityPageScrolledNotification,
240 platform_view_->DispatchSemanticsAction(uid, action, {});
244 flutter::SemanticsAction action,
245 fml::MallocMapping args) {
246 platform_view_->DispatchSemanticsAction(uid, action, std::move(args));
251 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
253 FML_DCHECK(oldObject.
node.id == newObject.
uid);
254 NSNumber* nodeId = @(oldObject.
node.id);
255 NSUInteger positionInChildlist = [oldObject.
parent.
children indexOfObject:oldObject];
257 [oldObject.
parent replaceChildAtIndex:positionInChildlist withChild:newObject];
258 [objects removeObjectForKey:nodeId];
259 objects[nodeId] = newObject;
262 static SemanticsObject* CreateObject(
const flutter::SemanticsNode& node,
263 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
264 if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
265 !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
268 }
else if (!node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) &&
269 (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
270 node.HasFlag(flutter::SemanticsFlags::kHasCheckedState))) {
272 }
else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
274 }
else if (node.IsPlatformViewNode()) {
276 initWithBridge:weak_ptr
278 platformView:weak_ptr->GetPlatformViewsController()->GetFlutterTouchInterceptingViewByID(
279 node.platformViewId)];
285 static bool DidFlagChange(
const flutter::SemanticsNode& oldNode,
286 const flutter::SemanticsNode& newNode,
287 SemanticsFlags flag) {
288 return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
292 flutter::SemanticsNodeUpdates& updates) {
295 object = CreateObject(updates[uid],
GetWeakPtr());
296 objects_.get()[@(uid)] =
object;
299 auto nodeEntry = updates.find(
object.node.id);
300 if (nodeEntry != updates.end()) {
302 flutter::SemanticsNode node = nodeEntry->second;
303 if (DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsTextField) ||
304 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
305 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
306 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasToggledState) ||
307 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasImplicitScrolling)) {
312 ReplaceSemanticsObject(
object, newSemanticsObject, objects_.get());
313 object = newSemanticsObject;
320 void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(
SemanticsObject*
object,
321 NSMutableArray<NSNumber*>* doomed_uids) {
322 [doomed_uids removeObject:@(
object.uid)];
324 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
330 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
335 return FindFirstFocusable([objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
340 if (!currentObject) {
343 if (currentObject.isAccessibilityElement) {
344 return currentObject;
357 NSString* type = annotatedEvent[
@"type"];
358 if ([type isEqualToString:
@"announce"]) {
359 NSString* message = annotatedEvent[
@"data"][
@"message"];
360 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
362 if ([type isEqualToString:
@"focus"]) {
364 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
369 return weak_factory_.GetWeakPtr();
373 [objects_ removeAllObjects];
374 previous_route_id_ = 0;
375 previous_routes_.clear();