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.flags.isToggled ||
self.node.flags.isChecked;
131 return self.nativeSwitch.accessibilityValue;
135 - (UIAccessibilityTraits)accessibilityTraits {
136 self.nativeSwitch.enabled =
self.node.flags.isEnabled;
138 return self.nativeSwitch.accessibilityTraits;
149 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
151 self = [
super initWithBridge:bridge uid:uid];
154 [_scrollView setShowsHorizontalScrollIndicator:NO];
155 [_scrollView setShowsVerticalScrollIndicator:NO];
156 [_scrollView setContentInset:UIEdgeInsetsZero];
157 [_scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
158 [
self.bridge->view() addSubview:_scrollView];
164 [_scrollView removeFromSuperview];
176 self.scrollView.frame =
self.accessibilityFrame;
177 self.scrollView.contentSize = [
self contentSizeInternal];
180 [
self.scrollView setContentOffset:self.contentOffsetInternal animated:NO];
185 return self.scrollView;
190 - (float)scrollExtentMax {
194 float scrollExtentMax =
self.node.scrollExtentMax;
195 if (isnan(scrollExtentMax)) {
196 scrollExtentMax = 0.0f;
197 }
else if (!isfinite(scrollExtentMax)) {
200 return scrollExtentMax;
203 - (float)scrollPosition {
207 float scrollPosition =
self.node.scrollPosition;
208 if (isnan(scrollPosition)) {
209 scrollPosition = 0.0f;
211 NSCAssert(isfinite(scrollPosition),
@"The scrollPosition must not be infinity");
212 return scrollPosition;
215 - (CGSize)contentSizeInternal {
217 const SkRect& rect =
self.node.rect;
219 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
220 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [
self scrollExtentMax]);
221 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
222 result = CGRectMake(rect.x(), rect.y(), rect.width() + [
self scrollExtentMax], rect.height());
224 result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
229 - (CGPoint)contentOffsetInternal {
231 CGPoint origin =
self.scrollView.frame.origin;
232 const SkRect& rect =
self.node.rect;
233 if (
self.
node.actions & flutter::kVerticalScrollSemanticsActions) {
235 }
else if (
self.
node.actions & flutter::kHorizontalScrollSemanticsActions) {
240 return CGPointMake(result.x - origin.x, result.y - origin.y);
258 NSMutableArray<SemanticsObject*>* _children;
262 #pragma mark - Designated initializers
264 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
266 FML_DCHECK(bridge) <<
"bridge must be set";
272 self = [
super initWithAccessibilityContainer:bridge->view()];
277 _children = [[NSMutableArray alloc] init];
278 _childrenInHitTestOrder = [[NSArray alloc] init];
294 [_children removeAllObjects];
300 #pragma mark - Semantic object property accesser
306 _children = [children mutableCopy];
312 - (void)setChildrenInHitTestOrder:(NSArray<
SemanticsObject*>*)childrenInHitTestOrder {
316 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
322 - (BOOL)hasChildren {
323 return [
self.children count] != 0;
326 #pragma mark - Semantic object method
328 - (BOOL)isAccessibilityBridgeAlive {
329 return self.
bridge.get() != nil;
332 - (void)setSemanticsNode:(const
flutter::SemanticsNode*)node {
336 - (void)accessibilityBridgeDidFinishUpdate {
342 - (BOOL)nodeWillCauseLayoutChange:(const
flutter::SemanticsNode*)node {
343 return self.node.rect != node->rect ||
self.node.transform != node->transform;
349 - (BOOL)nodeWillCauseScroll:(const
flutter::SemanticsNode*)node {
350 return !isnan(
self.node.scrollPosition) && !isnan(node->scrollPosition) &&
351 self.node.scrollPosition != node->scrollPosition;
358 - (BOOL)nodeShouldTriggerAnnouncement:(const
flutter::SemanticsNode*)node {
360 if (!node || !node->flags.isLiveRegion) {
365 if (!
self.node.flags.isLiveRegion) {
370 return self.node.label != node->label;
373 - (void)replaceChildAtIndex:(NSInteger)index withChild:(
SemanticsObject*)child {
377 [_children replaceObjectAtIndex:index withObject:child];
380 - (NSString*)routeName {
383 if (
self.node.flags.namesRoute) {
384 NSString* newName =
self.accessibilityLabel;
385 if (newName != nil && [newName length] > 0) {
389 if ([
self hasChildren]) {
391 NSString* newName = [child routeName];
392 if (newName != nil && [newName length] > 0) {
400 - (id)nativeAccessibility {
404 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
406 (const
flutter::StringAttributes&)attributes {
407 NSMutableAttributedString* attributedString =
408 [[NSMutableAttributedString alloc] initWithString:string];
409 for (
const auto& attribute : attributes) {
410 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
411 switch (attribute->type) {
412 case flutter::StringAttributeType::kLocale: {
413 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
414 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
415 NSDictionary* attributeDict = @{
416 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
418 [attributedString setAttributes:attributeDict range:range];
421 case flutter::StringAttributeType::kSpellOut: {
422 NSDictionary* attributeDict = @{
423 UIAccessibilitySpeechAttributeSpellOut : @YES,
425 [attributedString setAttributes:attributeDict range:range];
430 return attributedString;
433 - (void)showOnScreen {
434 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kShowOnScreen);
437 #pragma mark - UIAccessibility overrides
439 - (BOOL)isAccessibilityElement {
440 if (![
self isAccessibilityBridgeAlive]) {
449 if (
self.node.flags.scopesRoute) {
453 return [
self isFocusable];
456 - (bool)isFocusable {
465 return (
self.node.flags.hasImplicitScrolling &&
self.node.flags.isHidden)
467 || !
self.node.label.empty() || !
self.node.value.empty() || !
self.node.hint.empty() ||
468 (
self.node.actions & ~
flutter::kScrollableSemanticsActions) != 0;
472 if (
self.node.flags.scopesRoute) {
473 [edges addObject:self];
475 if ([
self hasChildren]) {
477 [child collectRoutes:edges];
483 if (!
self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
486 int32_t action_id = action.
uid;
487 std::vector<uint8_t> args;
489 args.push_back(action_id);
490 args.push_back(action_id >> 8);
491 args.push_back(action_id >> 16);
492 args.push_back(action_id >> 24);
493 self.bridge->DispatchSemanticsAction(
494 self.uid, flutter::SemanticsAction::kCustomAction,
495 fml::MallocMapping::Copy(args.data(), args.size() *
sizeof(uint8_t)));
499 - (NSString*)accessibilityIdentifier {
500 if (![
self isAccessibilityBridgeAlive]) {
504 if (
self.node.identifier.empty()) {
507 return @(
self.node.identifier.data());
510 - (NSString*)accessibilityLabel {
511 if (![
self isAccessibilityBridgeAlive]) {
514 NSString* label = nil;
515 if (!
self.node.label.empty()) {
516 label = @(
self.node.label.data());
518 if (!
self.node.tooltip.empty()) {
519 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
520 : @(
self.node.tooltip.data());
525 - (bool)containsPoint:(CGPoint)point {
527 return CGRectContainsPoint([
self globalRect], point);
531 - (id)search:(CGPoint)point {
534 if ([child containsPoint:point]) {
535 id childSearchResult = [child search:point];
536 if (childSearchResult != nil) {
537 return childSearchResult;
542 if ([
self containsPoint:point] && [
self isFocusable]) {
543 return self.nativeAccessibility;
555 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
556 return [
self search:point];
560 - (BOOL)accessibilityScrollToVisible {
566 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
568 [child showOnScreen];
574 - (NSAttributedString*)accessibilityAttributedLabel {
575 NSString* label =
self.accessibilityLabel;
576 if (label.length == 0) {
579 return [
self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
582 - (NSString*)accessibilityHint {
583 if (![
self isAccessibilityBridgeAlive]) {
587 if (
self.node.hint.empty()) {
590 return @(
self.node.hint.data());
593 - (NSAttributedString*)accessibilityAttributedHint {
594 NSString* hint = [
self accessibilityHint];
595 if (hint.length == 0) {
598 return [
self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
601 - (NSString*)accessibilityValue {
602 if (![
self isAccessibilityBridgeAlive]) {
606 if (!
self.node.value.empty()) {
607 return @(
self.node.value.data());
611 if (
self.node.flags.isInMutuallyExclusiveGroup) {
616 if (
self.node.flags.hasToggledState ||
self.node.flags.hasCheckedState) {
617 if (
self.node.flags.isToggled ||
self.node.flags.isChecked) {
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.flags.isHidden) {
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)) {
699 if (
self.node.flags.isSlider) {
704 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kTap);
708 - (void)accessibilityIncrement {
709 if (![
self isAccessibilityBridgeAlive]) {
712 if (
self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
713 self.node.value =
self.node.increasedValue;
714 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kIncrease);
718 - (void)accessibilityDecrement {
719 if (![
self isAccessibilityBridgeAlive]) {
722 if (
self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
723 self.node.value =
self.node.decreasedValue;
724 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDecrease);
728 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
729 if (![
self isAccessibilityBridgeAlive]) {
733 if (!
self.node.HasAction(action)) {
736 self.bridge->DispatchSemanticsAction(
self.uid, action);
740 - (BOOL)accessibilityPerformEscape {
741 if (![
self isAccessibilityBridgeAlive]) {
744 if (!
self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
747 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDismiss);
751 #pragma mark UIAccessibilityFocus overrides
753 - (void)accessibilityElementDidBecomeFocused {
754 if (![
self isAccessibilityBridgeAlive]) {
757 self.bridge->AccessibilityObjectDidBecomeFocused(
self.uid);
758 if (
self.node.flags.isHidden ||
self.node.flags.isHeader) {
761 if (
self.node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
762 self.bridge->DispatchSemanticsAction(
self.uid,
763 flutter::SemanticsAction::kDidGainAccessibilityFocus);
767 - (void)accessibilityElementDidLoseFocus {
768 if (![
self isAccessibilityBridgeAlive]) {
771 self.bridge->AccessibilityObjectDidLoseFocus(
self.uid);
772 if (
self.node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
773 self.bridge->DispatchSemanticsAction(
self.uid,
774 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
778 - (BOOL)accessibilityRespondsToUserInteraction {
780 if ((
self.node.actions & ~flutter::kSystemActions) != 0) {
784 if (!
self.node.customAccessibilityActions.empty()) {
795 #pragma mark - Designated initializers
797 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
799 self = [
super initWithBridge:bridge uid:uid];
803 #pragma mark - UIAccessibility overrides
805 - (UIAccessibilityTraits)accessibilityTraits {
806 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
807 if (
self.
node.HasAction(flutter::SemanticsAction::kIncrease) ||
808 self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
809 traits |= UIAccessibilityTraitAdjustable;
812 if (
self.
node.flags.hasToggledState ||
self.node.flags.hasCheckedState) {
813 traits |= UIAccessibilityTraitButton;
815 if (
self.
node.flags.isSelected) {
816 traits |= UIAccessibilityTraitSelected;
818 if (
self.
node.flags.isButton) {
819 traits |= UIAccessibilityTraitButton;
821 if (
self.
node.flags.hasEnabledState && !
self.node.flags.isEnabled) {
822 traits |= UIAccessibilityTraitNotEnabled;
824 if (
self.
node.flags.isHeader) {
825 traits |= UIAccessibilityTraitHeader;
827 if (
self.
node.flags.isImage) {
828 traits |= UIAccessibilityTraitImage;
830 if (
self.
node.flags.isLiveRegion) {
831 traits |= UIAccessibilityTraitUpdatesFrequently;
833 if (
self.
node.flags.isLink) {
834 traits |= UIAccessibilityTraitLink;
836 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
837 self.accessibilityLabel.length != 0 && !
self.node.flags.isTextField) {
838 traits = UIAccessibilityTraitStaticText;
846 @property(nonatomic, weak) UIView* platformView;
851 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
855 _platformView = platformView;
856 [platformView setFlutterAccessibilityContainer:self];
862 return self.platformView;
868 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
871 #pragma mark - initializers
873 - (instancetype)initWithSemanticsObject:(
SemanticsObject*)semanticsObject
874 bridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
875 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
880 self = [
super initWithAccessibilityContainer:bridge->view()];
883 _semanticsObject = semanticsObject;
890 #pragma mark - UIAccessibilityContainer overrides
892 - (NSInteger)accessibilityElementCount {
893 return self.semanticsObject.
children.count + 1;
896 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
897 if (index < 0 || index >= [
self accessibilityElementCount]) {
901 return self.semanticsObject.nativeAccessibility;
906 if ([child hasChildren]) {
907 return child.accessibilityContainer;
912 - (NSInteger)indexOfAccessibilityElement:(
id)element {
913 if (element ==
self.semanticsObject.nativeAccessibility) {
917 NSArray<SemanticsObject*>* children =
self.semanticsObject.children;
918 for (
size_t i = 0; i < [children count]; i++) {
928 #pragma mark - UIAccessibilityElement protocol
930 - (BOOL)isAccessibilityElement {
934 - (CGRect)accessibilityFrame {
940 return UIScreen.mainScreen.bounds;
943 - (id)accessibilityContainer {
949 :
self.semanticsObject.
parent.accessibilityContainer;
952 #pragma mark - UIAccessibilityAction overrides
954 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
955 return [
self.semanticsObject accessibilityScroll:direction];
constexpr int32_t kRootNodeId
constexpr float kScrollExtentMaxForInf
BOOL isAccessibilityBridgeAlive()
void accessibilityBridgeDidFinishUpdate()
flutter::SemanticsNode node
NSArray< SemanticsObject * > * children
fml::WeakPtr< flutter::AccessibilityBridgeIos > 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)