Skip to content

Commit 53e4dcf

Browse files
authored
Issues/80711 reland (flutter#26813)
1 parent 9db84d1 commit 53e4dcf

File tree

5 files changed

+394
-28
lines changed

5 files changed

+394
-28
lines changed

lib/ui/semantics/semantics_node.h

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,17 @@ enum class SemanticsAction : int32_t {
4444
kSetText = 1 << 21,
4545
};
4646

47-
const int kScrollableSemanticsActions =
48-
static_cast<int32_t>(SemanticsAction::kScrollLeft) |
49-
static_cast<int32_t>(SemanticsAction::kScrollRight) |
47+
const int kVerticalScrollSemanticsActions =
5048
static_cast<int32_t>(SemanticsAction::kScrollUp) |
5149
static_cast<int32_t>(SemanticsAction::kScrollDown);
5250

51+
const int kHorizontalScrollSemanticsActions =
52+
static_cast<int32_t>(SemanticsAction::kScrollLeft) |
53+
static_cast<int32_t>(SemanticsAction::kScrollRight);
54+
55+
const int kScrollableSemanticsActions =
56+
kVerticalScrollSemanticsActions | kHorizontalScrollSemanticsActions;
57+
5358
/// C/C++ representation of `SemanticsFlags` defined in
5459
/// `lib/ui/semantics.dart`.
5560
///\warning This must match the `SemanticsFlags` enum in

shell/platform/darwin/ios/framework/Source/SemanticsObject.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge_ios.h"
1414

1515
constexpr int32_t kRootNodeId = 0;
16+
// This can be arbitrary number as long as it is bigger than 0.
17+
constexpr float kScrollExtentMaxForInf = 1000;
1618

1719
@class FlutterCustomAccessibilityAction;
1820
@class FlutterPlatformViewSemanticsContainer;
@@ -31,7 +33,7 @@ constexpr int32_t kRootNodeId = 0;
3133
* The parent of this node in the node tree. Will be nil for the root node and
3234
* during transient state changes.
3335
*/
34-
@property(nonatomic, readonly) SemanticsObject* parent;
36+
@property(nonatomic, assign) SemanticsObject* parent;
3537

3638
/**
3739
* The accessibility bridge that this semantics object is attached to. This
@@ -94,6 +96,14 @@ constexpr int32_t kRootNodeId = 0;
9496

9597
- (BOOL)onCustomAccessibilityAction:(FlutterCustomAccessibilityAction*)action;
9698

99+
/**
100+
* Called after accessibility bridge finishes a semantics update.
101+
*
102+
* Subclasses can override this method if they contain states that can only be
103+
* updated once every node in the accessibility tree has finished updating.
104+
*/
105+
- (void)accessibilityBridgeDidFinishUpdate;
106+
97107
#pragma mark - Designated initializers
98108

99109
- (instancetype)init __attribute__((unavailable("Use initWithBridge instead")));
@@ -159,6 +169,18 @@ constexpr int32_t kRootNodeId = 0;
159169

160170
@end
161171

172+
/// The semantics object for scrollable. This class creates an UIScrollView to interact with the
173+
/// iOS.
174+
@interface FlutterScrollableSemanticsObject : UIScrollView
175+
176+
- (instancetype)init NS_UNAVAILABLE;
177+
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
178+
- (instancetype)initWithCoder:(NSCoder*)coder NS_UNAVAILABLE;
179+
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject NS_DESIGNATED_INITIALIZER;
180+
- (void)accessibilityBridgeDidFinishUpdate;
181+
182+
@end
183+
162184
/**
163185
* Represents a semantics object that has children and hence has to be presented to the OS as a
164186
* UIAccessibilityContainer.

shell/platform/darwin/ios/framework/Source/SemanticsObject.mm

Lines changed: 204 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,58 @@
3434
return flutter::SemanticsAction::kScrollUp;
3535
}
3636

37+
SkM44 GetGlobalTransform(SemanticsObject* reference) {
38+
SkM44 globalTransform = [reference node].transform;
39+
for (SemanticsObject* parent = [reference parent]; parent; parent = parent.parent) {
40+
globalTransform = parent.node.transform * globalTransform;
41+
}
42+
return globalTransform;
43+
}
44+
45+
SkPoint ApplyTransform(SkPoint& point, const SkM44& transform) {
46+
SkV4 vector = transform.map(point.x(), point.y(), 0, 1);
47+
return SkPoint::Make(vector.x / vector.w, vector.y / vector.w);
48+
}
49+
50+
CGPoint ConvertPointToGlobal(SemanticsObject* reference, CGPoint local_point) {
51+
SkM44 globalTransform = GetGlobalTransform(reference);
52+
SkPoint point = SkPoint::Make(local_point.x, local_point.y);
53+
point = ApplyTransform(point, globalTransform);
54+
// `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
55+
// the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
56+
// convert.
57+
CGFloat scale = [[[reference bridge]->view() window] screen].scale;
58+
auto result = CGPointMake(point.x() / scale, point.y() / scale);
59+
return [[reference bridge]->view() convertPoint:result toView:nil];
60+
}
61+
62+
CGRect ConvertRectToGlobal(SemanticsObject* reference, CGRect local_rect) {
63+
SkM44 globalTransform = GetGlobalTransform(reference);
64+
65+
SkPoint quad[4] = {
66+
SkPoint::Make(local_rect.origin.x, local_rect.origin.y), // top left
67+
SkPoint::Make(local_rect.origin.x + local_rect.size.width, local_rect.origin.y), // top right
68+
SkPoint::Make(local_rect.origin.x + local_rect.size.width,
69+
local_rect.origin.y + local_rect.size.height), // bottom right
70+
SkPoint::Make(local_rect.origin.x,
71+
local_rect.origin.y + local_rect.size.height) // bottom left
72+
};
73+
for (auto& point : quad) {
74+
point = ApplyTransform(point, globalTransform);
75+
}
76+
SkRect rect;
77+
NSCAssert(rect.setBoundsCheck(quad, 4), @"Transformed points can't form a rect");
78+
rect.setBounds(quad, 4);
79+
80+
// `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
81+
// the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
82+
// convert.
83+
CGFloat scale = [[[reference bridge]->view() window] screen].scale;
84+
auto result =
85+
CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
86+
return UIAccessibilityConvertFrameToScreenCoordinates(result, [reference bridge]->view());
87+
}
88+
3789
} // namespace
3890

3991
@implementation FlutterSwitchSemanticsObject {
@@ -88,6 +140,152 @@ - (UIAccessibilityTraits)accessibilityTraits {
88140

89141
@end // FlutterSwitchSemanticsObject
90142

143+
@interface FlutterScrollableSemanticsObject ()
144+
@property(nonatomic, strong) SemanticsObject* semanticsObject;
145+
@end
146+
147+
@implementation FlutterScrollableSemanticsObject {
148+
fml::scoped_nsobject<SemanticsObjectContainer> _container;
149+
}
150+
151+
- (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject {
152+
self = [super initWithFrame:CGRectZero];
153+
if (self) {
154+
_semanticsObject = [semanticsObject retain];
155+
[semanticsObject.bridge->view() addSubview:self];
156+
}
157+
return self;
158+
}
159+
160+
- (void)dealloc {
161+
_container.get().semanticsObject = nil;
162+
[_semanticsObject release];
163+
[self removeFromSuperview];
164+
[super dealloc];
165+
}
166+
167+
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
168+
return nil;
169+
}
170+
171+
- (NSMethodSignature*)methodSignatureForSelector:(SEL)sel {
172+
NSMethodSignature* result = [super methodSignatureForSelector:sel];
173+
if (!result) {
174+
result = [_semanticsObject methodSignatureForSelector:sel];
175+
}
176+
return result;
177+
}
178+
179+
- (void)forwardInvocation:(NSInvocation*)anInvocation {
180+
[anInvocation setTarget:_semanticsObject];
181+
[anInvocation invoke];
182+
}
183+
184+
- (void)accessibilityBridgeDidFinishUpdate {
185+
// In order to make iOS think this UIScrollView is scrollable, the following
186+
// requirements must be true.
187+
// 1. contentSize must be bigger than the frame size.
188+
// 2. The scrollable isAccessibilityElement must return YES
189+
//
190+
// Once the requirements are met, the iOS uses contentOffset to determine
191+
// what scroll actions are available. e.g. If the view scrolls vertically and
192+
// contentOffset is 0.0, only the scroll down action is available.
193+
[self setFrame:[_semanticsObject accessibilityFrame]];
194+
[self setContentSize:[self contentSizeInternal]];
195+
[self setContentOffset:[self contentOffsetInternal] animated:NO];
196+
if (self.contentSize.width > self.frame.size.width ||
197+
self.contentSize.height > self.frame.size.height) {
198+
self.isAccessibilityElement = YES;
199+
} else {
200+
self.isAccessibilityElement = NO;
201+
}
202+
}
203+
204+
- (void)setChildren:(NSArray<SemanticsObject*>*)children {
205+
[_semanticsObject setChildren:children];
206+
// The children's parent is pointing to _semanticsObject, need to manually
207+
// set it this object.
208+
for (SemanticsObject* child in _semanticsObject.children) {
209+
child.parent = (SemanticsObject*)self;
210+
}
211+
}
212+
213+
- (id)accessibilityContainer {
214+
if (_container == nil) {
215+
_container.reset([[SemanticsObjectContainer alloc]
216+
initWithSemanticsObject:(SemanticsObject*)self
217+
bridge:[_semanticsObject bridge]]);
218+
}
219+
return _container.get();
220+
}
221+
222+
// private methods
223+
224+
- (CGSize)contentSizeInternal {
225+
CGRect result;
226+
const SkRect& rect = _semanticsObject.node.rect;
227+
float scrollExtentMax = isfinite(_semanticsObject.node.scrollExtentMax)
228+
? _semanticsObject.node.scrollExtentMax
229+
: kScrollExtentMaxForInf + _semanticsObject.node.scrollPosition;
230+
if (_semanticsObject.node.actions & flutter::kVerticalScrollSemanticsActions) {
231+
result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + scrollExtentMax);
232+
} else if (_semanticsObject.node.actions & flutter::kHorizontalScrollSemanticsActions) {
233+
result = CGRectMake(rect.x(), rect.y(), rect.width() + scrollExtentMax, rect.height());
234+
} else {
235+
result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
236+
}
237+
return ConvertRectToGlobal(_semanticsObject, result).size;
238+
}
239+
240+
- (CGPoint)contentOffsetInternal {
241+
CGPoint result;
242+
CGPoint origin = self.frame.origin;
243+
const SkRect& rect = _semanticsObject.node.rect;
244+
if (_semanticsObject.node.actions & flutter::kVerticalScrollSemanticsActions) {
245+
result = ConvertPointToGlobal(
246+
_semanticsObject, CGPointMake(rect.x(), rect.y() + _semanticsObject.node.scrollPosition));
247+
} else if (_semanticsObject.node.actions & flutter::kHorizontalScrollSemanticsActions) {
248+
result = ConvertPointToGlobal(
249+
_semanticsObject, CGPointMake(rect.x() + _semanticsObject.node.scrollPosition, rect.y()));
250+
} else {
251+
result = origin;
252+
}
253+
return CGPointMake(result.x - origin.x, result.y - origin.y);
254+
}
255+
256+
// The following methods are explicitly forwarded to the wrapped SemanticsObject because the
257+
// forwarding logic above doesn't apply to them since they are also implemented in the
258+
// UIScrollView class, the base class.
259+
260+
- (BOOL)accessibilityActivate {
261+
return [_semanticsObject accessibilityActivate];
262+
}
263+
264+
- (void)accessibilityIncrement {
265+
[_semanticsObject accessibilityIncrement];
266+
}
267+
268+
- (void)accessibilityDecrement {
269+
[_semanticsObject accessibilityDecrement];
270+
}
271+
272+
- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
273+
return [_semanticsObject accessibilityScroll:direction];
274+
}
275+
276+
- (BOOL)accessibilityPerformEscape {
277+
return [_semanticsObject accessibilityPerformEscape];
278+
}
279+
280+
- (void)accessibilityElementDidBecomeFocused {
281+
[_semanticsObject accessibilityElementDidBecomeFocused];
282+
}
283+
284+
- (void)accessibilityElementDidLoseFocus {
285+
[_semanticsObject accessibilityElementDidLoseFocus];
286+
}
287+
@end // FlutterScrollableSemanticsObject
288+
91289
@implementation FlutterCustomAccessibilityAction {
92290
}
93291
@end
@@ -174,6 +372,9 @@ - (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
174372
_node = *node;
175373
}
176374

375+
- (void)accessibilityBridgeDidFinishUpdate { /* Do nothing by default */
376+
}
377+
177378
/**
178379
* Whether calling `setSemanticsNode:` with `node` would cause a layout change.
179380
*/
@@ -398,27 +599,9 @@ - (CGRect)accessibilityFrame {
398599
}
399600

