| 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"
7 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
8 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
9 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
10
11 FLUTTER_ASSERT_ARC
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;
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
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};
218 FlutterStandardTypedData* offsetData = [FlutterStandardTypedData
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
FlutterCodecs.h
FlutterMacros.h
FlutterSemanticsScrollView.h
SemanticsObject.h
FlutterScrollableSemanticsObject(CoordinateSpace)
Definition: SemanticsObject+UIFocusSystem.mm:181
FlutterSemanticsScrollView(UIFocusItemScrollableContainer)
Definition: SemanticsObject+UIFocusSystem.mm:198
SemanticsObject(UIFocusSystem)
Definition: SemanticsObject+UIFocusSystem.mm:32
-[SemanticsObject(UIFocusSystem) focusItem]
id< UIFocusItem > focusItem()
Definition: SemanticsObject+UIFocusSystem.mm:42
FlutterScrollableSemanticsObject
Definition: SemanticsObject.h:190
FlutterSemanticsScrollView
Definition: FlutterSemanticsScrollView.h:21
FlutterStandardMessageCodec
Definition: FlutterCodecs.h:209
FlutterStandardTypedData
Definition: FlutterCodecs.h:301
+[FlutterStandardTypedData typedDataWithFloat64:]
instancetype typedDataWithFloat64:(NSData *data)
Definition: FlutterStandardCodec.mm:162
SemanticsObject
Definition: SemanticsObject.h:32
SemanticsObject::parent
SemanticsObject * parent
Definition: SemanticsObject.h:42
SemanticsObject::childrenInHitTestOrder
NSArray< SemanticsObject * > * childrenInHitTestOrder
Definition: SemanticsObject.h:74
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
+[FlutterMessageCodec-p sharedInstance]
instancetype sharedInstance()