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 5#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" 6#include "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h" 7 8#include <utility> 9#include <vector> 10 11#import <UIKit/UIKit.h> 12 13#include "flutter/fml/logging.h" 14#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" 15#include "flutter/shell/platform/darwin/ios/platform_view_ios.h" 16 17namespace { 18 19constexpr int32_t kRootNodeId = 0; 20 21flutter::SemanticsAction GetSemanticsActionForScrollDirection( 22 UIAccessibilityScrollDirection direction) { 23 // To describe scroll direction, UIAccessibilityScrollDirection uses the direction the scroll bar 24 // moves in and SemanticsAction uses the direction the finger moves in. Both move in opposite 25 // directions, which is why the following maps left to right and vice versa. 26 switch (direction) { 27 case UIAccessibilityScrollDirectionRight: 28 case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using 29 // _node.textDirection. 30 return flutter::SemanticsAction::kScrollLeft; 31 case UIAccessibilityScrollDirectionLeft: 32 case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using 33 // _node.textDirection. 34 return flutter::SemanticsAction::kScrollRight; 35 case UIAccessibilityScrollDirectionUp: 36 return flutter::SemanticsAction::kScrollDown; 37 case UIAccessibilityScrollDirectionDown: 38 return flutter::SemanticsAction::kScrollUp; 39 } 40 FML_DCHECK(false); // Unreachable 41 return flutter::SemanticsAction::kScrollUp; 42} 43 44} // namespace 45 46@implementation FlutterCustomAccessibilityAction { 47} 48@end 49 50/** 51 * Represents a semantics object that has children and hence has to be presented to the OS as a 52 * UIAccessibilityContainer. 53 * 54 * The SemanticsObject class cannot implement the UIAccessibilityContainer protocol because an 55 * object that returns YES for isAccessibilityElement cannot also implement 56 * UIAccessibilityContainer. 57 * 58 * With the help of SemanticsObjectContainer, the hierarchy of semantic objects received from 59 * the framework, such as: 60 * 61 * SemanticsObject1 62 * SemanticsObject2 63 * SemanticsObject3 64 * SemanticsObject4 65 * 66 * is translated into the following hierarchy, which is understood by iOS: 67 * 68 * SemanticsObjectContainer1 69 * SemanticsObject1 70 * SemanticsObjectContainer2 71 * SemanticsObject2 72 * SemanticsObject3 73 * SemanticsObject4 74 * 75 * From Flutter's view of the world (the first tree seen above), we construct iOS's view of the 76 * world (second tree) as follows: We replace each SemanticsObjects that has children with a 77 * SemanticsObjectContainer, which has the original SemanticsObject and its children as children. 78 * 79 * SemanticsObjects have semantic information attached to them which is interpreted by 80 * VoiceOver (they return YES for isAccessibilityElement). The SemanticsObjectContainers are just 81 * there for structure and they don't provide any semantic information to VoiceOver (they return 82 * NO for isAccessibilityElement). 83 */ 84@interface SemanticsObjectContainer : NSObject 85- (instancetype)init __attribute__((unavailable("Use initWithSemanticsObject instead"))); 86- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject 87 bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge 88 NS_DESIGNATED_INITIALIZER; 89 90@property(nonatomic, weak) SemanticsObject* semanticsObject; 91 92@end 93 94@implementation SemanticsObject { 95 fml::scoped_nsobject<SemanticsObjectContainer> _container; 96} 97 98#pragma mark - Override base class designated initializers 99 100// Method declared as unavailable in the interface 101- (instancetype)init { 102 [self release]; 103 [super doesNotRecognizeSelector:_cmd]; 104 return nil; 105} 106 107#pragma mark - Designated initializers 108 109- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid { 110 FML_DCHECK(bridge) << "bridge must be set"; 111 FML_DCHECK(uid >= kRootNodeId); 112 self = [super init]; 113 114 if (self) { 115 _bridge = bridge; 116 _uid = uid; 117 _children = [[NSMutableArray alloc] init]; 118 } 119 120 return self; 121} 122 123- (void)dealloc { 124 for (SemanticsObject* child in _children) { 125 child.parent = nil; 126 } 127 [_children removeAllObjects]; 128 [_children release]; 129 _parent = nil; 130 _container.get().semanticsObject = nil; 131 [_platformViewSemanticsContainer release]; 132 [super dealloc]; 133} 134 135#pragma mark - Semantic object methods 136 137- (void)setSemanticsNode:(const flutter::SemanticsNode*)node { 138 _node = *node; 139} 140 141/** 142 * Whether calling `setSemanticsNode:` with `node` would cause a layout change. 143 */ 144- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node { 145 return [self node].rect != node->rect || [self node].transform != node->transform; 146} 147 148/** 149 * Whether calling `setSemanticsNode:` with `node` would cause a scroll event. 150 */ 151- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node { 152 return !isnan([self node].scrollPosition) && !isnan(node->scrollPosition) && 153 [self node].scrollPosition != node->scrollPosition; 154} 155 156- (BOOL)hasChildren { 157 if (_node.IsPlatformViewNode()) { 158 return YES; 159 } 160 return [self.children count] != 0; 161} 162 163#pragma mark - UIAccessibility overrides 164 165- (BOOL)isAccessibilityElement { 166 // Note: hit detection will only apply to elements that report 167 // -isAccessibilityElement of YES. The framework will continue scanning the 168 // entire element tree looking for such a hit. 169 170 // We enforce in the framework that no other useful semantics are merged with these nodes. 171 if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) 172 return false; 173 174 // If the only flag(s) set are scrolling related AND 175 // The only flags set are not kIsHidden OR 176 // The node doesn't have a label, value, or hint OR 177 // The only actions set are scrolling related actions. 178 // 179 // The kIsHidden flag set with any other flag just means this node is now 180 // hidden but still is a valid target for a11y focus in the tree, e.g. a list 181 // item that is currently off screen but the a11y navigation needs to know 182 // about. 183 return (([self node].flags & ~flutter::kScrollableSemanticsFlags) != 0 && 184 [self node].flags != static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) || 185 ![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() || 186 ([self node].actions & ~flutter::kScrollableSemanticsActions) != 0; 187} 188 189- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges { 190 if ([self node].HasFlag(flutter::SemanticsFlags::kScopesRoute)) 191 [edges addObject:self]; 192 if ([self hasChildren]) { 193 for (SemanticsObject* child in self.children) { 194 [child collectRoutes:edges]; 195 } 196 } 197} 198 199- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action { 200 if (![self node].HasAction(flutter::SemanticsAction::kCustomAction)) 201 return NO; 202 int32_t action_id = action.uid; 203 std::vector<uint8_t> args; 204 args.push_back(3); // type=int32. 205 args.push_back(action_id); 206 args.push_back(action_id >> 8); 207 args.push_back(action_id >> 16); 208 args.push_back(action_id >> 24); 209 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kCustomAction, 210 std::move(args)); 211 return YES; 212} 213 214- (NSString*)routeName { 215 // Returns the first non-null and non-empty semantic label of a child 216 // with an NamesRoute flag. Otherwise returns nil. 217 if ([self node].HasFlag(flutter::SemanticsFlags::kNamesRoute)) { 218 NSString* newName = [self accessibilityLabel]; 219 if (newName != nil && [newName length] > 0) { 220 return newName; 221 } 222 } 223 if ([self hasChildren]) { 224 for (SemanticsObject* child in self.children) { 225 NSString* newName = [child routeName]; 226 if (newName != nil && [newName length] > 0) { 227 return newName; 228 } 229 } 230 } 231 return nil; 232} 233 234- (NSString*)accessibilityLabel { 235 if ([self node].label.empty()) 236 return nil; 237 return @([self node].label.data()); 238} 239 240- (NSString*)accessibilityHint { 241 if ([self node].hint.empty()) 242 return nil; 243 return @([self node].hint.data()); 244} 245 246- (NSString*)accessibilityValue { 247 if ([self node].value.empty()) 248 return nil; 249 return @([self node].value.data()); 250} 251 252- (CGRect)accessibilityFrame { 253 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { 254 return [super accessibilityFrame]; 255 } 256 return [self globalRect]; 257} 258 259- (CGRect)globalRect { 260 SkMatrix44 globalTransform = [self node].transform; 261 for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) { 262 globalTransform = parent.node.transform * globalTransform; 263 } 264 265 SkPoint quad[4]; 266 [self node].rect.toQuad(quad); 267 for (auto& point : quad) { 268 SkScalar vector[4] = {point.x(), point.y(), 0, 1}; 269 globalTransform.mapScalars(vector); 270 point.set(vector[0] / vector[3], vector[1] / vector[3]); 271 } 272 SkRect rect; 273 rect.set(quad, 4); 274 275 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in 276 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to 277 // convert. 278 CGFloat scale = [[[self bridge]->view() window] screen].scale; 279 auto result = 280 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale); 281 return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge] -> view()); 282} 283 284#pragma mark - UIAccessibilityElement protocol 285 286- (id)accessibilityContainer { 287 if ([self hasChildren] || [self uid] == kRootNodeId) { 288 if (_container == nil) 289 _container.reset([[SemanticsObjectContainer alloc] initWithSemanticsObject:self 290 bridge:[self bridge]]); 291 return _container.get(); 292 } 293 if ([self parent] == nil) { 294 // This can happen when we have released the accessibility tree but iOS is 295 // still holding onto our objects. iOS can take some time before it 296 // realizes that the tree has changed. 297 return nil; 298 } 299 return [[self parent] accessibilityContainer]; 300} 301 302#pragma mark - UIAccessibilityAction overrides 303 304- (BOOL)accessibilityActivate { 305 if (![self node].HasAction(flutter::SemanticsAction::kTap)) 306 return NO; 307 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kTap); 308 return YES; 309} 310 311- (void)accessibilityIncrement { 312 if ([self node].HasAction(flutter::SemanticsAction::kIncrease)) { 313 [self node].value = [self node].increasedValue; 314 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kIncrease); 315 } 316} 317 318- (void)accessibilityDecrement { 319 if ([self node].HasAction(flutter::SemanticsAction::kDecrease)) { 320 [self node].value = [self node].decreasedValue; 321 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDecrease); 322 } 323} 324 325- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { 326 flutter::SemanticsAction action = GetSemanticsActionForScrollDirection(direction); 327 if (![self node].HasAction(action)) 328 return NO; 329 [self bridge] -> DispatchSemanticsAction([self uid], action); 330 return YES; 331} 332 333- (BOOL)accessibilityPerformEscape { 334 if (![self node].HasAction(flutter::SemanticsAction::kDismiss)) 335 return NO; 336 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kDismiss); 337 return YES; 338} 339 340#pragma mark UIAccessibilityFocus overrides 341 342- (void)accessibilityElementDidBecomeFocused { 343 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden)) { 344 [self bridge] -> DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen); 345 } 346 if ([self node].HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) { 347 [self bridge] -> DispatchSemanticsAction([self uid], 348 flutter::SemanticsAction::kDidGainAccessibilityFocus); 349 } 350} 351 352- (void)accessibilityElementDidLoseFocus { 353 if ([self node].HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) { 354 [self bridge] -> DispatchSemanticsAction([self uid], 355 flutter::SemanticsAction::kDidLoseAccessibilityFocus); 356 } 357} 358 359@end 360 361@implementation FlutterSemanticsObject { 362} 363 364#pragma mark - Override base class designated initializers 365 366// Method declared as unavailable in the interface 367- (instancetype)init { 368 [self release]; 369 [super doesNotRecognizeSelector:_cmd]; 370 return nil; 371} 372 373#pragma mark - Designated initializers 374 375- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge uid:(int32_t)uid { 376 self = [super initWithBridge:bridge uid:uid]; 377 return self; 378} 379 380#pragma mark - UIAccessibility overrides 381 382- (UIAccessibilityTraits)accessibilityTraits { 383 UIAccessibilityTraits traits = UIAccessibilityTraitNone; 384 if ([self node].HasAction(flutter::SemanticsAction::kIncrease) || 385 [self node].HasAction(flutter::SemanticsAction::kDecrease)) { 386 traits |= UIAccessibilityTraitAdjustable; 387 } 388 // TODO(jonahwilliams): switches should have a value of "on" or "off" 389 if ([self node].HasFlag(flutter::SemanticsFlags::kIsSelected) || 390 [self node].HasFlag(flutter::SemanticsFlags::kIsToggled) || 391 [self node].HasFlag(flutter::SemanticsFlags::kIsChecked)) { 392 traits |= UIAccessibilityTraitSelected; 393 } 394 if ([self node].HasFlag(flutter::SemanticsFlags::kIsButton)) { 395 traits |= UIAccessibilityTraitButton; 396 } 397 if ([self node].HasFlag(flutter::SemanticsFlags::kHasEnabledState) && 398 ![self node].HasFlag(flutter::SemanticsFlags::kIsEnabled)) { 399 traits |= UIAccessibilityTraitNotEnabled; 400 } 401 if ([self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) { 402 traits |= UIAccessibilityTraitHeader; 403 } 404 if ([self node].HasFlag(flutter::SemanticsFlags::kIsImage)) { 405 traits |= UIAccessibilityTraitImage; 406 } 407 if ([self node].HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) { 408 traits |= UIAccessibilityTraitUpdatesFrequently; 409 } 410 return traits; 411} 412 413@end 414 415@implementation FlutterPlatformViewSemanticsContainer { 416 SemanticsObject* _semanticsObject; 417 UIView* _platformView; 418} 419 420// Method declared as unavailable in the interface 421- (instancetype)init { 422 [self release]; 423 [super doesNotRecognizeSelector:_cmd]; 424 return nil; 425} 426 427- (instancetype)initWithSemanticsObject:(SemanticsObject*)object { 428 FML_CHECK(object); 429 if (self = [super init]) { 430 _semanticsObject = object; 431 flutter::FlutterPlatformViewsController* controller = 432 object.bridge->GetPlatformViewsController(); 433 if (controller) { 434 _platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view]; 435 } 436 self.accessibilityElements = @[ _semanticsObject, _platformView ]; 437 } 438 return self; 439} 440 441- (CGRect)accessibilityFrame { 442 return _semanticsObject.accessibilityFrame; 443} 444 445- (BOOL)isAccessibilityElement { 446 return NO; 447} 448 449- (id)accessibilityContainer { 450 return [_semanticsObject accessibilityContainer]; 451} 452 453- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { 454 return [_platformView accessibilityScroll:direction]; 455} 456 457@end 458 459@implementation SemanticsObjectContainer { 460 SemanticsObject* _semanticsObject; 461 fml::WeakPtr<flutter::AccessibilityBridge> _bridge; 462} 463 464#pragma mark - initializers 465 466// Method declared as unavailable in the interface 467- (instancetype)init { 468 [self release]; 469 [super doesNotRecognizeSelector:_cmd]; 470 return nil; 471} 472 473- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject 474 bridge:(fml::WeakPtr<flutter::AccessibilityBridge>)bridge { 475 FML_DCHECK(semanticsObject) << "semanticsObject must be set"; 476 self = [super init]; 477 478 if (self) { 479 _semanticsObject = semanticsObject; 480 _bridge = bridge; 481 } 482 483 return self; 484} 485 486#pragma mark - UIAccessibilityContainer overrides 487 488- (NSInteger)accessibilityElementCount { 489 NSInteger count = [[_semanticsObject children] count] + 1; 490 return count; 491} 492 493- (nullable id)accessibilityElementAtIndex:(NSInteger)index { 494 if (index < 0 || index >= [self accessibilityElementCount]) 495 return nil; 496 if (index == 0) { 497 return _semanticsObject; 498 } 499 500 SemanticsObject* child = [_semanticsObject children][index - 1]; 501 502 // Swap the original `SemanticsObject` to a `PlatformViewSemanticsContainer` 503 if (child.node.IsPlatformViewNode()) { 504 child.platformViewSemanticsContainer.index = index; 505 return child.platformViewSemanticsContainer; 506 } 507 508 if ([child hasChildren]) 509 return [child accessibilityContainer]; 510 return child; 511} 512 513- (NSInteger)indexOfAccessibilityElement:(id)element { 514 if (element == _semanticsObject) 515 return 0; 516 517 // FlutterPlatformViewSemanticsContainer is always the last element of its parent. 518 if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) { 519 return ((FlutterPlatformViewSemanticsContainer*)element).index; 520 } 521 522 NSMutableArray<SemanticsObject*>* children = [_semanticsObject children]; 523 for (size_t i = 0; i < [children count]; i++) { 524 SemanticsObject* child = children[i]; 525 if ((![child hasChildren] && child == element) || 526 ([child hasChildren] && [child accessibilityContainer] == element)) 527 return i + 1; 528 } 529 return NSNotFound; 530} 531 532#pragma mark - UIAccessibilityElement protocol 533 534- (BOOL)isAccessibilityElement { 535 return NO; 536} 537 538- (CGRect)accessibilityFrame { 539 return [_semanticsObject accessibilityFrame]; 540} 541 542- (id)accessibilityContainer { 543 if (!_bridge) { 544 return nil; 545 } 546 return ([_semanticsObject uid] == kRootNodeId) 547 ? _bridge->view() 548 : [[_semanticsObject parent] accessibilityContainer]; 549} 550 551#pragma mark - UIAccessibilityAction overrides 552 553- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { 554 return [_semanticsObject accessibilityScroll:direction]; 555} 556 557@end 558 559#pragma mark - AccessibilityBridge impl 560 561namespace flutter { 562 563AccessibilityBridge::AccessibilityBridge(UIView* view, 564 PlatformViewIOS* platform_view, 565 FlutterPlatformViewsController* platform_views_controller) 566 : view_(view), 567 platform_view_(platform_view), 568 platform_views_controller_(platform_views_controller), 569 objects_([[NSMutableDictionary alloc] init]), 570 weak_factory_(this), 571 previous_route_id_(0), 572 previous_routes_({}) { 573 accessibility_channel_.reset([[FlutterBasicMessageChannel alloc] 574 initWithName:@"flutter/accessibility" 575 binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger 576 codec:[FlutterStandardMessageCodec sharedInstance]]); 577 [accessibility_channel_.get() setMessageHandler:^(id message, FlutterReply reply) { 578 HandleEvent((NSDictionary*)message); 579 }]; 580} 581 582AccessibilityBridge::~AccessibilityBridge() { 583 clearState(); 584 view_.accessibilityElements = nil; 585} 586 587UIView<UITextInput>* AccessibilityBridge::textInputView() { 588 return [platform_view_->GetTextInputPlugin() textInputView]; 589} 590 591void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes, 592 flutter::CustomAccessibilityActionUpdates actions) { 593 BOOL layoutChanged = NO; 594 BOOL scrollOccured = NO; 595 for (const auto& entry : actions) { 596 const flutter::CustomAccessibilityAction& action = entry.second; 597 actions_[action.id] = action; 598 } 599 for (const auto& entry : nodes) { 600 const flutter::SemanticsNode& node = entry.second; 601 SemanticsObject* object = GetOrCreateObject(node.id, nodes); 602 layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node]; 603 scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node]; 604 [object setSemanticsNode:&node]; 605 NSUInteger newChildCount = node.childrenInTraversalOrder.size(); 606 NSMutableArray* newChildren = 607 [[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease]; 608 for (NSUInteger i = 0; i < newChildCount; ++i) { 609 SemanticsObject* child = GetOrCreateObject(node.childrenInTraversalOrder[i], nodes); 610 child.parent = object; 611 [newChildren addObject:child]; 612 } 613 object.children = newChildren; 614 if (node.customAccessibilityActions.size() > 0) { 615 NSMutableArray<FlutterCustomAccessibilityAction*>* accessibilityCustomActions = 616 [[[NSMutableArray alloc] init] autorelease]; 617 for (int32_t action_id : node.customAccessibilityActions) { 618 flutter::CustomAccessibilityAction& action = actions_[action_id]; 619 if (action.overrideId != -1) { 620 // iOS does not support overriding standard actions, so we ignore any 621 // custom actions that have an override id provided. 622 continue; 623 } 624 NSString* label = @(action.label.data()); 625 SEL selector = @selector(onCustomAccessibilityAction:); 626 FlutterCustomAccessibilityAction* customAction = 627 [[FlutterCustomAccessibilityAction alloc] initWithName:label 628 target:object 629 selector:selector]; 630 customAction.uid = action_id; 631 [accessibilityCustomActions addObject:customAction]; 632 } 633 object.accessibilityCustomActions = accessibilityCustomActions; 634 } 635 636 if (object.node.IsPlatformViewNode()) { 637 FlutterPlatformViewsController* controller = GetPlatformViewsController(); 638 if (controller) { 639 object.platformViewSemanticsContainer = 640 [[FlutterPlatformViewSemanticsContainer alloc] initWithSemanticsObject:object]; 641 } 642 } else if (object.platformViewSemanticsContainer) { 643 [object.platformViewSemanticsContainer release]; 644 } 645 } 646 647 SemanticsObject* root = objects_.get()[@(kRootNodeId)]; 648 649 bool routeChanged = false; 650 SemanticsObject* lastAdded = nil; 651 652 if (root) { 653 if (!view_.accessibilityElements) { 654 view_.accessibilityElements = @[ [root accessibilityContainer] ]; 655 } 656 NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease]; 657 [root collectRoutes:newRoutes]; 658 for (SemanticsObject* route in newRoutes) { 659 if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) != 660 previous_routes_.end()) { 661 lastAdded = route; 662 } 663 } 664 if (lastAdded == nil && [newRoutes count] > 0) { 665 int index = [newRoutes count] - 1; 666 lastAdded = [newRoutes objectAtIndex:index]; 667 } 668 if (lastAdded != nil && [lastAdded uid] != previous_route_id_) { 669 previous_route_id_ = [lastAdded uid]; 670 routeChanged = true; 671 } 672 previous_routes_.clear(); 673 for (SemanticsObject* route in newRoutes) { 674 previous_routes_.push_back([route uid]); 675 } 676 } else { 677 view_.accessibilityElements = nil; 678 } 679 680 NSMutableArray<NSNumber*>* doomed_uids = [NSMutableArray arrayWithArray:[objects_.get() allKeys]]; 681 if (root) 682 VisitObjectsRecursivelyAndRemove(root, doomed_uids); 683 [objects_ removeObjectsForKeys:doomed_uids]; 684 685 layoutChanged = layoutChanged || [doomed_uids count] > 0; 686 if (routeChanged) { 687 NSString* routeName = [lastAdded routeName]; 688 UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName); 689 } else if (layoutChanged) { 690 // TODO(goderbauer): figure out which node to focus next. 691 UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); 692 } 693 if (scrollOccured) { 694 // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5") 695 UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @""); 696 } 697} 698 699void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, flutter::SemanticsAction action) { 700 platform_view_->DispatchSemanticsAction(uid, action, {}); 701} 702 703void AccessibilityBridge::DispatchSemanticsAction(int32_t uid, 704 flutter::SemanticsAction action, 705 std::vector<uint8_t> args) { 706 platform_view_->DispatchSemanticsAction(uid, action, std::move(args)); 707} 708 709SemanticsObject* AccessibilityBridge::GetOrCreateObject(int32_t uid, 710 flutter::SemanticsNodeUpdates& updates) { 711 SemanticsObject* object = objects_.get()[@(uid)]; 712 if (!object) { 713 // New node case: simply create a new SemanticsObject. 714 flutter::SemanticsNode node = updates[uid]; 715 if (node.HasFlag(flutter::SemanticsFlags::kIsTextField) && 716 !node.HasFlag(flutter::SemanticsFlags::kIsReadOnly)) { 717 // Text fields are backed by objects that implement UITextInput. 718 object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease]; 719 } else { 720 object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr() uid:uid] autorelease]; 721 } 722 723 objects_.get()[@(uid)] = object; 724 } else { 725 // Existing node case 726 auto nodeEntry = updates.find(object.node.id); 727 if (nodeEntry != updates.end()) { 728 // There's an update for this node 729 flutter::SemanticsNode node = nodeEntry->second; 730 BOOL isTextField = node.HasFlag(flutter::SemanticsFlags::kIsTextField); 731 BOOL wasTextField = object.node.HasFlag(flutter::SemanticsFlags::kIsTextField); 732 BOOL isReadOnly = node.HasFlag(flutter::SemanticsFlags::kIsReadOnly); 733 BOOL wasReadOnly = object.node.HasFlag(flutter::SemanticsFlags::kIsReadOnly); 734 if (wasTextField != isTextField || isReadOnly != wasReadOnly) { 735 // The node changed its type from text field to something else, or vice versa. In this 736 // case, we cannot reuse the existing SemanticsObject implementation. Instead, we replace 737 // it with a new instance. 738 NSUInteger positionInChildlist = [object.parent.children indexOfObject:object]; 739 SemanticsObject* parent = object.parent; 740 [objects_ removeObjectForKey:@(node.id)]; 741 if (isTextField && !isReadOnly) { 742 // Text fields are backed by objects that implement UITextInput. 743 object = [[[TextInputSemanticsObject alloc] initWithBridge:GetWeakPtr() 744 uid:uid] autorelease]; 745 } else { 746 object = [[[FlutterSemanticsObject alloc] initWithBridge:GetWeakPtr() 747 uid:uid] autorelease]; 748 } 749 object.parent = parent; 750 [object.parent.children replaceObjectAtIndex:positionInChildlist withObject:object]; 751 objects_.get()[@(node.id)] = object; 752 } 753 } 754 } 755 return object; 756} 757 758void AccessibilityBridge::VisitObjectsRecursivelyAndRemove(SemanticsObject* object, 759 NSMutableArray<NSNumber*>* doomed_uids) { 760 [doomed_uids removeObject:@(object.uid)]; 761 for (SemanticsObject* child in [object children]) 762 VisitObjectsRecursivelyAndRemove(child, doomed_uids); 763} 764 765void AccessibilityBridge::HandleEvent(NSDictionary<NSString*, id>* annotatedEvent) { 766 NSString* type = annotatedEvent[@"type"]; 767 if ([type isEqualToString:@"announce"]) { 768 NSString* message = annotatedEvent[@"data"][@"message"]; 769 UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message); 770 } 771} 772 773fml::WeakPtr<AccessibilityBridge> AccessibilityBridge::GetWeakPtr() { 774 return weak_factory_.GetWeakPtr(); 775} 776 777void AccessibilityBridge::clearState() { 778 [objects_ removeAllObjects]; 779 previous_route_id_ = 0; 780 previous_routes_.clear(); 781} 782 783} // namespace flutter 784