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 [
self.bridge->view() addSubview:_scrollView];
163 [_scrollView removeFromSuperview];
175 self.scrollView.frame =
self.accessibilityFrame;
176 self.scrollView.contentSize = [
self contentSizeInternal];
177 [
self.scrollView setContentOffset:[
self contentOffsetInternal] animated:NO];
181 return self.scrollView;
186 - (float)scrollExtentMax {
190 float scrollExtentMax =
self.node.scrollExtentMax;
191 if (isnan(scrollExtentMax)) {
192 scrollExtentMax = 0.0f;
193 }
else if (!isfinite(scrollExtentMax)) {
196 return scrollExtentMax;
199 - (float)scrollPosition {
203 float scrollPosition =
self.node.scrollPosition;
204 if (isnan(scrollPosition)) {
205 scrollPosition = 0.0f;
207 NSCAssert(isfinite(scrollPosition),
@"The scrollPosition must not be infinity");
208 return scrollPosition;
211 - (CGSize)contentSizeInternal {
213 const SkRect& rect =
self.node.rect;
215 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
216 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [
self scrollExtentMax]);
217 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
218 result = CGRectMake(rect.x(), rect.y(), rect.width() + [
self scrollExtentMax], rect.height());
220 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
225 - (CGPoint)contentOffsetInternal {
227 CGPoint origin =
self.scrollView.frame.origin;
228 const SkRect& rect =
self.node.rect;
229 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
231 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
236 return CGPointMake(result.x - origin.x, result.y - origin.y);
254 NSMutableArray<SemanticsObject*>* _children;
258 #pragma mark - Designated initializers
260 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
262 FML_DCHECK(bridge) <<
"bridge must be set";
268 self = [
super initWithAccessibilityContainer:bridge->view()];
273 _children = [[NSMutableArray alloc] init];
274 _childrenInHitTestOrder = [[NSArray alloc] init];
290 [_children removeAllObjects];
296 #pragma mark - Semantic object property accesser
302 _children = [children mutableCopy];
308 - (void)setChildrenInHitTestOrder:(NSArray<
SemanticsObject*>*)childrenInHitTestOrder {
312 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
318 - (BOOL)hasChildren {
319 return [
self.children count] != 0;
322 #pragma mark - Semantic object method
324 - (BOOL)isAccessibilityBridgeAlive {
325 return self.
bridge.get() != nil;
328 - (void)setSemanticsNode:(const
flutter::SemanticsNode*)node {
332 - (void)accessibilityBridgeDidFinishUpdate {
338 - (BOOL)nodeWillCauseLayoutChange:(const
flutter::SemanticsNode*)node {
339 return self.node.rect != node->rect ||
self.node.transform != node->transform;
345 - (BOOL)nodeWillCauseScroll:(const
flutter::SemanticsNode*)node {
346 return !isnan(
self.node.scrollPosition) && !isnan(node->scrollPosition) &&
347 self.node.scrollPosition != node->scrollPosition;
354 - (BOOL)nodeShouldTriggerAnnouncement:(const
flutter::SemanticsNode*)node {
356 if (!node || !node->HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
361 if (!
self.node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
366 return self.node.label != node->label;
373 [_children replaceObjectAtIndex:index withObject:child];
376 - (NSString*)routeName {
379 if (
self.node.HasFlag(flutter::SemanticsFlags::kNamesRoute)) {
380 NSString* newName =
self.accessibilityLabel;
381 if (newName != nil && [newName length] > 0) {
385 if ([
self hasChildren]) {
387 NSString* newName = [child routeName];
388 if (newName != nil && [newName length] > 0) {
396 - (id)nativeAccessibility {
400 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
402 (const
flutter::StringAttributes&)attributes {
403 NSMutableAttributedString* attributedString =
404 [[NSMutableAttributedString alloc] initWithString:string];
405 for (
const auto& attribute : attributes) {
406 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
407 switch (attribute->type) {
408 case flutter::StringAttributeType::kLocale: {
409 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
410 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
411 NSDictionary* attributeDict = @{
412 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
414 [attributedString setAttributes:attributeDict range:range];
417 case flutter::StringAttributeType::kSpellOut: {
418 if (@available(iOS 13.0, *)) {
419 NSDictionary* attributeDict = @{
420 UIAccessibilitySpeechAttributeSpellOut : @YES,
422 [attributedString setAttributes:attributeDict range:range];
428 return attributedString;
431 - (void)showOnScreen {
432 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kShowOnScreen);
435 #pragma mark - UIAccessibility overrides
437 - (BOOL)isAccessibilityElement {
438 if (![
self isAccessibilityBridgeAlive]) {
447 if (
self.node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
451 return [
self isFocusable];
454 - (bool)isFocusable {
463 return ((
self.node.flags & flutter::kScrollableSemanticsFlags) != 0 &&
464 (
self.node.flags &
static_cast<int32_t
>(flutter::SemanticsFlags::kIsHidden)) != 0) ||
465 !
self.node.label.empty() || !
self.node.value.empty() || !
self.node.hint.empty() ||
466 (
self.node.actions & ~
flutter::kScrollableSemanticsActions) != 0;
470 if (
self.node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
471 [edges addObject:self];
473 if ([
self hasChildren]) {
475 [child collectRoutes:edges];
481 if (!
self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
484 int32_t action_id = action.
uid;
485 std::vector<uint8_t> args;
487 args.push_back(action_id);
488 args.push_back(action_id >> 8);
489 args.push_back(action_id >> 16);
490 args.push_back(action_id >> 24);
491 self.bridge->DispatchSemanticsAction(
492 self.uid, flutter::SemanticsAction::kCustomAction,
493 fml::MallocMapping::Copy(args.data(), args.size() *
sizeof(uint8_t)));
497 - (NSString*)accessibilityIdentifier {
498 if (![
self isAccessibilityBridgeAlive]) {
502 if (
self.node.identifier.empty()) {
505 return @(
self.node.identifier.data());
508 - (NSString*)accessibilityLabel {
509 if (![
self isAccessibilityBridgeAlive]) {
512 NSString* label = nil;
513 if (!
self.node.label.empty()) {
514 label = @(
self.node.label.data());
516 if (!
self.node.tooltip.empty()) {
517 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
518 : @(
self.node.tooltip.data());
523 - (bool)containsPoint:(CGPoint)point {
525 return CGRectContainsPoint([
self globalRect], point);
529 - (id)search:(CGPoint)point {
532 if ([child containsPoint:point]) {
533 id childSearchResult = [child search:point];
534 if (childSearchResult != nil) {
535 return childSearchResult;
540 if ([
self containsPoint:point] && [
self isFocusable]) {
541 return self.nativeAccessibility;
553 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
554 return [
self search:point];
558 - (BOOL)accessibilityScrollToVisible {
564 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
566 [child showOnScreen];
572 - (NSAttributedString*)accessibilityAttributedLabel {
573 NSString* label =
self.accessibilityLabel;
574 if (label.length == 0) {
577 return [
self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
580 - (NSString*)accessibilityHint {
581 if (![
self isAccessibilityBridgeAlive]) {
585 if (
self.node.hint.empty()) {
588 return @(
self.node.hint.data());
591 - (NSAttributedString*)accessibilityAttributedHint {
592 NSString* hint = [
self accessibilityHint];
593 if (hint.length == 0) {
596 return [
self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
599 - (NSString*)accessibilityValue {
600 if (![
self isAccessibilityBridgeAlive]) {
604 if (!
self.node.value.empty()) {
605 return @(
self.node.value.data());
609 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsInMutuallyExclusiveGroup)) {
614 if (
self.node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
615 self.node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
616 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsToggled) ||
617 self.node.HasFlag(flutter::SemanticsFlags::kIsChecked)) {
627 - (NSAttributedString*)accessibilityAttributedValue {
628 NSString* value = [
self accessibilityValue];
629 if (value.length == 0) {
632 return [
self createAttributedStringFromString:value withAttributes:self.node.valueAttributes];
635 - (CGRect)accessibilityFrame {
636 if (![
self isAccessibilityBridgeAlive]) {
637 return CGRectMake(0, 0, 0, 0);
640 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsHidden)) {
641 return [
super accessibilityFrame];
643 return [
self globalRect];
646 - (CGRect)globalRect {
647 const SkRect& rect =
self.node.rect;
648 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
652 #pragma mark - UIAccessibilityElement protocol
654 - (void)setAccessibilityContainer:(
id)container {
659 - (id)accessibilityContainer {
668 if (![
self isAccessibilityBridgeAlive]) {
672 if ([
self hasChildren] ||
self.uid ==
kRootNodeId) {
673 if (
self.container == nil) {
677 return self.container;
679 if (
self.parent == nil) {
685 return self.parent.accessibilityContainer;
688 #pragma mark - UIAccessibilityAction overrides
690 - (BOOL)accessibilityActivate {
691 if (![
self isAccessibilityBridgeAlive]) {
694 if (!
self.node.HasAction(flutter::SemanticsAction::kTap)) {
697 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kTap);
701 - (void)accessibilityIncrement {
702 if (![
self isAccessibilityBridgeAlive]) {
705 if (
self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
706 self.node.value =
self.node.increasedValue;
707 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kIncrease);
711 - (void)accessibilityDecrement {
712 if (![
self isAccessibilityBridgeAlive]) {
715 if (
self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
716 self.node.value =
self.node.decreasedValue;
717 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDecrease);
721 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
722 if (![
self isAccessibilityBridgeAlive]) {
726 if (!
self.node.HasAction(action)) {
729 self.bridge->DispatchSemanticsAction(
self.uid, action);
733 - (BOOL)accessibilityPerformEscape {
734 if (![
self isAccessibilityBridgeAlive]) {
737 if (!
self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
740 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDismiss);
744 #pragma mark UIAccessibilityFocus overrides
746 - (void)accessibilityElementDidBecomeFocused {
747 if (![
self isAccessibilityBridgeAlive]) {
750 self.bridge->AccessibilityObjectDidBecomeFocused(
self.uid);
751 if (
self.node.HasFlag(flutter::SemanticsFlags::kIsHidden) ||
752 self.node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
755 if (
self.node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
756 self.bridge->DispatchSemanticsAction(
self.uid,
757 flutter::SemanticsAction::kDidGainAccessibilityFocus);
761 - (void)accessibilityElementDidLoseFocus {
762 if (![
self isAccessibilityBridgeAlive]) {
765 self.bridge->AccessibilityObjectDidLoseFocus(
self.uid);
766 if (
self.node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
767 self.bridge->DispatchSemanticsAction(
self.uid,
768 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
776 #pragma mark - Designated initializers
778 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
780 self = [
super initWithBridge:bridge uid:uid];
784 #pragma mark - UIAccessibility overrides
786 - (UIAccessibilityTraits)accessibilityTraits {
787 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
788 if (
self.
node.HasAction(flutter::SemanticsAction::kIncrease) ||
789 self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
790 traits |= UIAccessibilityTraitAdjustable;
793 if (
self.
node.HasFlag(flutter::SemanticsFlags::kHasToggledState) ||
794 self.node.HasFlag(flutter::SemanticsFlags::kHasCheckedState)) {
795 traits |= UIAccessibilityTraitButton;
797 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsSelected)) {
798 traits |= UIAccessibilityTraitSelected;
800 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsButton)) {
801 traits |= UIAccessibilityTraitButton;
803 if (
self.
node.HasFlag(flutter::SemanticsFlags::kHasEnabledState) &&
804 !
self.node.HasFlag(flutter::SemanticsFlags::kIsEnabled)) {
805 traits |= UIAccessibilityTraitNotEnabled;
807 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsHeader)) {
808 traits |= UIAccessibilityTraitHeader;
810 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsImage)) {
811 traits |= UIAccessibilityTraitImage;
813 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsLiveRegion)) {
814 traits |= UIAccessibilityTraitUpdatesFrequently;
816 if (
self.
node.HasFlag(flutter::SemanticsFlags::kIsLink)) {
817 traits |= UIAccessibilityTraitLink;
819 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
820 self.accessibilityLabel.length != 0 &&
821 !
self.node.HasFlag(flutter::SemanticsFlags::kIsTextField)) {
822 traits = UIAccessibilityTraitStaticText;
830 @property(nonatomic, weak) UIView* platformView;
835 - (instancetype)initWithBridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
839 _platformView = platformView;
840 [platformView setFlutterAccessibilityContainer:self];
846 return self.platformView;
852 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
855 #pragma mark - initializers
858 bridge:(fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
859 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
864 self = [
super initWithAccessibilityContainer:bridge->view()];
867 _semanticsObject = semanticsObject;
874 #pragma mark - UIAccessibilityContainer overrides
876 - (NSInteger)accessibilityElementCount {
877 return self.semanticsObject.
children.count + 1;
880 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
881 if (index < 0 || index >= [
self accessibilityElementCount]) {
885 return self.semanticsObject.nativeAccessibility;
890 if ([child hasChildren]) {
891 return child.accessibilityContainer;
896 - (NSInteger)indexOfAccessibilityElement:(
id)element {
897 if (element ==
self.semanticsObject.nativeAccessibility) {
901 NSArray<SemanticsObject*>* children =
self.semanticsObject.children;
902 for (
size_t i = 0; i < [children count]; i++) {
912 #pragma mark - UIAccessibilityElement protocol
914 - (BOOL)isAccessibilityElement {
918 - (CGRect)accessibilityFrame {
919 return self.semanticsObject.accessibilityFrame;
922 - (id)accessibilityContainer {
928 :
self.semanticsObject.
parent.accessibilityContainer;
931 #pragma mark - UIAccessibilityAction overrides
933 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
934 return [
self.semanticsObject accessibilityScroll:direction];