33// found in the LICENSE file.
44
55#import " SemanticsObject.h"
6+ #include " flutter/lib/ui/semantics/semantics_node.h"
7+ #import " flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
68#import " flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
9+ #import " flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h"
710
811FLUTTER_ASSERT_ARC
912
2730// translated to calls such as -[NSObject accessibilityActivate]), while most
2831// other key events are dispatched to the framework.
2932@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 ;
3038@end
3139
3240@implementation SemanticsObject (UIFocusSystem)
3341
42+ - (id <UIFocusItem>)focusItem {
43+ return self;
44+ }
45+
3446#pragma mark - UIFocusEnvironment Conformance
3547
3648- (void )setNeedsFocusUpdate {
@@ -49,7 +61,7 @@ - (void)didUpdateFocusInContext:(UIFocusUpdateContext*)context
4961
5062- (id <UIFocusEnvironment>)parentFocusEnvironment {
5163 // The root SemanticsObject node's parent is the FlutterView.
52- return self.parent ?: self.bridge ->view ();
64+ return self.parent . focusItem ?: self.bridge ->view ();
5365}
5466
5567- (NSArray <id<UIFocusEnvironment>>*)preferredFocusEnvironments {
@@ -71,8 +83,57 @@ - (BOOL)canBecomeFocused {
7183 return self.node .HasAction (flutter::SemanticsAction::kTap );
7284}
7385
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.
7491- (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);
76137}
77138
78139#pragma mark - UIFocusItemContainer Conformance
@@ -87,16 +148,94 @@ - (CGRect)frame {
87148 //
88149 // This method is only supposed to return items within the given
89150 // rect but returning everything in the subtree seems to work fine.
90- NSMutableArray <SemanticsObject* >* reversedItems =
151+ NSMutableArray <id <UIFocusItem> >* reversedItems =
91152 [[NSMutableArray alloc ] initWithCapacity: self .childrenInHitTestOrder.count];
92153 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 ];
95156 }
96157 return reversedItems;
97158}
98159
99160- (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];
101240}
102241@end
0 commit comments