Flutter Engine
 
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.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.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 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;
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
181@interface FlutterScrollableSemanticsObject (CoordinateSpace)
182@end
183
184@implementation FlutterScrollableSemanticsObject (CoordinateSpace)
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
197@interface FlutterSemanticsScrollView (UIFocusItemScrollableContainer) <
198 UIFocusItemScrollableContainer>
199@end
200
201@implementation FlutterSemanticsScrollView (UIFocusItemScrollableContainer)
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(
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
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