Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
accessibility_bridge.mm
Go to the documentation of this file.
1// Copyright 2013 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
6
7#include <utility>
8
14
16
17#pragma GCC diagnostic error "-Wundeclared-selector"
18
20
21namespace flutter {
22namespace {
23
24class DefaultIosDelegate : public AccessibilityBridge::IosDelegate {
25 public:
26 bool IsFlutterViewControllerPresentingModalViewController(
27 FlutterViewController* view_controller) override {
28 if (view_controller) {
29 return view_controller.isPresentingViewController;
30 } else {
31 return false;
32 }
33 }
34
35 void PostAccessibilityNotification(UIAccessibilityNotifications notification,
36 id argument) override {
37 UIAccessibilityPostNotification(notification, argument);
38 }
39};
40} // namespace
41
43 FlutterViewController* view_controller,
44 PlatformViewIOS* platform_view,
45 __weak FlutterPlatformViewsController* platform_views_controller,
46 std::unique_ptr<IosDelegate> ios_delegate)
47 : view_controller_(view_controller),
48 platform_view_(platform_view),
49 platform_views_controller_(platform_views_controller),
50 objects_([[NSMutableDictionary alloc] init]),
51 previous_routes_({}),
52 ios_delegate_(ios_delegate ? std::move(ios_delegate)
53 : std::make_unique<DefaultIosDelegate>()),
54 weak_factory_(this) {
55 accessibility_channel_ = [[FlutterBasicMessageChannel alloc]
56 initWithName:@"flutter/accessibility"
57 binaryMessenger:platform_view->GetOwnerViewController().engine.binaryMessenger
58 codec:[FlutterStandardMessageCodec sharedInstance]];
59 fml::WeakPtr<AccessibilityBridge> weak_self = GetWeakPtr();
60 [accessibility_channel_ setMessageHandler:^(id message, FlutterReply reply) {
61 if (weak_self) {
62 weak_self->HandleEvent((NSDictionary*)message);
63 }
64 }];
65}
66
67AccessibilityBridge::~AccessibilityBridge() {
68 [accessibility_channel_ setMessageHandler:nil];
69 clearState();
70}
71
72UIView<UITextInput>* AccessibilityBridge::textInputView() {
73 return [[platform_view_->GetOwnerViewController().engine textInputPlugin] textInputView];
74}
75
76void AccessibilityBridge::AccessibilityObjectDidBecomeFocused(int32_t id) {
77 last_focused_semantics_object_id_ = id;
78 [accessibility_channel_ sendMessage:@{@"type" : @"didGainFocus", @"nodeId" : @(id)}];
79}
80
81void AccessibilityBridge::AccessibilityObjectDidLoseFocus(int32_t id) {
82 if (last_focused_semantics_object_id_ == id) {
83 last_focused_semantics_object_id_ = kSemanticObjectIdInvalid;
84 }
85}
86
87void AccessibilityBridge::UpdateSemantics(
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;
96 }
97 for (const auto& entry : nodes) {
98 const flutter::SemanticsNode& node = entry.second;
99 SemanticsObject* object = GetOrCreateObject(node.id, nodes);
100 layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
101 scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
102 needsAnnouncement = [object nodeShouldTriggerAnnouncement:&node];
103 [object setSemanticsNode:&node];
104 NSUInteger newChildCountInTraversalOrder = node.childrenInTraversalOrder.size();
105 NSMutableArray* newChildren =
106 [[NSMutableArray alloc] initWithCapacity:newChildCountInTraversalOrder];
107 for (NSUInteger i = 0; i < newChildCountInTraversalOrder; ++i) {
108 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes);
109 [newChildren addObject:child];
110 }
111 NSUInteger newChildCountInHitTestOrder = node.childrenInHitTestOrder.size();
112 NSMutableArray* newChildrenInHitTestOrder =
113 [[NSMutableArray alloc] initWithCapacity:newChildCountInHitTestOrder];
114 for (NSUInteger i = 0; i < newChildCountInHitTestOrder; ++i) {
115 SemanticsObject* child = GetOrCreateObject(node.childrenInHitTestOrder[i], nodes);
116 [newChildrenInHitTestOrder addObject:child];
117 }
118 object.children = newChildren;
119 object.childrenInHitTestOrder = newChildrenInHitTestOrder;
120 if (!node.customAccessibilityActions.empty()) {
121 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions =
122 [[NSMutableArray alloc] init];
123 for (int32_t action_id : node.customAccessibilityActions) {
124 flutter::CustomAccessibilityAction& action = actions_[action_id];
125 if (action.overrideId != -1) {
126 // iOS does not support overriding standard actions, so we ignore any
127 // custom actions that have an override id provided.
128 continue;
129 }
130 NSString* label = @(action.label.data());
131 SEL selector = @selector(onCustomAccessibilityAction:);
133 [[FlutterCustomAccessibilityAction alloc] initWithName:label
134 target:object
135 selector:selector];
136 customAction.uid = action_id;
137 [accessibilityCustomActions addObject:customAction];
138 }
139 object.accessibilityCustomActions = accessibilityCustomActions;
140 }
141
142 if (needsAnnouncement) {
143 // Try to be more polite - iOS 11+ supports
144 // UIAccessibilitySpeechAttributeQueueAnnouncement which should avoid
145 // interrupting system notifications or other elements.
146 // Expectation: roughly match the behavior of polite announcements on
147 // Android.
148 NSString* announcement = [[NSString alloc] initWithUTF8String:object.node.label.c_str()];
149 UIAccessibilityPostNotification(
150 UIAccessibilityAnnouncementNotification,
151 [[NSAttributedString alloc] initWithString:announcement
152 attributes:@{
153 UIAccessibilitySpeechAttributeQueueAnnouncement : @YES
154 }]);
155 }
156 }
157
158 SemanticsObject* root = objects_[@(kRootNodeId)];
159
160 bool routeChanged = false;
161 SemanticsObject* lastAdded = nil;
162
163 if (root) {
164 if (!view_controller_.view.accessibilityElements) {
165 view_controller_.view.accessibilityElements =
166 @[ [root accessibilityContainer] ?: [NSNull null] ];
167 }
168 NSMutableArray<SemanticsObject*>* newRoutes = [[NSMutableArray alloc] init];
169 [root collectRoutes:newRoutes];
170 // Finds the last route that is not in the previous routes.
171 for (SemanticsObject* route in newRoutes) {
172 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
173 previous_routes_.end()) {
174 lastAdded = route;
175 }
176 }
177 // If all the routes are in the previous route, get the last route.
178 if (lastAdded == nil && [newRoutes count] > 0) {
179 int index = [newRoutes count] - 1;
180 lastAdded = [newRoutes objectAtIndex:index];
181 }
182 // There are two cases if lastAdded != nil
183 // 1. lastAdded is not in previous routes. In this case,
184 // [lastAdded uid] != previous_route_id_
185 // 2. All new routes are in previous routes and
186 // lastAdded = newRoutes.last.
187 // In the first case, we need to announce new route. In the second case,
188 // we need to announce if one list is shorter than the other.
189 if (lastAdded != nil &&
190 ([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
191 previous_route_id_ = [lastAdded uid];
192 routeChanged = true;
193 }
194 previous_routes_.clear();
195 for (SemanticsObject* route in newRoutes) {
196 previous_routes_.push_back([route uid]);
197 }
198 } else {
199 view_controller_.viewIfLoaded.accessibilityElements = nil;
200 }
201
202 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:objects_.allKeys];
203 if (root) {
204 VisitObjectsRecursivelyAndRemove(root, doomed_uids);
205 }
206 [objects_ removeObjectsForKeys:doomed_uids];
207
208 for (SemanticsObject* object in objects_.allValues) {
209 [object accessibilityBridgeDidFinishUpdate];
210 }
211
212 if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_controller_)) {
213 layoutChanged = layoutChanged || [doomed_uids count] > 0;
214
215 if (routeChanged) {
216 NSString* routeName = [lastAdded routeName];
217 ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification,
218 routeName);
219 }
220
221 if (layoutChanged) {
222 SemanticsObject* next = FindNextFocusableIfNecessary();
223 SemanticsObject* lastFocused = [objects_ objectForKey:@(last_focused_semantics_object_id_)];
224 // Only specify the focus item if the new focus is different, avoiding double focuses on the
225 // same item. See: https://github.com/flutter/flutter/issues/104176. If there is a route
226 // change, we always refocus.
227 ios_delegate_->PostAccessibilityNotification(
228 UIAccessibilityLayoutChangedNotification,
229 (routeChanged || next != lastFocused) ? next.nativeAccessibility : NULL);
230 } else if (scrollOccured) {
231 // TODO(chunhtai): figure out what string to use for notification. At this
232 // point, it is guarantee the previous focused object is still in the tree
233 // so that we don't need to worry about focus lost. (e.g. "Screen 0 of 3")
234 ios_delegate_->PostAccessibilityNotification(
235 UIAccessibilityPageScrolledNotification,
236 FindNextFocusableIfNecessary().nativeAccessibility);
237 }
238 }
239}
240
241void AccessibilityBridge::DispatchSemanticsAction(int32_t node_uid,
243 // TODO(team-ios): Remove implicit view assumption.
244 // https://github.com/flutter/flutter/issues/142845
245 platform_view_->DispatchSemanticsAction(kFlutterImplicitViewId, node_uid, action, {});
246}
247
248void AccessibilityBridge::DispatchSemanticsAction(int32_t node_uid,
251 // TODO(team-ios): Remove implicit view assumption.
252 // https://github.com/flutter/flutter/issues/142845
253 platform_view_->DispatchSemanticsAction(kFlutterImplicitViewId, node_uid, action,
254 std::move(args));
255}
256
257static void ReplaceSemanticsObject(SemanticsObject* oldObject,
258 SemanticsObject* newObject,
259 NSMutableDictionary<NSNumber*, SemanticsObject*>* objects) {
260 // `newObject` should represent the same id as `oldObject`.
261 FML_DCHECK(oldObject.node.id == newObject.uid);
262 NSNumber* nodeId = @(oldObject.node.id);
263 NSUInteger positionInChildlist = [oldObject.parent.children indexOfObject:oldObject];
264 oldObject.children = @[];
265 [oldObject.parent replaceChildAtIndex:positionInChildlist withChild:newObject];
266 [objects removeObjectForKey:nodeId];
267 objects[nodeId] = newObject;
268}
269
270static SemanticsObject* CreateObject(const flutter::SemanticsNode& node,
271 const fml::WeakPtr<AccessibilityBridge>& weak_ptr) {
272 if (node.flags.isTextField && !node.flags.isReadOnly) {
273 // Text fields are backed by objects that implement UITextInput.
274 return [[TextInputSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id];
275 } else if (!node.flags.isInMutuallyExclusiveGroup &&
278 return [[FlutterSwitchSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id];
279 } else if (node.flags.hasImplicitScrolling) {
280 return [[FlutterScrollableSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id];
281 } else if (node.IsPlatformViewNode()) {
282 FlutterPlatformViewsController* platformViewsController =
283 weak_ptr->GetPlatformViewsController();
284 FlutterTouchInterceptingView* touchInterceptingView =
285 [platformViewsController flutterTouchInterceptingViewForId:node.platformViewId];
286 return [[FlutterPlatformViewSemanticsContainer alloc] initWithBridge:weak_ptr
287 uid:node.id
288 platformView:touchInterceptingView];
289 } else {
290 return [[FlutterSemanticsObject alloc] initWithBridge:weak_ptr uid:node.id];
291 }
292}
293
294SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid,
296 SemanticsObject* object = objects_[@(uid)];
297 if (!object) {
298 object = CreateObject(updates[uid], GetWeakPtr());
299 objects_[@(uid)] = object;
300 } else {
301 // Existing node case
302 auto nodeEntry = updates.find(object.node.id);
303 if (nodeEntry != updates.end()) {
304 // There's an update for this node
305 flutter::SemanticsNode node = nodeEntry->second;
306 if (object.node.flags.isTextField != node.flags.isTextField ||
307 object.node.flags.isReadOnly != node.flags.isReadOnly ||
308 (object.node.flags.isChecked == flutter::SemanticsCheckState::kNone) !=
312 object.node.flags.hasImplicitScrolling != node.flags.hasImplicitScrolling
313
314 ) {
315 // The node changed its type. In this case, we cannot reuse the existing
316 // SemanticsObject implementation. Instead, we replace it with a new
317 // instance.
318 SemanticsObject* newSemanticsObject = CreateObject(node, GetWeakPtr());
319 ReplaceSemanticsObject(object, newSemanticsObject, objects_);
320 object = newSemanticsObject;
321 }
322 }
323 }
324 return object;
325}
326
327void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(SemanticsObject* object,
328 NSMutableArray<NSNumber*>* doomed_uids) {
329 [doomed_uids removeObject:@(object.uid)];
330 for (SemanticsObject* child in [object children])
331 VisitObjectsRecursivelyAndRemove(child, doomed_uids);
332}
333
334SemanticsObject* AccessibilityBridge::FindNextFocusableIfNecessary() {
335 // This property will be -1 if the focus is outside of the flutter
336 // application. In this case, we should not refocus anything.
337 if (last_focused_semantics_object_id_ == kSemanticObjectIdInvalid) {
338 return nil;
339 }
340
341 // Tries to refocus the previous focused semantics object to avoid random jumps.
342 return FindFirstFocusable(objects_[@(last_focused_semantics_object_id_)]);
343}
344
345SemanticsObject* AccessibilityBridge::FindFirstFocusable(SemanticsObject* parent) {
346 SemanticsObject* currentObject = parent ?: objects_[@(kRootNodeId)];
347 if (!currentObject) {
348 return nil;
349 }
350 if (currentObject.isAccessibilityElement) {
351 return currentObject;
352 }
353
354 for (SemanticsObject* child in [currentObject children]) {
355 SemanticsObject* candidate = FindFirstFocusable(child);
356 if (candidate) {
357 return candidate;
358 }
359 }
360 return nil;
361}
362
363void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) {
364 NSString* type = annotatedEvent[@"type"];
365 if ([type isEqualToString:@"announce"]) {
366 NSString* message = annotatedEvent[@"data"][@"message"];
367 ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message);
368 }
369 if ([type isEqualToString:@"focus"]) {
370 SemanticsObject* node = objects_[annotatedEvent[@"nodeId"]];
371 ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, node);
372 }
373}
374
375fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() {
376 return weak_factory_.GetWeakPtr();
377}
378
379void AccessibilityBridge::clearState() {
380 [objects_ removeAllObjects];
381 previous_route_id_ = 0;
382 previous_routes_.clear();
383 view_controller_.viewIfLoaded.accessibilityElements = nil;
384}
385
386} // namespace flutter
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
std::unique_ptr< flutter::PlatformViewIOS > platform_view
NS_ASSUME_NONNULL_BEGIN constexpr int32_t kRootNodeId
AccessibilityBridge()
Creates a new instance of a accessibility bridge.
A Mapping like NonOwnedMapping, but uses Free as its release proc.
Definition mapping.h:144
const char * message
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
uint32_t * target
#define FML_DCHECK(condition)
Definition logging.h:122
NSArray< SemanticsObject * > * children
SemanticsObject * parent
flutter::SemanticsNode node
FlutterTextInputPlugin * textInputPlugin
std::unordered_map< int32_t, SemanticsNode > SemanticsNodeUpdates
std::unordered_map< int32_t, CustomAccessibilityAction > CustomAccessibilityActionUpdates
Definition ref_ptr.h:261
impeller::ShaderType type
SemanticsCheckState isChecked
SemanticsTristate isToggled
std::vector< int32_t > childrenInHitTestOrder
std::vector< int32_t > customAccessibilityActions
bool IsPlatformViewNode() const
std::vector< int32_t > childrenInTraversalOrder
const uintptr_t id
int BOOL