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