|
34 | 34 | return flutter::SemanticsAction::kScrollUp; |
35 | 35 | } |
36 | 36 |
|
| 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 | + |
37 | 89 | } // namespace |
38 | 90 |
|
39 | 91 | @implementation FlutterSwitchSemanticsObject { |
@@ -88,6 +140,175 @@ - (UIAccessibilityTraits)accessibilityTraits { |
88 | 140 |
|
89 | 141 | @end // FlutterSwitchSemanticsObject |
90 | 142 |
|
| 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 | +- (float)scrollExtentMax { |
| 225 | + if (![_semanticsObject isAccessibilityBridgeAlive]) { |
| 226 | + return 0.0f; |
| 227 | + } |
| 228 | + float scrollExtentMax = _semanticsObject.node.scrollExtentMax; |
| 229 | + if (isnan(scrollExtentMax)) { |
| 230 | + scrollExtentMax = 0.0f; |
| 231 | + } else if (!isfinite(scrollExtentMax)) { |
| 232 | + scrollExtentMax = kScrollExtentMaxForInf + [self scrollPosition]; |
| 233 | + } |
| 234 | + return scrollExtentMax; |
| 235 | +} |
| 236 | + |
| 237 | +- (float)scrollPosition { |
| 238 | + if (![_semanticsObject isAccessibilityBridgeAlive]) { |
| 239 | + return 0.0f; |
| 240 | + } |
| 241 | + float scrollPosition = _semanticsObject.node.scrollPosition; |
| 242 | + if (isnan(scrollPosition)) { |
| 243 | + scrollPosition = 0.0f; |
| 244 | + } |
| 245 | + NSCAssert(isfinite(scrollPosition), @"The scrollPosition must not be infinity"); |
| 246 | + return scrollPosition; |
| 247 | +} |
| 248 | + |
| 249 | +- (CGSize)contentSizeInternal { |
| 250 | + CGRect result; |
| 251 | + const SkRect& rect = _semanticsObject.node.rect; |
| 252 | + |
| 253 | + if (_semanticsObject.node.actions & flutter::kVerticalScrollSemanticsActions) { |
| 254 | + result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height() + [self scrollExtentMax]); |
| 255 | + } else if (_semanticsObject.node.actions & flutter::kHorizontalScrollSemanticsActions) { |
| 256 | + result = CGRectMake(rect.x(), rect.y(), rect.width() + [self scrollExtentMax], rect.height()); |
| 257 | + } else { |
| 258 | + result = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); |
| 259 | + } |
| 260 | + return ConvertRectToGlobal(_semanticsObject, result).size; |
| 261 | +} |
| 262 | + |
| 263 | +- (CGPoint)contentOffsetInternal { |
| 264 | + CGPoint result; |
| 265 | + CGPoint origin = self.frame.origin; |
| 266 | + const SkRect& rect = _semanticsObject.node.rect; |
| 267 | + if (_semanticsObject.node.actions & flutter::kVerticalScrollSemanticsActions) { |
| 268 | + result = ConvertPointToGlobal(_semanticsObject, |
| 269 | + CGPointMake(rect.x(), rect.y() + [self scrollPosition])); |
| 270 | + } else if (_semanticsObject.node.actions & flutter::kHorizontalScrollSemanticsActions) { |
| 271 | + result = ConvertPointToGlobal(_semanticsObject, |
| 272 | + CGPointMake(rect.x() + [self scrollPosition], rect.y())); |
| 273 | + } else { |
| 274 | + result = origin; |
| 275 | + } |
| 276 | + return CGPointMake(result.x - origin.x, result.y - origin.y); |
| 277 | +} |
| 278 | + |
| 279 | +// The following methods are explicitly forwarded to the wrapped SemanticsObject because the |
| 280 | +// forwarding logic above doesn't apply to them since they are also implemented in the |
| 281 | +// UIScrollView class, the base class. |
| 282 | + |
| 283 | +- (BOOL)accessibilityActivate { |
| 284 | + return [_semanticsObject accessibilityActivate]; |
| 285 | +} |
| 286 | + |
| 287 | +- (void)accessibilityIncrement { |
| 288 | + [_semanticsObject accessibilityIncrement]; |
| 289 | +} |
| 290 | + |
| 291 | +- (void)accessibilityDecrement { |
| 292 | + [_semanticsObject accessibilityDecrement]; |
| 293 | +} |
| 294 | + |
| 295 | +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { |
| 296 | + return [_semanticsObject accessibilityScroll:direction]; |
| 297 | +} |
| 298 | + |
| 299 | +- (BOOL)accessibilityPerformEscape { |
| 300 | + return [_semanticsObject accessibilityPerformEscape]; |
| 301 | +} |
| 302 | + |
| 303 | +- (void)accessibilityElementDidBecomeFocused { |
| 304 | + [_semanticsObject accessibilityElementDidBecomeFocused]; |
| 305 | +} |
| 306 | + |
| 307 | +- (void)accessibilityElementDidLoseFocus { |
| 308 | + [_semanticsObject accessibilityElementDidLoseFocus]; |
| 309 | +} |
| 310 | +@end // FlutterScrollableSemanticsObject |
| 311 | + |
91 | 312 | @implementation FlutterCustomAccessibilityAction { |
92 | 313 | } |
93 | 314 | @end |
@@ -174,6 +395,9 @@ - (void)setSemanticsNode:(const flutter::SemanticsNode*)node { |
174 | 395 | _node = *node; |
175 | 396 | } |
176 | 397 |
|
| 398 | +- (void)accessibilityBridgeDidFinishUpdate { /* Do nothing by default */ |
| 399 | +} |
| 400 | + |
177 | 401 | /** |
178 | 402 | * Whether calling `setSemanticsNode:` with `node` would cause a layout change. |
179 | 403 | */ |
@@ -398,27 +622,9 @@ - (CGRect)accessibilityFrame { |
398 | 622 | } |
399 | 623 |
|
400 | 624 | - (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()); |
| 625 | + const SkRect& rect = [self node].rect; |
| 626 | + CGRect localRect = CGRectMake(rect.x(), rect.y(), rect.width(), rect.height()); |
| 627 | + return ConvertRectToGlobal(self, localRect); |
422 | 628 | } |
423 | 629 |
|
424 | 630 | #pragma mark - UIAccessibilityElement protocol |
|
0 commit comments