Flutter iOS Embedder
SemanticsObject+UIFocusSystem.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 
5 #import "SemanticsObject.h"
6 #include "flutter/lib/ui/semantics/semantics_node.h"
10 
12 
13 // The SemanticsObject class conforms to UIFocusItem and UIFocusItemContainer
14 // protocols, so the SemanticsObject tree can also be used to represent
15 // interactive UI components on screen that can receive UIFocusSystem focus.
16 //
17 // Typically, physical key events received by the FlutterViewController is
18 // first delivered to the framework, but that stopped working for navigation keys
19 // since iOS 15 when full keyboard access (FKA) is on, because those events are
20 // consumed by the UIFocusSystem and never dispatched to the UIResponders in the
21 // application (see
22 // https://developer.apple.com/documentation/uikit/uikeycommand/3780513-wantspriorityoversystembehavior
23 // ). FKA relies on the iOS focus engine, to enable FKA on iOS 15+, we use
24 // SemanticsObject to provide the iOS focus engine with the required hierarchical
25 // information and geometric context.
26 //
27 // The focus engine focus is different from accessibility focus, or even the
28 // currentFocus of the Flutter FocusManager in the framework. On iOS 15+, FKA
29 // key events are dispatched to the current iOS focus engine focus (and
30 // translated to calls such as -[NSObject accessibilityActivate]), while most
31 // other key events are dispatched to the framework.
32 @interface SemanticsObject (UIFocusSystem) <UIFocusItem, UIFocusItemContainer>
33 /// The `UIFocusItem` that represents this SemanticsObject.
34 ///
35 /// For regular `SemanticsObject`s, this method returns `self`,
36 /// for `FlutterScrollableSemanticsObject`s, this method returns its scroll view.
37 - (id<UIFocusItem>)focusItem;
38 @end
39 
40 @implementation SemanticsObject (UIFocusSystem)
41 
42 - (id<UIFocusItem>)focusItem {
43  return self;
44 }
45 
46 #pragma mark - UIFocusEnvironment Conformance
47 
48 - (void)setNeedsFocusUpdate {
49 }
50 
51 - (void)updateFocusIfNeeded {
52 }
53 
54 - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext*)context {
55  return YES;
56 }
57 
58 - (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
59  withAnimationCoordinator:(UIFocusAnimationCoordinator*)coordinator {
60 }
61 
62 - (id<UIFocusEnvironment>)parentFocusEnvironment {
63  // The root SemanticsObject node's parent is the FlutterView.
64  return self.parent.focusItem ?: ([self isAccessibilityBridgeAlive] ? self.bridge->view() : nil);
65 }
66 
67 - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
68  return nil;
69 }
70 
71 - (id<UIFocusItemContainer>)focusItemContainer {
72  return self;
73 }
74 
75 #pragma mark - UIFocusItem Conformance
76 
77 - (BOOL)canBecomeFocused {
78  if (self.node.flags.isHidden) {
79  return NO;
80  }
81  // Currently only supports SemanticsObjects that handle
82  // -[NSObject accessibilityActivate].
83  return self.node.HasAction(flutter::SemanticsAction::kTap);
84 }
85 
86 // The frame is described in the `coordinateSpace` of the
87 // `parentFocusEnvironment` (all `parentFocusEnvironment`s are `UIFocusItem`s).
88 //
89 // See also the `coordinateSpace` implementation.
90 // TODO(LongCatIsLooong): use CoreGraphics types.
91 - (CGRect)frame {
92  if (![self isAccessibilityBridgeAlive]) {
93  return CGRectZero;
94  }
95  SkPoint quad[4] = {SkPoint::Make(self.node.rect.left(), self.node.rect.top()),
96  SkPoint::Make(self.node.rect.left(), self.node.rect.bottom()),
97  SkPoint::Make(self.node.rect.right(), self.node.rect.top()),
98  SkPoint::Make(self.node.rect.right(), self.node.rect.bottom())};
99 
100  SkM44 transform = self.node.transform;
101  FlutterSemanticsScrollView* scrollView;
102  for (SemanticsObject* ancestor = self.parent; ancestor; ancestor = ancestor.parent) {
103  if ([ancestor isKindOfClass:[FlutterScrollableSemanticsObject class]]) {
104  scrollView = ((FlutterScrollableSemanticsObject*)ancestor).scrollView;
105  break;
106  }
107  transform = ancestor.node.transform * transform;
108  }
109 
110  for (auto& vertex : quad) {
111  SkV4 vector = transform.map(vertex.x(), vertex.y(), 0, 1);
112  vertex = SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
113  }
114 
115  SkRect rect;
116  rect.setBounds({quad, 4});
117  // If this UIFocusItemContainer's coordinateSpace is a UIScrollView, offset
118  // the rect by `contentOffset` because the contentOffset translation is
119  // incorporated into the paint transform at different node depth in UIKit
120  // and Flutter. In Flutter, the translation is added to the cells
121  // while in UIKit the viewport's bounds is manipulated (IOW, each cell's frame
122  // in the UIScrollView coordinateSpace does not change when the UIScrollView
123  // scrolls).
124  CGRect unscaledRect =
125  CGRectMake(rect.x() + scrollView.bounds.origin.x, rect.y() + scrollView.bounds.origin.y,
126  rect.width(), rect.height());
127  if (scrollView) {
128  return unscaledRect;
129  }
130  // `rect` could be in physical pixels since the root RenderObject ("RenderView")
131  // applies a transform that turns logical pixels to physical pixels. Undo the
132  // transform by dividing the coordinates by the screen's scale factor, if this
133  // UIFocusItem's reported `coordinateSpace` is the root view (which means this
134  // UIFocusItem is not inside of a scroll view).
135  //
136  // Screen can be nil if the FlutterView is covered by another native view.
137  CGFloat scale = (self.bridge->view().window.screen ?: UIScreen.mainScreen).scale;
138  return CGRectMake(unscaledRect.origin.x / scale, unscaledRect.origin.y / scale,
139  unscaledRect.size.width / scale, unscaledRect.size.height / scale);
140 }
141 
142 #pragma mark - UIFocusItemContainer Conformance
143 
144 - (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
145  // It seems the iOS focus system relies heavily on focusItemsInRect
146  // (instead of preferredFocusEnvironments) for directional navigation.
147  //
148  // The order of the items seems to be important, menus and dialogs become
149  // unreachable via FKA if the returned children are organized
150  // in hit-test order.
151  //
152  // This method is only supposed to return items within the given
153  // rect but returning everything in the subtree seems to work fine.
154  NSMutableArray<id<UIFocusItem>>* reversedItems =
155  [[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count];
156  for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) {
157  SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i];
158  [reversedItems addObject:child.focusItem];
159  }
160  return reversedItems;
161 }
162 
163 - (id<UICoordinateSpace>)coordinateSpace {
164  // A regular SemanticsObject uses the same coordinate space as its parent.
165  return self.parent.coordinateSpace
166  ?: ([self isAccessibilityBridgeAlive] ? self.bridge->view() : nil);
167 }
168 
169 @end
170 
171 /// Scrollable containers interact with the iOS focus engine using the
172 /// `UIFocusItemScrollableContainer` protocol. The said protocol (and other focus-related protocols)
173 /// does not provide means to inform the focus system of layout changes. In order for the focus
174 /// highlight to update properly as the scroll view scrolls, this implementation incorporates a
175 /// UIScrollView into the focus hierarchy to workaround the highlight update problem.
176 ///
177 /// As a result, in the current implementation only scrollable containers and the root node
178 /// establish their own `coordinateSpace`s. All other `UIFocusItemContainter`s use the same
179 /// `coordinateSpace` as the containing UIScrollView, or the root `FlutterView`, whichever is
180 /// closer.
181 ///
182 /// See also the `frame` method implementation.
183 #pragma mark - Scrolling
184 
186 @end
187 
189 - (id<UICoordinateSpace>)coordinateSpace {
190  // A scrollable SemanticsObject uses the same coordinate space as the scroll view.
191  // This may not work very well in nested scroll views.
192  return self.scrollView;
193 }
194 
195 - (id<UIFocusItem>)focusItem {
196  return self.scrollView;
197 }
198 
199 @end
200 
202  UIFocusItemScrollableContainer>
203 @end
204 
206 
207 #pragma mark - FlutterSemanticsScrollView UIFocusItemScrollableContainer Conformance
208 
209 - (CGSize)visibleSize {
210  return self.frame.size;
211 }
212 
213 - (void)setContentOffset:(CGPoint)contentOffset {
214  [super setContentOffset:contentOffset];
215  // Do no send flutter::SemanticsAction::kScrollToOffset if it's triggered
216  // by a framework update.
217  if (![self.semanticsObject isAccessibilityBridgeAlive] || !self.isDoingSystemScrolling) {
218  return;
219  }
220 
221  double offset[2] = {contentOffset.x, contentOffset.y};
223  typedDataWithFloat64:[NSData dataWithBytes:&offset length:sizeof(offset)]];
224  NSData* encoded = [[FlutterStandardMessageCodec sharedInstance] encode:offsetData];
225  self.semanticsObject.bridge->DispatchSemanticsAction(
226  self.semanticsObject.uid, flutter::SemanticsAction::kScrollToOffset,
227  fml::MallocMapping::Copy(encoded.bytes, encoded.length));
228 }
229 
230 - (BOOL)canBecomeFocused {
231  return NO;
232 }
233 
234 - (id<UIFocusEnvironment>)parentFocusEnvironment {
235  return self.semanticsObject.parentFocusEnvironment;
236 }
237 
238 - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
239  return nil;
240 }
241 
242 - (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
243  return [self.semanticsObject focusItemsInRect:rect];
244 }
245 @end
instancetype typedDataWithFloat64:(NSData *data)
SemanticsObject * parent
NSArray< SemanticsObject * > * childrenInHitTestOrder
instancetype sharedInstance()