15 UIAccessibilityScrollDirection direction) {
22 case UIAccessibilityScrollDirectionRight:
23 case UIAccessibilityScrollDirectionPrevious:
25 return flutter::SemanticsAction::kScrollRight;
26 case UIAccessibilityScrollDirectionLeft:
27 case UIAccessibilityScrollDirectionNext:
29 return flutter::SemanticsAction::kScrollLeft;
30 case UIAccessibilityScrollDirectionUp:
31 return flutter::SemanticsAction::kScrollDown;
32 case UIAccessibilityScrollDirectionDown:
33 return flutter::SemanticsAction::kScrollUp;
36 return flutter::SemanticsAction::kScrollUp;
40 SkM44 globalTransform = [reference node].transform;
42 globalTransform = parent.node.transform * globalTransform;
44 return globalTransform;
48 SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
49 return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
54 SkPoint point = SkPoint::Make(local_point.x, local_point.y);
59 UIScreen* screen = reference.
bridge->view().window.screen;
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];
70 SkPoint::Make(local_rect.origin.x, local_rect.origin.y),
71 SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y),
72 SkPoint::Make(local_rect.origin.x + local_rect.size.width,
73 local_rect.origin.y + local_rect.size.height),
74 SkPoint::Make(local_rect.origin.x,
75 local_rect.origin.y + local_rect.size.height)
77 for (
auto& point : quad) {
81 NSCAssert(rect.setBoundsCheck(quad, 4),
@"Transformed points can't form a rect");
82 rect.setBounds(quad, 4);
87 UIScreen* screen = reference.
bridge->view().window.screen;
89 CGFloat scale = (screen ?: UIScreen.mainScreen).scale;
91 CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
92 return UIAccessibilityConvertFrameToScreenCoordinates(result, reference.
bridge->view());
98 @property(nonatomic, retain, readonly) UISwitch* nativeSwitch;
103 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
105 self = [
super initWithBridge:bridge uid:uid];
107 _nativeSwitch = [[UISwitch alloc] init];
112 - (NSMethodSignature*)methodSignatureForSelector:(
SEL)sel {
113 NSMethodSignature* result = [
super methodSignatureForSelector:sel];
115 result = [
self.nativeSwitch methodSignatureForSelector:sel];
120 - (void)forwardInvocation:(NSInvocation*)anInvocation {
121 anInvocation.target =
self.nativeSwitch;
122 [anInvocation invoke];
125 - (NSString*)accessibilityValue {
126 self.nativeSwitch.on =
self.node.HasFlag(flutter::SemanticsFlags::kIsToggled) ||
127 self.node.HasFlag(flutter::SemanticsFlags::kIsChecked);
132 return self.nativeSwitch.accessibilityValue;
136 - (UIAccessibilityTraits)accessibilityTraits {
137 self.nativeSwitch.enabled =
self.node.HasFlag(flutter::SemanticsFlags::kIsEnabled);
139 return self.nativeSwitch.accessibilityTraits;
150 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
152 self = [
super initWithBridge:bridge uid:uid];
155 [_scrollView setShowsHorizontalScrollIndicator:NO];
156 [_scrollView setShowsVerticalScrollIndicator:NO];
157 [_scrollView setContentInset:UIEdgeInsetsZero];
158 [_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
159 [
self.bridge->view() addSubview:_scrollView];
165 [_scrollView removeFromSuperview];
177 self.scrollView.frame =
self.accessibilityFrame;
178 self.scrollView.contentSize = [
self contentSizeInternal];
181 [
self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
186 return self.scrollView;
191 - (float)scrollExtentMax {
195 float scrollExtentMax =
self.node.scrollExtentMax;
196 if (isnan(scrollExtentMax)) {
197 scrollExtentMax = 0.0f;
198 }
else if (!isfinite(scrollExtentMax)) {
201 return scrollExtentMax;
204 - (float)scrollPosition {
208 float scrollPosition =
self.node.scrollPosition;
209 if (isnan(scrollPosition)) {
210 scrollPosition = 0.0f;
212 NSCAssert(isfinite(scrollPosition),
@"The scrollPosition must not be infinity");
213 return scrollPosition;
216 - (CGSize)contentSizeInternal {
218 const SkRect& rect =
self.node.rect;
220 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
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());
225 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
230 - (CGPoint)contentOffsetInternal {
232 CGPoint origin =
self.scrollView.frame.origin;
233 const SkRect& rect =
self.node.rect;
234 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
236 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
241 return CGPointMake(result.x - origin.x, result.y - origin.y);
259 NSMutableArray<SemanticsObject*>* _children;
263 #pragma mark - Designated initializers
265 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
267 FML_DCHECK(bridge) <<
"bridge must be set";
273 self = [
super initWithAccessibilityContainer:bridge->view()];
278 _children = [[NSMutableArray alloc] init];
279 _childrenInHitTestOrder = [[NSArray alloc] init];
295 [_children removeAllObjects];
301 #pragma mark - Semantic object property accesser
307 _children = [children mutableCopy];
313 - (void)setChildrenInHitTestOrder:(NSArray<
SemanticsObject*>*)childrenInHitTestOrder {
317 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
323 - (BOOL)hasChildren {
324 return [
self.children count] != 0;
327 #pragma mark - Semantic object method
329 - (BOOL)isAccessibilityBridgeAlive {
330 return self.
bridge.get() != nil;
333 - (void)setSemanticsNode:(const
flutter::SemanticsNode*)node {
337 - (void)accessibilityBridgeDidFinishUpdate {
343 - (BOOL)nodeWillCauseLayoutChange:(const
flutter::SemanticsNode*)node {
344 return self.node.rect != node->rect ||
self.node.transform != node->transform;
350 - (BOOL)nodeWillCauseScroll:(const
flutter::SemanticsNode*)node {
351 return !isnan(
self.node.scrollPosition) && !isnan(node->scrollPosition) &&
352 self.node.scrollPosition != node->scrollPosition;
359 - (BOOL)nodeShouldTriggerAnnouncement:(const
flutter::SemanticsNode*)node {
361 if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
366 if (!
self.node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
371 return self.node.label != node->label;
378 [_children replaceObjectAtIndex:index withObject:child];
381 - (NSString*)routeName {
384 if (
self.node.HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
385 NSString* newName =
self.accessibilityLabel;
386 if (newName != nil && [newName length] > 0) {
390 if ([
self hasChildren]) {
392 NSString* newName = [child routeName];
393 if (newName != nil && [newName length] > 0) {
401 - (id)nativeAccessibility {
405 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
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) {
413 case flutter::StringAttributeType::kLocale: {
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()),
419 [attributedString setAttributes:attributeDict range:range];
422 case flutter::StringAttributeType::kSpellOut: {
423 if (@available(iOS 13.0, *)) {
424 NSDictionary* attributeDict = @{
425 UIAccessibilitySpeechAttributeSpellOut : @YES,
427 [attributedString setAttributes:attributeDict range:range];
433 return attributedString;
436 - (void)showOnScreen {
437 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kShowOnScreen);
440 #pragma mark - UIAccessibility overrides
442 - (BOOL)isAccessibilityElement {
443 if (![
self isAccessibilityBridgeAlive]) {
452 if (
self.node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
456 return [
self isFocusable];
459 - (bool)isFocusable {
468 return ((
self.node.flags & flutter::kScrollableSemanticsFlags) != 0 &&
469 (
self.node.flags &
static_cast<int32_t
>(flutter::SemanticsFlags::kIsHidden)) != 0) ||
470 !
self.node.label.empty() || !
self.node.value.empty() || !
self.node.hint.empty() ||
471 (
self.node.actions & ~
flutter::kScrollableSemanticsActions) != 0;
475 if (
self.node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
476 [edges addObject:self];
478 if ([
self hasChildren]) {
480 [child collectRoutes:edges];
486 if (!
self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
489 int32_t action_id = action.
uid;
490 std::vector<uint8_t> args;
492 args.push_back(action_id);
493 args.push_back(action_id >> 8);
494 args.push_back(action_id >> 16);
495 args.push_back(action_id >> 24);
496 self.bridge->DispatchSemanticsAction(
497 self.uid, flutter::SemanticsAction::kCustomAction,
498 fml::MallocMapping::Copy(args.data(), args.size() *
sizeof(uint8_t)));
502 - (NSString*)accessibilityIdentifier {
503 if (![
self isAccessibilityBridgeAlive]) {
507 if (
self.node.identifier.empty()) {
510 return @(
self.node.identifier.data());
513 - (NSString*)accessibilityLabel {
514 if (![
self isAccessibilityBridgeAlive]) {
517 NSString* label = nil;
518 if (!
self.node.label.empty()) {
519 label = @(
self.node.label.data());
521 if (!
self.node.tooltip.empty()) {
522 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
523 : @(
self.node.tooltip.data());
528 - (bool)containsPoint:(CGPoint)point {
530 return CGRectContainsPoint([
self globalRect], point);
534 - (id)search:(CGPoint)point {
537 if ([child containsPoint:point]) {
538 id childSearchResult = [child search:point];
539 if (childSearchResult != nil) {
540 return childSearchResult;
545 if ([
self containsPoint:point] && [
self isFocusable]) {
546 return self.nativeAccessibility;
558 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
559 return [
self search:point];
563 - (BOOL)accessibilityScrollToVisible {
569 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
571 [child showOnScreen];
577 - (NSAttributedString*)accessibilityAttributedLabel {
578 NSString* label =
self.accessibilityLabel;
579 if (label.length == 0) {
582 return [
self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
585 - (NSString*)accessibilityHint {
586 if (![
self isAccessibilityBridgeAlive]) {
590 if (
self.node.hint.empty()) {
593 return @(
self.node.hint.data());
596 - (NSAttributedString*)accessibilityAttributedHint {
597 NSString* hint = [
self accessibilityHint];
598 if (hint.length == 0) {
601 return [
self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
604 - (NSString*)accessibilityValue {
605 if (![
self isAccessibilityBridgeAlive]) {
609 if (!
self.node.value.empty()) {
610 return @(
self.node.value.data());
614 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
619 if (
self.node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
620 self.node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
621 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsToggled) ||
622 self.node.HasFlag(flutter::SemanticsFlags::kIsChecked)) {
632 - (NSAttributedString*)accessibilityAttributedValue {
633 NSString* value = [
self accessibilityValue];
634 if (value.length == 0) {
637 return [
self createAttributedStringFromString:value withAttributes:self.node.valueAttributes];
640 - (CGRect)accessibilityFrame {
641 if (![
self isAccessibilityBridgeAlive]) {
642 return CGRectMake(0, 0, 0, 0);
645 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsHidden)) {
646 return [
super accessibilityFrame];
648 return [
self globalRect];
651 - (CGRect)globalRect {
652 const SkRect& rect =
self.node.rect;
653 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
657 #pragma mark - UIAccessibilityElement protocol
659 - (void)setAccessibilityContainer:(
id)container {
664 - (id)accessibilityContainer {
673 if (![
self isAccessibilityBridgeAlive]) {
677 if ([
self hasChildren] ||
self.uid ==
kRootNodeId) {
678 if (
self.container == nil) {
682 return self.container;
684 if (
self.parent == nil) {
690 return self.parent.accessibilityContainer;
693 #pragma mark - UIAccessibilityAction overrides
695 - (BOOL)accessibilityActivate {
696 if (![
self isAccessibilityBridgeAlive]) {
699 if (!
self.node.HasAction(flutter::SemanticsAction::kTap)) {
704 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsSlider)) {
709 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kTap);
713 - (void)accessibilityIncrement {
714 if (![
self isAccessibilityBridgeAlive]) {
717 if (
self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
718 self.node.value =
self.node.increasedValue;
719 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kIncrease);
723 - (void)accessibilityDecrement {
724 if (![
self isAccessibilityBridgeAlive]) {
727 if (
self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
728 self.node.value =
self.node.decreasedValue;
729 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDecrease);
733 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
734 if (![
self isAccessibilityBridgeAlive]) {
738 if (!
self.node.HasAction(action)) {
741 self.bridge->DispatchSemanticsAction(
self.uid, action);
745 - (BOOL)accessibilityPerformEscape {
746 if (![
self isAccessibilityBridgeAlive]) {
749 if (!
self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
752 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDismiss);
756 #pragma mark UIAccessibilityFocus overrides
758 - (void)accessibilityElementDidBecomeFocused {
759 if (![
self isAccessibilityBridgeAlive]) {
762 self.bridge->AccessibilityObjectDidBecomeFocused(
self.uid);
763 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsHidden) ||
764 self.node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
767 if (
self.node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
768 self.bridge->DispatchSemanticsAction(
self.uid,
769 flutter::SemanticsAction::kDidGainAccessibilityFocus);
773 - (void)accessibilityElementDidLoseFocus {
774 if (![
self isAccessibilityBridgeAlive]) {
777 self.bridge->AccessibilityObjectDidLoseFocus(
self.uid);
778 if (
self.node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
779 self.bridge->DispatchSemanticsAction(
self.uid,
780 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
788 #pragma mark - Designated initializers
790 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
792 self = [
super initWithBridge:bridge uid:uid];
796 #pragma mark - UIAccessibility overrides
798 - (UIAccessibilityTraits)accessibilityTraits {
799 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
800 if (
self.
node.HasAction(flutter::SemanticsAction::kIncrease) ||
801 self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
802 traits |= UIAccessibilityTraitAdjustable;
805 if (
self.
node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
806 self.node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
807 traits |= UIAccessibilityTraitButton;
809 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsSelected)) {
810 traits |= UIAccessibilityTraitSelected;
812 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsButton)) {
813 traits |= UIAccessibilityTraitButton;
815 if (
self.
node.HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
816 !
self.node.HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
817 traits |= UIAccessibilityTraitNotEnabled;
819 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
820 traits |= UIAccessibilityTraitHeader;
822 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsImage)) {
823 traits |= UIAccessibilityTraitImage;
825 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
826 traits |= UIAccessibilityTraitUpdatesFrequently;
828 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsLink)) {
829 traits |= UIAccessibilityTraitLink;
831 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
832 self.accessibilityLabel.length != 0 &&
833 !
self.node.HasFlag(flutter::SemanticsFlags::kIsTextField)) {
834 traits = UIAccessibilityTraitStaticText;
842 @property(nonatomic, weak) UIView* platformView;
847 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
851 _platformView = platformView;
852 [platformView setFlutterAccessibilityContainer:self];
858 return self.platformView;
864 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
867 #pragma mark - initializers
870 bridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
871 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
876 self = [
super initWithAccessibilityContainer:bridge->view()];
879 _semanticsObject = semanticsObject;
886 #pragma mark - UIAccessibilityContainer overrides
888 - (NSInteger)accessibilityElementCount {
889 return self.semanticsObject.
children.count + 1;
892 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
893 if (index < 0 || index >= [
self accessibilityElementCount]) {
897 return self.semanticsObject.nativeAccessibility;
902 if ([child hasChildren]) {
903 return child.accessibilityContainer;
908 - (NSInteger)indexOfAccessibilityElement:(
id)element {
909 if (element ==
self.semanticsObject.nativeAccessibility) {
913 NSArray<SemanticsObject*>* children =
self.semanticsObject.children;
914 for (
size_t i = 0; i < [children count]; i++) {
924 #pragma mark - UIAccessibilityElement protocol
926 - (BOOL)isAccessibilityElement {
930 - (CGRect)accessibilityFrame {
931 return self.semanticsObject.accessibilityFrame;
934 - (id)accessibilityContainer {
940 :
self.semanticsObject.
parent.accessibilityContainer;
943 #pragma mark - UIAccessibilityAction overrides
945 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
946 return [
self.semanticsObject accessibilityScroll:direction];