Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
SemanticsObject.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
9
11
12namespace {
13
15 UIAccessibilityScrollDirection direction) {
16 // To describe the vertical scroll direction, UIAccessibilityScrollDirection uses the
17 // direction the scroll bar moves in and SemanticsAction uses the direction the finger
18 // moves in. However, the horizontal scroll direction matches the SemanticsAction direction.
19 // That is way the following maps vertical opposite of the SemanticsAction, but the horizontal
20 // maps directly.
21 switch (direction) {
22 case UIAccessibilityScrollDirectionRight:
23 case UIAccessibilityScrollDirectionPrevious: // TODO(abarth): Support RTL using
24 // _node.textDirection.
26 case UIAccessibilityScrollDirectionLeft:
27 case UIAccessibilityScrollDirectionNext: // TODO(abarth): Support RTL using
28 // _node.textDirection.
30 case UIAccessibilityScrollDirectionUp:
32 case UIAccessibilityScrollDirectionDown:
34 }
35 FML_DCHECK(false); // Unreachable
37}
38
40 SkM44 globalTransform = [reference node].transform;
41 for (SemanticsObject* parent = [reference parent]; parent; parent = parent.parent) {
42 globalTransform = parent.node.transform * globalTransform;
43 }
44 return globalTransform;
45}
46
47SkPoint ApplyTransform(SkPoint& point, const SkM44& transform) {
48 SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
49 return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
50}
51
52CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) {
53 UIView* containerView = [reference bridgeView];
54 if (!containerView) {
55 return CGPointZero;
56 }
57 SkM44 globalTransform = GetGlobalTransform(reference);
58 SkPoint point = SkPoint::Make(local_point.x, local_point.y);
59 point = ApplyTransform(point, globalTransform);
60 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
61 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
62 // convert.
63 UIScreen* screen = containerView.window.screen;
64 // Screen can be nil if the FlutterView is covered by another native view.
65 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
66 auto result = CGPointMake(point.x() / scale, point.y() / scale);
67 return [containerView convertPoint:result toView:nil];
68}
69
70CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
71 UIView* containerView = [reference bridgeView];
72 if (!containerView) {
73 return CGRectZero;
74 }
75 SkM44 globalTransform = GetGlobalTransform(reference);
76
77 SkPoint quad[4] = {
78 SkPoint::Make(local_rect.origin.x, local_rect.origin.y), // top left
79 SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y), // top right
80 SkPoint::Make(local_rect.origin.x + local_rect.size.width,
81 local_rect.origin.y + local_rect.size.height), // bottom right
82 SkPoint::Make(local_rect.origin.x,
83 local_rect.origin.y + local_rect.size.height) // bottom left
84 };
85 for (auto& point : quad) {
86 point = ApplyTransform(point, globalTransform);
87 }
88 SkRect rect;
89 NSCAssert(rect.setBoundsCheck({quad, 4}), @"Transformed points can't form a rect");
90 rect.setBounds({quad, 4});
91
92 // `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
93 // the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
94 // convert.
95 UIScreen* screen = containerView.window.screen;
96 // Screen can be nil if the FlutterView is covered by another native view.
97 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
98 auto result =
99 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
100 return UIAccessibilityConvertFrameToScreenCoordinates(result, containerView);
101}
102
103} // namespace
104
106@property(nonatomic, retain, readonly) UISwitch* nativeSwitch;
107@end
108
109@implementation FlutterSwitchSemanticsObject
110
111- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
112 uid:(int32_t)uid {
113 self = [super initWithBridge:bridge uid:uid];
114 if (self) {
115 _nativeSwitch = [[UISwitch alloc] init];
116 }
117 return self;
118}
119
120- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
121 NSMethodSignature* result = [super methodSignatureForSelector:sel];
122 if (!result) {
123 result = [self.nativeSwitch methodSignatureForSelector:sel];
124 }
125 return result;
126}
127
128- (void)forwardInvocation:(NSInvocation*)anInvocation {
129 anInvocation.target = self.nativeSwitch;
130 [anInvocation invoke];
131}
132
133- (NSString*)accessibilityValue {
134 self.nativeSwitch.on = self.node.flags.isToggled == flutter::SemanticsTristate::kTrue ||
135 self.node.flags.isChecked == flutter::SemanticsCheckState::kTrue;
136
137 if (!self.bridge) {
138 return nil;
139 } else {
140 return self.nativeSwitch.accessibilityValue;
141 }
142}
143
144- (UIAccessibilityTraits)accessibilityTraits {
145 self.nativeSwitch.enabled = self.node.flags.isEnabled == flutter::SemanticsTristate::kTrue;
146
147 return self.nativeSwitch.accessibilityTraits;
148}
149
150@end // FlutterSwitchSemanticsObject
151
154@end
155
157
158- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
159 uid:(int32_t)uid {
160 self = [super initWithBridge:bridge uid:uid];
161 if (self) {
162 _scrollView = [[FlutterSemanticsScrollView alloc] initWithSemanticsObject:self];
163 [_scrollView setShowsHorizontalScrollIndicator:NO];
164 [_scrollView setShowsVerticalScrollIndicator:NO];
165 [_scrollView setContentInset:UIEdgeInsetsZero];
166 [_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
167 UIView* containerView = self.bridgeView;
168 if (containerView) {
169 [containerView addSubview:_scrollView];
170 }
171 }
172 return self;
173}
174
175- (void)dealloc {
176 [_scrollView removeFromSuperview];
177}
178
180 // In order to make iOS think this UIScrollView is scrollable, the following
181 // requirements must be true.
182 // 1. contentSize must be bigger than the frame size.
183 // 2. The scrollable isAccessibilityElement must return YES
184 //
185 // Once the requirements are met, the iOS uses contentOffset to determine
186 // what scroll actions are available. e.g. If the view scrolls vertically and
187 // contentOffset is 0.0, only the scroll down action is available.
188 self.scrollView.frame = self.accessibilityFrame;
189 self.scrollView.contentSize = [self contentSizeInternal];
190 // See the documentation on `isDoingSystemScrolling`.
191 if (!self.scrollView.isDoingSystemScrolling) {
192 [self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
193 }
194}
195
197 return self.scrollView;
198}
199
200// private methods
201
202- (float)scrollExtentMax {
203 if (!self.bridge) {
204 return 0.0f;
205 }
206 float scrollExtentMax = self.node.scrollExtentMax;
207 if (isnan(scrollExtentMax)) {
208 scrollExtentMax = 0.0f;
209 } else if (!isfinite(scrollExtentMax)) {
210 scrollExtentMax = kScrollExtentMaxForInf + [self scrollPosition];
211 }
212 return scrollExtentMax;
213}
214
215- (float)scrollPosition {
216 if (!self.bridge) {
217 return 0.0f;
218 }
219 float scrollPosition = self.node.scrollPosition;
220 if (isnan(scrollPosition)) {
221 scrollPosition = 0.0f;
222 }
223 NSCAssert(isfinite(scrollPosition), @"The scrollPosition must not be infinity");
224 return scrollPosition;
225}
226
227- (CGSize)contentSizeInternal {
228 CGRect result;
229 const SkRect& rect = self.node.rect;
230
232 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [self scrollExtentMax]);
233 } else if (self.node.actions & flutter::kHorizontalScrollSemanticsActions) {
234 result = CGRectMake(rect.x(), rect.y(), rect.width() + [self scrollExtentMax], rect.height());
235 } else {
236 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
237 }
238 return ConvertRectToGlobal(self, result).size;
239}
240
241- (CGPoint)contentOffsetInternal {
242 CGPoint result;
243 CGPoint origin = self.scrollView.frame.origin;
244 const SkRect& rect = self.node.rect;
246 result = ConvertPointToGlobal(self, CGPointMake(rect.x(), rect.y() + [self scrollPosition]));
247 } else if (self.node.actions & flutter::kHorizontalScrollSemanticsActions) {
248 result = ConvertPointToGlobal(self, CGPointMake(rect.x() + [self scrollPosition], rect.y()));
249 } else {
250 result = origin;
251 }
252 return CGPointMake(result.x - origin.x, result.y - origin.y);
253}
254
255@end // FlutterScrollableSemanticsObject
256
257@implementation FlutterCustomAccessibilityAction {
258}
259@end
260
261@interface SemanticsObject ()
262@property(nonatomic) SemanticsObjectContainer* container;
263
264/** Should only be called in conjunction with setting child/parent relationship. */
265@property(nonatomic, weak, readwrite) SemanticsObject* parent;
266
267@end
268
269@implementation SemanticsObject {
271 NSMutableArray<SemanticsObject*>* _children;
273}
274
275#pragma mark - Designated initializers
276
277- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
278 uid:(int32_t)uid {
279 FML_DCHECK(bridge) << "bridge must be set";
280 FML_DCHECK(uid >= kRootNodeId);
281 // Initialize with the UIView as the container.
282 // The UIView will not necessarily be accessibility parent for this object.
283 // The bridge informs the OS of the actual structure via
284 // `accessibilityContainer` and `accessibilityElementAtIndex`.
285 self = [super initWithAccessibilityContainer:bridge ? bridge->view() : nil];
286
287 if (self) {
288 _weakBridge = bridge;
289 _uid = uid;
290 _children = [[NSMutableArray alloc] init];
291 _childrenInHitTestOrder = [[NSArray alloc] init];
292 }
293
294 return self;
295}
296
297- (void)dealloc {
298 // Set parent and children parents to nil explicitly in dealloc.
299 // -[UIAccessibilityElement dealloc] has in the past called into -accessibilityContainer
300 // and self.children. There have also been crashes related to iOS
301 // accessing methods during dealloc, and there's a lag before the tree changes.
302 // See https://github.com/flutter/engine/pull/4602 and
303 // https://github.com/flutter/engine/pull/27786.
304 for (SemanticsObject* child in _children) {
305 child.parent = nil;
306 }
307 [_children removeAllObjects];
308
309 _parent = nil;
310 _inDealloc = YES;
311}
312
313#pragma mark - Semantic object property accesser
314
315- (NSArray<SemanticsObject*>*)children {
316 return [_children copy];
317}
318
319- (void)setChildren:(NSArray<SemanticsObject*>*)children {
320 for (SemanticsObject* child in _children) {
321 child.parent = nil;
322 }
323 _children = [children mutableCopy];
324 for (SemanticsObject* child in _children) {
325 child.parent = self;
326 }
327}
328
329- (void)setChildrenInHitTestOrder:(NSArray<SemanticsObject*>*)childrenInHitTestOrder {
330 for (SemanticsObject* child in _childrenInHitTestOrder) {
331 child.parent = nil;
332 }
333 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
334 for (SemanticsObject* child in _childrenInHitTestOrder) {
335 child.parent = self;
336 }
337}
338
339- (BOOL)hasChildren {
340 return [self.children count] != 0;
341}
342
343#pragma mark - Semantic object method
344
345- (flutter::AccessibilityBridgeIos*)bridge {
346 return _weakBridge.get();
347}
348
349- (UIView*)bridgeView {
350 flutter::AccessibilityBridgeIos* bridge = [self bridge];
351 return bridge ? bridge->view() : nil;
352}
353
354- (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
355 _node = *node;
356}
357
358- (void)accessibilityBridgeDidFinishUpdate { /* Do nothing by default */
359}
360
361/**
362 * Whether calling `setSemanticsNode:` with `node` would cause a layout change.
363 */
364- (BOOL)nodeWillCauseLayoutChange:(const flutter::SemanticsNode*)node {
365 return self.node.rect != node->rect || self.node.transform != node->transform;
366}
367
368/**
369 * Whether calling `setSemanticsNode:` with `node` would cause a scroll event.
370 */
371- (BOOL)nodeWillCauseScroll:(const flutter::SemanticsNode*)node {
372 return !isnan(self.node.scrollPosition) && !isnan(node->scrollPosition) &&
373 self.node.scrollPosition != node->scrollPosition;
374}
375
376/**
377 * Whether calling `setSemanticsNode:` with `node` should trigger an
378 * announcement.
379 */
380- (BOOL)nodeShouldTriggerAnnouncement:(const flutter::SemanticsNode*)node {
381 // The node dropped the live region flag, if it ever had one.
382 if (!node || !node->flags.isLiveRegion) {
383 return NO;
384 }
385
386 // The node has gained a new live region flag, always announce.
387 if (!self.node.flags.isLiveRegion) {
388 return YES;
389 }
390
391 // The label has updated, and the new node has a live region flag.
392 return self.node.label != node->label;
393}
394
395- (void)replaceChildAtIndex:(NSInteger)index withChild:(SemanticsObject*)child {
396 SemanticsObject* oldChild = _children[index];
397 oldChild.parent = nil;
398 child.parent = self;
399 [_children replaceObjectAtIndex:index withObject:child];
400}
401
402- (NSString*)routeName {
403 // Returns the first non-null and non-empty semantic label of a child
404 // with an NamesRoute flag. Otherwise returns nil.
405 if (self.node.flags.namesRoute) {
406 NSString* newName = self.accessibilityLabel;
407 if (newName != nil && [newName length] > 0) {
408 return newName;
409 }
410 }
411 if ([self hasChildren]) {
412 for (SemanticsObject* child in self.children) {
413 NSString* newName = [child routeName];
414 if (newName != nil && [newName length] > 0) {
415 return newName;
416 }
417 }
418 }
419 return nil;
420}
421
422- (id)nativeAccessibility {
423 return self;
424}
425
426- (NSAttributedString*)createAttributedStringFromString:(NSString*)string
427 withAttributes:
428 (const flutter::StringAttributes&)attributes {
429 NSMutableAttributedString* attributedString =
430 [[NSMutableAttributedString alloc] initWithString:string];
431 for (const auto& attribute : attributes) {
432 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
433 switch (attribute->type) {
435 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
436 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
437 NSDictionary* attributeDict = @{
438 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
439 };
440 [attributedString setAttributes:attributeDict range:range];
441 break;
442 }
444 NSDictionary* attributeDict = @{
445 UIAccessibilitySpeechAttributeSpellOut : @YES,
446 };
447 [attributedString setAttributes:attributeDict range:range];
448 break;
449 }
450 }
451 }
452 return attributedString;
453}
454
455- (void)showOnScreen {
456 flutter::AccessibilityBridgeIos* bridge = self.bridge;
457 if (!bridge) {
458 return;
459 }
461}
462
463#pragma mark - UIAccessibility overrides
464
465- (BOOL)isAccessibilityElement {
466 if (!self.bridge) {
467 return false;
468 }
469
470 // Note: hit detection will only apply to elements that report
471 // -isAccessibilityElement of YES. The framework will continue scanning the
472 // entire element tree looking for such a hit.
473
474 // We enforce in the framework that no other useful semantics are merged with these nodes.
475 if (self.node.flags.scopesRoute) {
476 return false;
477 }
478
479 return [self isFocusable];
480}
481
482- (NSString*)accessibilityLanguage {
483 flutter::AccessibilityBridgeIos* bridge = self.bridge;
484 if (!bridge) {
485 return nil;
486 }
487
488 if (!self.node.locale.empty()) {
489 return @(self.node.locale.data());
490 }
491 return bridge->GetDefaultLocale();
492}
493
494- (bool)isFocusable {
495 // If the node is scrollable AND hidden OR
496 // The node has a label, value, or hint OR
497 // The node has non-scrolling related actions.
498 //
499 // The kIsHidden flag set with the scrollable flag means this node is now
500 // hidden but still is a valid target for a11y focus in the tree, e.g. a list
501 // item that is currently off screen but the a11y navigation needs to know
502 // about.
503 return (self.node.flags.hasImplicitScrolling && self.node.flags.isHidden)
504
505 || !self.node.label.empty() || !self.node.value.empty() || !self.node.hint.empty() ||
506 (self.node.actions & ~flutter::kScrollableSemanticsActions) != 0;
507}
508
509- (void)collectRoutes:(NSMutableArray<SemanticsObject*>*)edges {
510 if (self.node.flags.scopesRoute) {
511 [edges addObject:self];
512 }
513 if ([self hasChildren]) {
514 for (SemanticsObject* child in self.children) {
515 [child collectRoutes:edges];
516 }
517 }
518}
519
520- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action {
521 flutter::AccessibilityBridgeIos* bridge = self.bridge;
522 if (!bridge) {
523 return NO;
524 }
525 if (!self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
526 return NO;
527 }
528 int32_t action_id = action.uid;
529 std::vector<uint8_t> args;
530 args.push_back(3); // type=int32.
531 args.push_back(action_id);
532 args.push_back(action_id >> 8);
533 args.push_back(action_id >> 16);
534 args.push_back(action_id >> 24);
537 fml::MallocMapping::Copy(args.data(), args.size() * sizeof(uint8_t)));
538 return YES;
539}
540
541- (NSString*)accessibilityIdentifier {
542 if (!self.bridge) {
543 return nil;
544 }
545
546 if (self.node.identifier.empty()) {
547 return nil;
548 }
549 return @(self.node.identifier.data());
550}
551
552- (NSString*)accessibilityLabel {
553 if (!self.bridge) {
554 return nil;
555 }
556 NSString* label = nil;
557 if (!self.node.label.empty()) {
558 label = @(self.node.label.data());
559 }
560 if (!self.node.tooltip.empty()) {
561 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
562 : @(self.node.tooltip.data());
563 }
564 return label;
565}
566
567- (bool)containsPoint:(CGPoint)point {
568 // The point is in global coordinates, so use the global rect here.
569 return CGRectContainsPoint([self globalRect], point);
570}
571
572// Finds the first eligiable semantics object in hit test order.
573- (id)search:(CGPoint)point {
574 // Search children in hit test order.
575 for (SemanticsObject* child in [self childrenInHitTestOrder]) {
576 if ([child containsPoint:point]) {
577 id childSearchResult = [child search:point];
578 if (childSearchResult != nil) {
579 return childSearchResult;
580 }
581 }
582 }
583 // Check if the current semantic object should be returned.
584 if ([self containsPoint:point] && [self isFocusable]) {
585 return self.nativeAccessibility;
586 }
587 return nil;
588}
589
590// iOS uses this method to determine the hittest results when users touch
591// explore in VoiceOver.
592//
593// For overlapping UIAccessibilityElements (e.g. a stack) in IOS, the focus
594// goes to the smallest object before IOS 16, but to the top-left object in
595// IOS 16. Overrides this method to focus the first eligiable semantics
596// object in hit test order.
597- (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
598 if (!self.bridge) {
599 return nil;
600 }
601 return [self search:point];
602}
603
604// iOS calls this method when this item is swipe-to-focusd in VoiceOver.
605- (BOOL)accessibilityScrollToVisible {
606 [self showOnScreen];
607 return YES;
608}
609
610// iOS calls this method when this item is swipe-to-focusd in VoiceOver.
611- (BOOL)accessibilityScrollToVisibleWithChild:(id)child {
612 if ([child isKindOfClass:[SemanticsObject class]]) {
613 [child showOnScreen];
614 return YES;
615 }
616 return NO;
617}
618
619- (NSAttributedString*)accessibilityAttributedLabel {
620 NSString* label = self.accessibilityLabel;
621 if (label.length == 0) {
622 return nil;
623 }
624 return [self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
625}
626
627- (NSString*)accessibilityHint {
628 if (!self.bridge) {
629 return nil;
630 }
631
632 if (self.node.hint.empty()) {
633 return nil;
634 }
635 return @(self.node.hint.data());
636}
637
638- (NSAttributedString*)accessibilityAttributedHint {
639 NSString* hint = [self accessibilityHint];
640 if (hint.length == 0) {
641 return nil;
642 }
643 return [self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
644}
645
646- (NSString*)accessibilityValue {
647 if (!self.bridge) {
648 return nil;
649 }
650
651 if (!self.node.value.empty()) {
652 return @(self.node.value.data());
653 }
654
655 // iOS does not announce values of native radio buttons.
656 if (self.node.flags.isInMutuallyExclusiveGroup) {
657 return nil;
658 }
659
660 // FlutterSwitchSemanticsObject should supercede these conditionals.
661
662 if (self.node.flags.isToggled == flutter::SemanticsTristate::kTrue ||
663 self.node.flags.isChecked == flutter::SemanticsCheckState::kTrue) {
664 return @"1";
665 } else if (self.node.flags.isToggled == flutter::SemanticsTristate::kFalse ||
666 self.node.flags.isChecked == flutter::SemanticsCheckState::kFalse) {
667 return @"0";
668 }
669
670 return nil;
671}
672
673- (NSAttributedString*)accessibilityAttributedValue {
674 NSString* value = [self accessibilityValue];
675 if (value.length == 0) {
676 return nil;
677 }
678 return [self createAttributedStringFromString:value withAttributes:self.node.valueAttributes];
679}
680
681- (CGRect)accessibilityFrame {
682 if (!self.bridge) {
683 return CGRectZero;
684 }
685
686 if (self.node.flags.isHidden) {
687 return [super accessibilityFrame];
688 }
689 return [self globalRect];
690}
691
692- (CGRect)globalRect {
693 const SkRect& rect = self.node.rect;
694 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
695 return ConvertRectToGlobal(self, localRect);
696}
697
698#pragma mark - UIAccessibilityElement protocol
699
700- (void)setAccessibilityContainer:(id)container {
701 // Explicit noop. The containers are calculated lazily in `accessibilityContainer`.
702 // See also: https://github.com/flutter/flutter/issues/54366
703}
704
705- (id)accessibilityContainer {
706 if (_inDealloc) {
707 // In iOS9, `accessibilityContainer` will be called by `[UIAccessibilityElementSuperCategory
708 // dealloc]` during `[super dealloc]`. And will crash when accessing `_children` which has
709 // called `[_children release]` in `[SemanticsObject dealloc]`.
710 // https://github.com/flutter/flutter/issues/87247
711 return nil;
712 }
713
714 if (!self.bridge) {
715 return nil;
716 }
717
718 if ([self hasChildren] || self.uid == kRootNodeId) {
719 if (self.container == nil) {
720 self.container = [[SemanticsObjectContainer alloc] initWithSemanticsObject:self];
721 }
722 return self.container;
723 }
724 if (self.parent == nil) {
725 // This can happen when we have released the accessibility tree but iOS is
726 // still holding onto our objects. iOS can take some time before it
727 // realizes that the tree has changed.
728 return nil;
729 }
730 return self.parent.accessibilityContainer;
731}
732
733#pragma mark - UIAccessibilityAction overrides
734
735- (BOOL)accessibilityActivate {
736 flutter::AccessibilityBridgeIos* bridge = self.bridge;
737 if (!bridge) {
738 return NO;
739 }
740 if (!self.node.HasAction(flutter::SemanticsAction::kTap)) {
741 // Prevent sliders to receive a regular tap which will change the value.
742 //
743 // This is needed because it causes slider to select to middle if it
744 // does not have a semantics tap.
745 if (self.node.flags.isSlider) {
746 return YES;
747 }
748 return NO;
749 }
751 return YES;
752}
753
754- (void)accessibilityIncrement {
755 flutter::AccessibilityBridgeIos* bridge = self.bridge;
756 if (!bridge) {
757 return;
758 }
759 if (self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
760 self.node.value = self.node.increasedValue;
762 }
763}
764
765- (void)accessibilityDecrement {
766 flutter::AccessibilityBridgeIos* bridge = self.bridge;
767 if (!bridge) {
768 return;
769 }
770 if (self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
771 self.node.value = self.node.decreasedValue;
773 }
774}
775
776- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
777 flutter::AccessibilityBridgeIos* bridge = self.bridge;
778 if (!bridge) {
779 return NO;
780 }
782 if (!self.node.HasAction(action)) {
783 return NO;
784 }
785 bridge->DispatchSemanticsAction(self.uid, action);
786 return YES;
787}
788
789- (BOOL)accessibilityPerformEscape {
790 flutter::AccessibilityBridgeIos* bridge = self.bridge;
791 if (!bridge) {
792 return NO;
793 }
794 if (!self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
795 return NO;
796 }
798 return YES;
799}
800
801#pragma mark UIAccessibilityFocus overrides
802
803- (void)accessibilityElementDidBecomeFocused {
804 flutter::AccessibilityBridgeIos* bridge = self.bridge;
805 if (!bridge) {
806 return;
807 }
809 if (self.node.flags.isHidden || self.node.flags.isHeader) {
810 [self showOnScreen];
811 }
814 }
815}
816
817- (void)accessibilityElementDidLoseFocus {
818 flutter::AccessibilityBridgeIos* bridge = self.bridge;
819 if (!bridge) {
820 return;
821 }
825 }
826}
827
828- (BOOL)accessibilityRespondsToUserInteraction {
829 if (self.node.flags.isAccessibilityFocusBlocked) {
830 return false;
831 }
832
833 // Return true only if the node contains actions other than system actions.
834 if ((self.node.actions & ~flutter::kSystemActions) != 0) {
835 return true;
836 }
837
838 if (!self.node.customAccessibilityActions.empty()) {
839 return true;
840 }
841
842 return false;
843}
844
845@end
846
847@implementation FlutterSemanticsObject
848
849#pragma mark - Designated initializers
850
851- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
852 uid:(int32_t)uid {
853 self = [super initWithBridge:bridge uid:uid];
854 return self;
855}
856
857#pragma mark - UIAccessibility overrides
858
859- (UIAccessibilityTraits)accessibilityTraits {
860 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
861 if (self.node.HasAction(flutter::SemanticsAction::kIncrease) ||
863 traits |= UIAccessibilityTraitAdjustable;
864 }
865 // This should also capture radio buttons.
866 if (self.node.flags.isToggled != flutter::SemanticsTristate::kNone ||
867 self.node.flags.isChecked != flutter::SemanticsCheckState::kNone) {
868 traits |= UIAccessibilityTraitButton;
869 }
870 if (self.node.flags.isSelected == flutter::SemanticsTristate::kTrue) {
871 traits |= UIAccessibilityTraitSelected;
872 }
873 if (self.node.flags.isButton) {
874 traits |= UIAccessibilityTraitButton;
875 }
876 if (self.node.flags.isEnabled == flutter::SemanticsTristate::kFalse) {
877 traits |= UIAccessibilityTraitNotEnabled;
878 }
879 if (self.node.headingLevel > 0) {
880 // VoiceOver treats UIAccessibilityTraitHeader as heading.
881 traits |= UIAccessibilityTraitHeader;
882 }
883 if (self.node.flags.isImage) {
884 traits |= UIAccessibilityTraitImage;
885 }
886 if (self.node.flags.isLiveRegion) {
887 traits |= UIAccessibilityTraitUpdatesFrequently;
888 }
889 if (self.node.flags.isLink) {
890 traits |= UIAccessibilityTraitLink;
891 }
892 if (traits == UIAccessibilityTraitNone && ![self hasChildren] &&
893 self.accessibilityLabel.length != 0 && !self.node.flags.isTextField) {
894 traits = UIAccessibilityTraitStaticText;
895 }
896 return traits;
897}
898
899@end
900
902@property(nonatomic, weak) UIView* platformView;
903@end
904
906
907- (instancetype)initWithBridge:(fml::WeakPtr<flutter::AccessibilityBridgeIos>)bridge
908 uid:(int32_t)uid
909 platformView:(nonnull FlutterTouchInterceptingView*)platformView {
910 if (self = [super initWithBridge:bridge uid:uid]) {
911 _platformView = platformView;
912 [platformView setFlutterAccessibilityContainer:self];
913 }
914 return self;
915}
916
918 return self.platformView;
919}
920
921@end
922
923@implementation SemanticsObjectContainer {
924}
925
926#pragma mark - initializers
927
928- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject {
929 FML_DCHECK(semanticsObject) << "semanticsObject must be set";
930 // Initialize with the UIView as the container.
931 // The UIView will not necessarily be accessibility parent for this object.
932 // The bridge informs the OS of the actual structure via
933 // `accessibilityContainer` and `accessibilityElementAtIndex`.
934 self = [super initWithAccessibilityContainer:semanticsObject.bridgeView];
935
936 if (self) {
937 _semanticsObject = semanticsObject;
938 }
939
940 return self;
941}
942
943#pragma mark - UIAccessibilityContainer overrides
944
945- (NSInteger)accessibilityElementCount {
946 return self.semanticsObject.children.count + 1;
947}
948
949- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
950 if (index < 0 || index >= [self accessibilityElementCount]) {
951 return nil;
952 }
953 if (index == 0) {
954 return self.semanticsObject.nativeAccessibility;
955 }
956
957 SemanticsObject* child = self.semanticsObject.children[index - 1];
958
959 if ([child hasChildren]) {
960 return child.accessibilityContainer;
961 }
962 return child.nativeAccessibility;
963}
964
965- (NSInteger)indexOfAccessibilityElement:(id)element {
966 if (element == self.semanticsObject.nativeAccessibility) {
967 return 0;
968 }
969
970 NSArray<SemanticsObject*>* children = self.semanticsObject.children;
971 for (size_t i = 0; i < [children count]; i++) {
972 SemanticsObject* child = children[i];
973 if ((![child hasChildren] && child.nativeAccessibility == element) ||
974 ([child hasChildren] && [child.nativeAccessibility accessibilityContainer] == element)) {
975 return i + 1;
976 }
977 }
978 return NSNotFound;
979}
980
981#pragma mark - UIAccessibilityElement protocol
982
983- (BOOL)isAccessibilityElement {
984 return NO;
985}
986
987- (CGRect)accessibilityFrame {
988 // For OverlayPortals, the child element is sometimes outside the bounds of the parent
989 // Even if it's marked accessible, VoiceControl labels will not appear if it's too
990 // spatially distant. Set the frame to be the max screen size so all children are guaraenteed
991 // to be contained.
992
993 return UIScreen.mainScreen.bounds;
994}
995
996- (id)accessibilityContainer {
997 return ([self.semanticsObject uid] == kRootNodeId)
998 ? self.semanticsObject.bridgeView
999 : self.semanticsObject.parent.accessibilityContainer;
1000}
1001
1002#pragma mark - UIAccessibilityAction overrides
1003
1004- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
1005 return [self.semanticsObject accessibilityScroll:direction];
1006}
1007
1008@end
NS_ASSUME_NONNULL_BEGIN constexpr int32_t kRootNodeId
constexpr float kScrollExtentMaxForInf
NSMutableArray< SemanticsObject * > * _children
BOOL _inDealloc
Interface that represents an accessibility bridge for iOS.
virtual void DispatchSemanticsAction(int32_t id, flutter::SemanticsAction action)=0
virtual void AccessibilityObjectDidLoseFocus(int32_t id)=0
virtual void AccessibilityObjectDidBecomeFocused(int32_t id)=0
virtual NSString * GetDefaultLocale()=0
virtual UIView * view() const =0
static MallocMapping Copy(const T *begin, const T *end)
Definition mapping.h:162
int32_t value
G_BEGIN_DECLS G_MODULE_EXPORT FlValue * args
#define FML_DCHECK(condition)
Definition logging.h:122
FlutterSemanticsScrollView * scrollView
NSArray< SemanticsObject * > * children
SemanticsObject * parent
nullable UIView * bridgeView()
void accessibilityBridgeDidFinishUpdate()
nullable flutter::AccessibilityBridgeIos * bridge()
size_t length
CGRect ConvertRectToGlobal(SemanticsObject *reference, CGRect local_rect)
flutter::SemanticsAction GetSemanticsActionForScrollDirection(UIAccessibilityScrollDirection direction)
CGPoint ConvertPointToGlobal(SemanticsObject *reference, CGPoint local_point)
SkM44 GetGlobalTransform(SemanticsObject *reference)
SkPoint ApplyTransform(SkPoint &point, const SkM44 &transform)
auto WeakPtr(const std::shared_ptr< T > &pointer)
constexpr int kHorizontalScrollSemanticsActions
constexpr int kSystemActions
The following actions are not user-initiated.
constexpr int kVerticalScrollSemanticsActions
std::vector< StringAttributePtr > StringAttributes
const uintptr_t id
int BOOL