9 #include "flutter/fml/logging.h"
15 #include "flutter/common/constants.h"
17 #pragma GCC diagnostic error "-Wundeclared-selector"
24 constexpr int32_t kSemanticObjectIdInvalid = -1;
26 class DefaultIosDelegate :
public AccessibilityBridge::IosDelegate {
28 bool IsFlutterViewControllerPresentingModalViewController(
30 if (view_controller) {
31 return view_controller.isPresentingViewController;
37 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
38 id argument)
override {
39 UIAccessibilityPostNotification(notification, argument);
48 std::unique_ptr<IosDelegate> ios_delegate)
49 : view_controller_(view_controller),
51 platform_views_controller_(platform_views_controller),
52 last_focused_semantics_object_id_(kSemanticObjectIdInvalid),
53 objects_([[NSMutableDictionary alloc] init]),
55 ios_delegate_(ios_delegate ? std::move(ios_delegate)
56 : std::make_unique<DefaultIosDelegate>()),
59 initWithName:
@"flutter/accessibility"
60 binaryMessenger:
platform_view->GetOwnerViewController().engine.binaryMessenger
62 [accessibility_channel_ setMessageHandler:^(
id message,
FlutterReply reply) {
63 HandleEvent((NSDictionary*)message);
67 AccessibilityBridge::~AccessibilityBridge() {
68 [accessibility_channel_ setMessageHandler:nil];
72 UIView<UITextInput>* AccessibilityBridge::textInputView() {
73 return [[platform_view_->GetOwnerViewController().engine
textInputPlugin] textInputView];
76 void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t
id) {
77 last_focused_semantics_object_id_ = id;
78 [accessibility_channel_ sendMessage:@{
@"type" :
@"didGainFocus",
@"nodeId" : @(id)}];
81 void AccessibilityBridge::AccessibilityObjectDidLoseFocus(int32_t
id) {
82 if (last_focused_semantics_object_id_ ==
id) {
83 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
87 void AccessibilityBridge::UpdateSemantics(
88 flutter::SemanticsNodeUpdates nodes,
89 const flutter::CustomAccessibilityActionUpdates& actions) {
90 BOOL layoutChanged = NO;
91 BOOL scrollOccured = NO;
92 BOOL needsAnnouncement = NO;
93 for (
const auto& entry : actions) {
94 const flutter::CustomAccessibilityAction& action = entry.second;
95 actions_[action.id] = action;
97 for (
const auto& entry : nodes) {
98 const flutter::SemanticsNode& node = entry.second;
100 layoutChanged = layoutChanged || [
object nodeWillCauseLayoutChange:&node];
101 scrollOccured = scrollOccured || [
object nodeWillCauseScroll:&node];
102 needsAnnouncement = [
object nodeShouldTriggerAnnouncement:&node];
103 [
object setSemanticsNode:&node];
104 NSUInteger newChildCount = node.childrenInTraversalOrder.size();
105 NSMutableArray* newChildren = [[NSMutableArray alloc] initWithCapacity:newChildCount];
106 for (NSUInteger i = 0; i < newChildCount; ++i) {
107 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
108 [newChildren addObject:child];
110 NSMutableArray* newChildrenInHitTestOrder =
111 [[NSMutableArray alloc] initWithCapacity:newChildCount];
112 for (NSUInteger i = 0; i < newChildCount; ++i) {
113 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
114 [newChildrenInHitTestOrder addObject:child];
116 object.children = newChildren;
117 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
118 if (!node.customAccessibilityActions.empty()) {
119 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
120 [[NSMutableArray alloc] init];
121 for (int32_t action_id : node.customAccessibilityActions) {
122 flutter::CustomAccessibilityAction& action = actions_[action_id];
123 if (action.overrideId != -1) {
128 NSString* label = @(action.label.data());
129 SEL selector =
@selector(onCustomAccessibilityAction:);
134 customAction.
uid = action_id;
135 [accessibilityCustomActions addObject:customAction];
137 object.accessibilityCustomActions = accessibilityCustomActions;
140 if (needsAnnouncement) {
146 NSString* announcement = [[NSString alloc] initWithUTF8String:
object.node.label.c_str()];
147 UIAccessibilityPostNotification(
148 UIAccessibilityAnnouncementNotification,
149 [[NSAttributedString alloc] initWithString:announcement
151 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
158 bool routeChanged =
false;
162 if (!view_controller_.view.accessibilityElements) {
163 view_controller_.view.accessibilityElements =
164 @[ [root accessibilityContainer] ?: [NSNull
null] ];
166 NSMutableArray<SemanticsObject*>* newRoutes = [[NSMutableArray alloc] init];
167 [root collectRoutes:newRoutes];
170 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
171 previous_routes_.end()) {
176 if (lastAdded == nil && [newRoutes count] > 0) {
177 int index = [newRoutes count] - 1;
178 lastAdded = [newRoutes objectAtIndex:index];
187 if (lastAdded != nil &&
188 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
189 previous_route_id_ = [lastAdded uid];
192 previous_routes_.clear();
194 previous_routes_.push_back([route uid]);
197 view_controller_.viewIfLoaded.accessibilityElements = nil;
200 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:objects_.allKeys];
202 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
204 [objects_ removeObjectsForKeys:doomed_uids];
207 [
object accessibilityBridgeDidFinishUpdate];
210 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
211 layoutChanged = layoutChanged || [doomed_uids count] > 0;
214 NSString* routeName = [lastAdded routeName];
215 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
221 SemanticsObject* lastFocused = [objects_ objectForKey:@(last_focused_semantics_object_id_)];
225 ios_delegate_->PostAccessibilityNotification(
226 UIAccessibilityLayoutChangedNotification,
228 }
else if (scrollOccured) {
232 ios_delegate_->PostAccessibilityNotification(
233 UIAccessibilityPageScrolledNotification,
234 FindNextFocusableIfNecessary().nativeAccessibility);
239 void AccessibilityBridge::DispatchSemanticsAction(int32_t node_uid,
240 flutter::SemanticsAction action) {
243 platform_view_->DispatchSemanticsAction(kFlutterImplicitViewId, node_uid, action, {});
246 void AccessibilityBridge::DispatchSemanticsAction(int32_t node_uid,
247 flutter::SemanticsAction action,
248 fml::MallocMapping args) {
251 platform_view_->DispatchSemanticsAction(kFlutterImplicitViewId, node_uid, action,
257 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
259 FML_DCHECK(oldObject.
node.id == newObject.
uid);
260 NSNumber* nodeId = @(oldObject.
node.id);
261 NSUInteger positionInChildlist = [oldObject.
parent.
children indexOfObject:oldObject];
263 [oldObject.
parent replaceChildAtIndex:positionInChildlist withChild:newObject];
264 [objects removeObjectForKey:nodeId];
265 objects[nodeId] = newObject;
268 static SemanticsObject* CreateObject(
const flutter::SemanticsNode& node,
269 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
270 if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) &&
271 !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) {
274 }
else if (!node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup) &&
275 (node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
276 node.HasFlag(flutter::SemanticsFlags::kHasCheckedState))) {
278 }
else if (node.HasFlag(flutter::SemanticsFlags::kHasImplicitScrolling)) {
280 }
else if (node.IsPlatformViewNode()) {
282 weak_ptr->GetPlatformViewsController();
284 [platformViewsController flutterTouchInterceptingViewForId:node.platformViewId];
287 platformView:touchInterceptingView];
293 static bool DidFlagChange(
const flutter::SemanticsNode& oldNode,
294 const flutter::SemanticsNode& newNode,
295 SemanticsFlags flag) {
296 return oldNode.HasFlag(flag) != newNode.HasFlag(flag);
300 flutter::SemanticsNodeUpdates& updates) {
303 object = CreateObject(updates[uid], GetWeakPtr());
304 objects_[@(uid)] =
object;
307 auto nodeEntry = updates.find(
object.node.id);
308 if (nodeEntry != updates.end()) {
310 flutter::SemanticsNode node = nodeEntry->second;
311 if (DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsTextField) ||
312 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kIsReadOnly) ||
313 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasCheckedState) ||
314 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasToggledState) ||
315 DidFlagChange(
object.node, node, flutter::SemanticsFlags::kHasImplicitScrolling)) {
319 SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr());
320 ReplaceSemanticsObject(
object, newSemanticsObject, objects_);
321 object = newSemanticsObject;
328 void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(
SemanticsObject*
object,
329 NSMutableArray<NSNumber*>* doomed_uids) {
330 [doomed_uids removeObject:@(
object.uid)];
332 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
338 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
343 return FindFirstFocusable(objects_[@(last_focused_semantics_object_id_)]);
348 if (!currentObject) {
351 if (currentObject.isAccessibilityElement) {
352 return currentObject;
364 void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
365 NSString* type = annotatedEvent[
@"type"];
366 if ([type isEqualToString:
@"announce"]) {
367 NSString* message = annotatedEvent[
@"data"][
@"message"];
368 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
370 if ([type isEqualToString:
@"focus"]) {
372 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
376 fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {
377 return weak_factory_.GetWeakPtr();
380 void AccessibilityBridge::clearState() {
381 [objects_ removeAllObjects];
382 previous_route_id_ = 0;
383 previous_routes_.clear();
384 view_controller_.viewIfLoaded.accessibilityElements = nil;
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
FlutterTextInputPlugin * textInputPlugin
constexpr int32_t kRootNodeId
AccessibilityBridge()
Creates a new instance of a accessibility bridge.
flutter::SemanticsNode node
NSArray< SemanticsObject * > * children