400601
- (CGRect)globalRect {
401-
SkM44 globalTransform = [self node].transform;
402-
for (SemanticsObject* parent = [self parent]; parent; parent = parent.parent) {
403-
globalTransform = parent.node.transform * globalTransform;
404-
}
405-
406-
SkPoint quad[4];
407-
[self node].rect.toQuad(quad);
408-
for (auto& point : quad) {
409-
SkV4 vector = globalTransform.map(point.x(), point.y(), 0, 1);
410-
point.set(vector.x / vector.w, vector.y / vector.w);
411-
}
412-
SkRect rect;
413-
rect.setBounds(quad, 4);
414-
415-
// `rect` is in the physical pixel coordinate system. iOS expects the accessibility frame in
416-
// the logical pixel coordinate system. Therefore, we divide by the `scale` (pixel ratio) to
417-
// convert.
418-
CGFloat scale = [[[self bridge]->view() window] screen].scale;
419-
auto result =
420-
CGRectMake(rect.x() / scale, rect.y() / scale, rect.width() / scale, rect.height() / scale);
421-
return UIAccessibilityConvertFrameToScreenCoordinates(result, [self bridge]->view());
602+
const SkRect& rect = [self node].rect;
603+
CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height());
604+
return ConvertRectToGlobal(self, localRect);
422605
}
423606

424607
#pragma mark - UIAccessibilityElement protocol

0 commit comments

Comments
 (0)