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.bridge->view();
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 & static_cast<int32_t>(flutter::SemanticsFlags::kIsHidden)) != 0) {
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  SkPoint quad[4] = {SkPoint::Make(self.node.rect.left(), self.node.rect.top()),
93  SkPoint::Make(self.node.rect.left(), self.node.rect.bottom()),
94  SkPoint::Make(self.node.rect.right(), self.node.rect.top()),
95  SkPoint::Make(self.node.rect.right(), self.node.rect.bottom())};
96 
97  SkM44 transform = self.node.transform;
98  FlutterSemanticsScrollView* scrollView;
99  for (SemanticsObject* ancestor = self.parent; ancestor; ancestor = ancestor.parent) {
100  if ([ancestor isKindOfClass:[FlutterScrollableSemanticsObject class]]) {
101  scrollView = ((FlutterScrollableSemanticsObject*)ancestor).scrollView;
102  break;
103  }
104  transform = ancestor.node.transform * transform;
105  }
106 
107  for (auto& vertex : quad) {
108  SkV4 vector = transform.map(vertex.x(), vertex.y(), 0, 1);
109  vertex = SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
110  }
111 
112  SkRect rect;
113  rect.setBounds(quad, 4);
114  // If this UIFocusItemContainer's coordinateSpace is a UIScrollView, offset
115  // the rect by `contentOffset` because the contentOffset translation is
116  // incorporated into the paint transform at different node depth in UIKit
117  // and Flutter. In Flutter, the translation is added to the cells
118  // while in UIKit the viewport's bounds is manipulated (IOW, each cell's frame
119  // in the UIScrollView coordinateSpace does not change when the UIScrollView
120  // scrolls).
121  CGRect unscaledRect =
122  CGRectMake(rect.x() + scrollView.bounds.origin.x, rect.y() + scrollView.bounds.origin.y,
123  rect.width(), rect.height());
124  if (scrollView) {
125  return unscaledRect;
126  }
127  // `rect` could be in physical pixels since the root RenderObject ("RenderView")
128  // applies a transform that turns logical pixels to physical pixels. Undo the
129  // transform by dividing the coordinates by the screen's scale factor, if this
130  // UIFocusItem's reported `coordinateSpace` is the root view (which means this
131  // UIFocusItem is not inside of a scroll view).
132  //
133  // Screen can be nil if the FlutterView is covered by another native view.
134  CGFloat scale = (self.bridge->view().window.screen ?: UIScreen.mainScreen).scale;
135  return CGRectMake(unscaledRect.origin.x / scale, unscaledRect.origin.y / scale,
136  unscaledRect.size.width / scale, unscaledRect.size.height / scale);
137 }
138 
139 #pragma mark - UIFocusItemContainer Conformance
140 
141 - (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
142  // It seems the iOS focus system relies heavily on focusItemsInRect
143  // (instead of preferredFocusEnvironments) for directional navigation.
144  //
145  // The order of the items seems to be important, menus and dialogs become
146  // unreachable via FKA if the returned children are organized
147  // in hit-test order.
148  //
149  // This method is only supposed to return items within the given
150  // rect but returning everything in the subtree seems to work fine.
151  NSMutableArray<id<UIFocusItem>>* reversedItems =
152  [[NSMutableArray alloc] initWithCapacity:self.childrenInHitTestOrder.count];
153  for (NSUInteger i = 0; i < self.childrenInHitTestOrder.count; ++i) {
154  SemanticsObject* child = self.childrenInHitTestOrder[self.childrenInHitTestOrder.count - 1 - i];
155  [reversedItems addObject:child.focusItem];
156  }
157  return reversedItems;
158 }
159 
160 - (id<UICoordinateSpace>)coordinateSpace {
161  // A regular SemanticsObject uses the same coordinate space as its parent.
162  return self.parent.coordinateSpace ?: self.bridge->view();
163 }
164 
165 @end
166 
167 /// Scrollable containers interact with the iOS focus engine using the
168 /// `UIFocusItemScrollableContainer` protocol. The said protocol (and other focus-related protocols)
169 /// does not provide means to inform the focus system of layout changes. In order for the focus
170 /// highlight to update properly as the scroll view scrolls, this implementation incorporates a
171 /// UIScrollView into the focus hierarchy to workaround the highlight update problem.
172 ///
173 /// As a result, in the current implementation only scrollable containers and the root node
174 /// establish their own `coordinateSpace`s. All other `UIFocusItemContainter`s use the same
175 /// `coordinateSpace` as the containing UIScrollView, or the root `FlutterView`, whichever is
176 /// closer.
177 ///
178 /// See also the `frame` method implementation.
179 #pragma mark - Scrolling
180 
182 @end
183 
185 - (id<UICoordinateSpace>)coordinateSpace {
186  // A scrollable SemanticsObject uses the same coordinate space as the scroll view.
187  // This may not work very well in nested scroll views.
188  return self.scrollView;
189 }
190 
191 - (id<UIFocusItem>)focusItem {
192  return self.scrollView;
193 }
194 
195 @end
196 
198  UIFocusItemScrollableContainer>
199 @end
200 
202 
203 #pragma mark - FlutterSemanticsScrollView UIFocusItemScrollableContainer Conformance
204 
205 - (CGSize)visibleSize {
206  return self.frame.size;
207 }
208 
209 - (void)setContentOffset:(CGPoint)contentOffset {
210  [super setContentOffset:contentOffset];
211  // Do no send flutter::SemanticsAction::kScrollToOffset if it's triggered
212  // by a framework update.
213  if (![self.semanticsObject isAccessibilityBridgeAlive] || !self.isDoingSystemScrolling) {
214  return;
215  }
216 
217  double offset[2] = {contentOffset.x, contentOffset.y};
219  typedDataWithFloat64:[NSData dataWithBytes:&offset length:sizeof(offset)]];
220  NSData* encoded = [[FlutterStandardMessageCodec sharedInstance] encode:offsetData];
221  self.semanticsObject.bridge->DispatchSemanticsAction(
222  self.semanticsObject.uid, flutter::SemanticsAction::kScrollToOffset,
223  fml::MallocMapping::Copy(encoded.bytes, encoded.length));
224 }
225 
226 - (BOOL)canBecomeFocused {
227  return NO;
228 }
229 
230 - (id<UIFocusEnvironment>)parentFocusEnvironment {
231  return self.semanticsObject.parentFocusEnvironment;
232 }
233 
234 - (NSArray<id<UIFocusEnvironment>>*)preferredFocusEnvironments {
235  return nil;
236 }
237 
238 - (NSArray<id<UIFocusItem>>*)focusItemsInRect:(CGRect)rect {
239  return [self.semanticsObject focusItemsInRect:rect];
240 }
241 @end
+[FlutterStandardTypedData typedDataWithFloat64:]
instancetype typedDataWithFloat64:(NSData *data)
Definition: FlutterStandardCodec.mm:162
SemanticsObject::parent
SemanticsObject * parent
Definition: SemanticsObject.h:42
FlutterScrollableSemanticsObject(CoordinateSpace)
Definition: SemanticsObject+UIFocusSystem.mm:181
FlutterSemanticsScrollView.h
FlutterSemanticsScrollView(UIFocusItemScrollableContainer)
Definition: SemanticsObject+UIFocusSystem.mm:197
SemanticsObject(UIFocusSystem)
Definition: SemanticsObject+UIFocusSystem.mm:32
FlutterMacros.h
-[SemanticsObject(UIFocusSystem) focusItem]
id< UIFocusItem > focusItem()
Definition: SemanticsObject+UIFocusSystem.mm:42
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
FlutterStandardMessageCodec
Definition: FlutterCodecs.h:209
SemanticsObject::childrenInHitTestOrder
NSArray< SemanticsObject * > * childrenInHitTestOrder
Definition: SemanticsObject.h:74
FlutterCodecs.h
SemanticsObject.h
FlutterStandardTypedData
Definition: FlutterCodecs.h:300
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:189
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()
SemanticsObject
Definition: SemanticsObject.h:31