3
3
// found in the LICENSE file.
4
4
5
5
#import " SemanticsObject.h"
6
+ #include " flutter/lib/ui/semantics/semantics_node.h"
7
+ #import " flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
6
8
#import " flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
9
+ #import " flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
7
10
8
11
FLUTTER_ASSERT_ARC
9
12
27
30
// translated to calls such as -[NSObject accessibilityActivate]), while most
28
31
// other key events are dispatched to the framework.
29
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 ;
30
38
@end
31
39
32
40
@implementation SemanticsObject (UIFocusSystem)
33
41
42
+ - (id <UIFocusItem>)focusItem {
43
+ return self;
44
+ }
45
+
34
46
#pragma mark - UIFocusEnvironment Conformance
35
47
36
48
- (void )setNeedsFocusUpdate {
@@ -49,7 +61,7 @@ - (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
49
61
50
62
- (id <UIFocusEnvironment>)parentFocusEnvironment {
51
63
// The root SemanticsObject node's parent is the FlutterView.
52
- return self.parent ?: self.bridge ->view ();
64
+ return self.parent . focusItem ?: self.bridge ->view ();
53
65
}
54
66
55
67
- (NSArray <id<UIFocusEnvironment>>*)preferredFocusEnvironments {
@@ -71,8 +83,57 @@ - (BOOL)canBecomeFocused {
71
83
return self.node .HasAction (flutter::SemanticsAction::kTap );
72
84
}
73
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.
74
91
- (CGRect )frame {
75
- return self.accessibilityFrame ;
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);
76
137
}
77
138
78
139
#pragma mark - UIFocusItemContainer Conformance
@@ -87,16 +148,94 @@ - (CGRect)frame {
87
148
//
88
149
// This method is only supposed to return items within the given
89
150
// rect but returning everything in the subtree seems to work fine.
90
- NSMutableArray <SemanticsObject* >* reversedItems =
151
+ NSMutableArray <id <UIFocusItem> >* reversedItems =
91
152
[[NSMutableArray alloc ] initWithCapacity: self .childrenInHitTestOrder.count];
92
153
for (NSUInteger i = 0 ; i < self.childrenInHitTestOrder .count ; ++i) {
93
- [reversedItems
94
- addObject: self .childrenInHitTestOrder[ self .childrenInHitTestOrder.count - 1 - i] ];
154
+ SemanticsObject* child = self. childrenInHitTestOrder [ self .childrenInHitTestOrder.count - 1 - i];
155
+ [reversedItems addObject: child.focusItem ];
95
156
}
96
157
return reversedItems;
97
158
}
98
159
99
160
- (id <UICoordinateSpace>)coordinateSpace {
100
- return self.bridge ->view ();
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];
101
240
}
102
241
@end
0 commit comments