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 == flutter::SemanticsTristate::kTrue ||
127 self.node.flags.isChecked == flutter::SemanticsCheckState::kTrue;
132 return self.nativeSwitch.accessibilityValue;
136 - (UIAccessibilityTraits)accessibilityTraits {
137 self.nativeSwitch.enabled =
self.node.flags.isEnabled == flutter::SemanticsTristate::kTrue;
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];
293 if (child.parent ==
self) {
297 [_children removeAllObjects];
303 #pragma mark - Semantic object property accesser
307 if (child.parent ==
self) {
311 _children = [children mutableCopy];
317 - (void)setChildrenInHitTestOrder:(NSArray<
SemanticsObject*>*)childrenInHitTestOrder {
318 _childrenInHitTestOrder = [childrenInHitTestOrder copy];
321 - (BOOL)hasChildren {
322 return [
self.children count] != 0;
325 #pragma mark - Semantic object method
327 - (BOOL)isAccessibilityBridgeAlive {
328 return self.
bridge.get() != nil;
331 - (void)setSemanticsNode:(const
flutter::SemanticsNode*)node {
335 - (void)accessibilityBridgeDidFinishUpdate {
341 - (BOOL)nodeWillCauseLayoutChange:(const
flutter::SemanticsNode*)node {
342 return self.node.rect != node->rect ||
self.node.transform != node->transform;
348 - (BOOL)nodeWillCauseScroll:(const
flutter::SemanticsNode*)node {
349 return !isnan(
self.node.scrollPosition) && !isnan(node->scrollPosition) &&
350 self.node.scrollPosition != node->scrollPosition;
357 - (BOOL)nodeShouldTriggerAnnouncement:(const
flutter::SemanticsNode*)node {
359 if (!node || !node->flags.isLiveRegion) {
364 if (!
self.node.flags.isLiveRegion) {
369 return self.node.label != node->label;
372 - (void)replaceChildAtIndex:(NSInteger)index withChild:(
SemanticsObject*)child {
376 [_children replaceObjectAtIndex:index withObject:child];
379 - (NSString*)routeName {
382 if (
self.node.flags.namesRoute) {
383 NSString* newName =
self.accessibilityLabel;
384 if (newName != nil && [newName length] > 0) {
388 if ([
self hasChildren]) {
390 NSString* newName = [child routeName];
391 if (newName != nil && [newName length] > 0) {
399 - (id)nativeAccessibility {
403 - (NSAttributedString*)createAttributedStringFromString:(NSString*)string
405 (const
flutter::StringAttributes&)attributes {
406 NSMutableAttributedString* attributedString =
407 [[NSMutableAttributedString alloc] initWithString:string];
408 for (
const auto& attribute : attributes) {
409 NSRange range = NSMakeRange(attribute->start, attribute->end - attribute->start);
410 switch (attribute->type) {
411 case flutter::StringAttributeType::kLocale: {
412 std::shared_ptr<flutter::LocaleStringAttribute> locale_attribute =
413 std::static_pointer_cast<flutter::LocaleStringAttribute>(attribute);
414 NSDictionary* attributeDict = @{
415 UIAccessibilitySpeechAttributeLanguage : @(locale_attribute->locale.data()),
417 [attributedString setAttributes:attributeDict range:range];
420 case flutter::StringAttributeType::kSpellOut: {
421 NSDictionary* attributeDict = @{
422 UIAccessibilitySpeechAttributeSpellOut : @YES,
424 [attributedString setAttributes:attributeDict range:range];
429 return attributedString;
432 - (void)showOnScreen {
433 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kShowOnScreen);
436 #pragma mark - UIAccessibility overrides
438 - (BOOL)isAccessibilityElement {
439 if (![
self isAccessibilityBridgeAlive]) {
448 if (
self.node.flags.scopesRoute) {
452 return [
self isFocusable];
455 - (NSString*)accessibilityLanguage {
456 if (![
self isAccessibilityBridgeAlive]) {
460 if (!
self.node.locale.empty()) {
461 return @(
self.node.locale.data());
463 return self.bridge->GetDefaultLocale();
466 - (bool)isFocusable {
475 return (
self.node.flags.hasImplicitScrolling &&
self.node.flags.isHidden)
477 || !
self.node.label.empty() || !
self.node.value.empty() || !
self.node.hint.empty() ||
478 (
self.node.actions & ~
flutter::kScrollableSemanticsActions) != 0;
482 if (
self.node.flags.scopesRoute) {
483 [edges addObject:self];
485 if ([
self hasChildren]) {
487 [child collectRoutes:edges];
493 if (!
self.node.HasAction(flutter::SemanticsAction::kCustomAction)) {
496 int32_t action_id = action.
uid;
497 std::vector<uint8_t> args;
499 args.push_back(action_id);
500 args.push_back(action_id >> 8);
501 args.push_back(action_id >> 16);
502 args.push_back(action_id >> 24);
503 self.bridge->DispatchSemanticsAction(
504 self.uid, flutter::SemanticsAction::kCustomAction,
505 fml::MallocMapping::Copy(args.data(), args.size() *
sizeof(uint8_t)));
509 - (NSString*)accessibilityIdentifier {
510 if (![
self isAccessibilityBridgeAlive]) {
514 if (
self.node.identifier.empty()) {
517 return @(
self.node.identifier.data());
520 - (NSString*)accessibilityLabel {
521 if (![
self isAccessibilityBridgeAlive]) {
524 NSString* label = nil;
525 if (!
self.node.label.empty()) {
526 label = @(
self.node.label.data());
528 if (!
self.node.tooltip.empty()) {
529 label = label ? [NSString stringWithFormat:@"%@\n%@", label, @(self.node.tooltip.data())]
530 : @(
self.node.tooltip.data());
535 - (bool)containsPoint:(CGPoint)point {
537 return CGRectContainsPoint([
self globalRect], point);
541 - (id)search:(CGPoint)point {
544 if ([child containsPoint:point]) {
545 id childSearchResult = [child search:point];
546 if (childSearchResult != nil) {
547 return childSearchResult;
552 if ([
self containsPoint:point] && [
self isFocusable]) {
553 return self.nativeAccessibility;
565 - (id)_accessibilityHitTest:(CGPoint)point withEvent:(UIEvent*)event {
566 return [
self search:point];
570 - (BOOL)accessibilityScrollToVisible {
576 - (BOOL)accessibilityScrollToVisibleWithChild:(
id)child {
578 [child showOnScreen];
584 - (NSAttributedString*)accessibilityAttributedLabel {
585 NSString* label =
self.accessibilityLabel;
586 if (label.length == 0) {
589 return [
self createAttributedStringFromString:label withAttributes:self.node.labelAttributes];
592 - (NSString*)accessibilityHint {
593 if (![
self isAccessibilityBridgeAlive]) {
597 if (
self.node.hint.empty()) {
600 return @(
self.node.hint.data());
603 - (NSAttributedString*)accessibilityAttributedHint {
604 NSString* hint = [
self accessibilityHint];
605 if (hint.length == 0) {
608 return [
self createAttributedStringFromString:hint withAttributes:self.node.hintAttributes];
611 - (NSString*)accessibilityValue {
612 if (![
self isAccessibilityBridgeAlive]) {
616 if (!
self.node.value.empty()) {
617 return @(
self.node.value.data());
621 if (
self.node.flags.isInMutuallyExclusiveGroup) {
627 if (
self.node.flags.isToggled == flutter::SemanticsTristate::kTrue ||
628 self.node.flags.isChecked == flutter::SemanticsCheckState::kTrue) {
630 }
else if (
self.node.flags.isToggled == flutter::SemanticsTristate::kFalse ||
631 self.node.flags.isChecked == flutter::SemanticsCheckState::kFalse) {
638 - (NSAttributedString*)accessibilityAttributedValue {
639 NSString* value = [
self accessibilityValue];
640 if (value.length == 0) {
643 return [
self createAttributedStringFromString:value withAttributes:self.node.valueAttributes];
646 - (CGRect)accessibilityFrame {
647 if (![
self isAccessibilityBridgeAlive]) {
648 return CGRectMake(0, 0, 0, 0);
651 if (
self.node.flags.isHidden) {
652 return [
super accessibilityFrame];
654 return [
self globalRect];
657 - (CGRect)globalRect {
658 const SkRect& rect =
self.node.rect;
659 CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
663 #pragma mark - UIAccessibilityElement protocol
665 - (void)setAccessibilityContainer:(
id)container {
670 - (id)accessibilityContainer {
679 if (![
self isAccessibilityBridgeAlive]) {
683 if ([
self hasChildren] ||
self.uid ==
kRootNodeId) {
684 if (
self.container == nil) {
688 return self.container;
690 if (
self.parent == nil) {
696 return self.parent.accessibilityContainer;
699 #pragma mark - UIAccessibilityAction overrides
701 - (BOOL)accessibilityActivate {
702 if (![
self isAccessibilityBridgeAlive]) {
705 if (!
self.node.HasAction(flutter::SemanticsAction::kTap)) {
710 if (
self.node.flags.isSlider) {
715 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kTap);
719 - (void)accessibilityIncrement {
720 if (![
self isAccessibilityBridgeAlive]) {
723 if (
self.node.HasAction(flutter::SemanticsAction::kIncrease)) {
724 self.node.value =
self.node.increasedValue;
725 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kIncrease);
729 - (void)accessibilityDecrement {
730 if (![
self isAccessibilityBridgeAlive]) {
733 if (
self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
734 self.node.value =
self.node.decreasedValue;
735 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDecrease);
739 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
740 if (![
self isAccessibilityBridgeAlive]) {
744 if (!
self.node.HasAction(action)) {
747 self.bridge->DispatchSemanticsAction(
self.uid, action);
751 - (BOOL)accessibilityPerformEscape {
752 if (![
self isAccessibilityBridgeAlive]) {
755 if (!
self.node.HasAction(flutter::SemanticsAction::kDismiss)) {
758 self.bridge->DispatchSemanticsAction(
self.uid, flutter::SemanticsAction::kDismiss);
762 #pragma mark UIAccessibilityFocus overrides
764 - (void)accessibilityElementDidBecomeFocused {
765 if (![
self isAccessibilityBridgeAlive]) {
768 self.bridge->AccessibilityObjectDidBecomeFocused(
self.uid);
769 if (
self.node.flags.isHidden ||
self.node.flags.isHeader) {
772 if (
self.node.HasAction(flutter::SemanticsAction::kDidGainAccessibilityFocus)) {
773 self.bridge->DispatchSemanticsAction(
self.uid,
774 flutter::SemanticsAction::kDidGainAccessibilityFocus);
778 - (void)accessibilityElementDidLoseFocus {
779 if (![
self isAccessibilityBridgeAlive]) {
782 self.bridge->AccessibilityObjectDidLoseFocus(
self.uid);
783 if (
self.node.HasAction(flutter::SemanticsAction::kDidLoseAccessibilityFocus)) {
784 self.bridge->DispatchSemanticsAction(
self.uid,
785 flutter::SemanticsAction::kDidLoseAccessibilityFocus);
789 - (BOOL)accessibilityRespondsToUserInteraction {
790 if (
self.node.flags.isAccessibilityFocusBlocked) {
795 if ((
self.node.actions & ~flutter::kSystemActions) != 0) {
799 if (!
self.node.customAccessibilityActions.empty()) {
810 #pragma mark - Designated initializers
812 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
814 self = [
super initWithBridge:bridge uid:uid];
818 #pragma mark - UIAccessibility overrides
820 - (UIAccessibilityTraits)accessibilityTraits {
821 UIAccessibilityTraits traits = UIAccessibilityTraitNone;
822 if (
self.
node.HasAction(flutter::SemanticsAction::kIncrease) ||
823 self.node.HasAction(flutter::SemanticsAction::kDecrease)) {
824 traits |= UIAccessibilityTraitAdjustable;
827 if (
self.
node.flags.isToggled != flutter::SemanticsTristate::kNone ||
828 self.node.flags.isChecked != flutter::SemanticsCheckState::kNone) {
829 traits |= UIAccessibilityTraitButton;
831 if (
self.
node.flags.isSelected == flutter::SemanticsTristate::kTrue) {
832 traits |= UIAccessibilityTraitSelected;
834 if (
self.
node.flags.isButton) {
835 traits |= UIAccessibilityTraitButton;
837 if (
self.
node.flags.isEnabled == flutter::SemanticsTristate::kFalse) {
838 traits |= UIAccessibilityTraitNotEnabled;
840 if (
self.
node.headingLevel > 0) {
842 traits |= UIAccessibilityTraitHeader;
844 if (
self.
node.flags.isImage) {
845 traits |= UIAccessibilityTraitImage;
847 if (
self.
node.flags.isLiveRegion) {
848 traits |= UIAccessibilityTraitUpdatesFrequently;
850 if (
self.
node.flags.isLink) {
851 traits |= UIAccessibilityTraitLink;
853 if (traits == UIAccessibilityTraitNone && ![
self hasChildren] &&
854 self.accessibilityLabel.length != 0 && !
self.node.flags.isTextField) {
855 traits = UIAccessibilityTraitStaticText;
863 @property(nonatomic, weak) UIView* platformView;
868 - (instancetype)initWithBridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge
872 _platformView = platformView;
873 [platformView setFlutterAccessibilityContainer:self];
879 return self.platformView;
885 fml::WeakPtr<flutter::AccessibilityBridgeIos> _bridge;
888 #pragma mark - initializers
890 - (instancetype)initWithSemanticsObject:(
SemanticsObject*)semanticsObject
891 bridge:(
fml::WeakPtr<
flutter::AccessibilityBridgeIos>)bridge {
892 FML_DCHECK(semanticsObject) <<
"semanticsObject must be set";
897 self = [
super initWithAccessibilityContainer:bridge->view()];
900 _semanticsObject = semanticsObject;
907 #pragma mark - UIAccessibilityContainer overrides
909 - (NSInteger)accessibilityElementCount {
910 return self.semanticsObject.
children.count + 1;
913 - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
914 if (index < 0 || index >= [
self accessibilityElementCount]) {
918 return self.semanticsObject.nativeAccessibility;
923 if ([child hasChildren]) {
924 return child.accessibilityContainer;
929 - (NSInteger)indexOfAccessibilityElement:(
id)element {
930 if (element ==
self.semanticsObject.nativeAccessibility) {
934 NSArray<SemanticsObject*>* children =
self.semanticsObject.children;
935 for (
size_t i = 0; i < [children count]; i++) {
945 #pragma mark - UIAccessibilityElement protocol
947 - (BOOL)isAccessibilityElement {
951 - (CGRect)accessibilityFrame {
957 return UIScreen.mainScreen.bounds;
960 - (id)accessibilityContainer {
966 :
self.semanticsObject.
parent.accessibilityContainer;
969 #pragma mark - UIAccessibilityAction overrides
971 - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
972 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)