Flutter Engine Uber Docs
Docs for the entire Flutter Engine repo.
 
Loading...
Searching...
No Matches
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"
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
185@interface FlutterScrollableSemanticsObject (CoordinateSpace)
186@end
187
188@implementation FlutterScrollableSemanticsObject (CoordinateSpace)
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
201@interface FlutterSemanticsScrollView (UIFocusItemScrollableContainer) <
202 UIFocusItemScrollableContainer>
203@end
204
205@implementation FlutterSemanticsScrollView (UIFocusItemScrollableContainer)
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(
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
static MallocMapping Copy(const T *begin, const T *end)
Definition mapping.h:162
instancetype typedDataWithFloat64:(NSData *data)
SemanticsObject * parent
NSArray< SemanticsObject * > * childrenInHitTestOrder
instancetype sharedInstance()
const uintptr_t id
int BOOL