Skip to content

Commit 47a9c76

Browse files
Chris YangRBogie
authored andcommitted
Add a11y support for embedded iOS platform view (flutter#8156)
Follow up the framework change in flutter/flutter#29304. Inject the accessibility element tree in the semantic node if the node is for platform views. flutter/flutter#29302
1 parent 85d913c commit 47a9c76

File tree

7 files changed

+124
-6
lines changed

7 files changed

+124
-6
lines changed

lib/ui/semantics/semantics_node.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
namespace blink {
1010

11+
constexpr int32_t kMinPlatfromViewId = -1;
12+
1113
SemanticsNode::SemanticsNode() = default;
1214

1315
SemanticsNode::SemanticsNode(const SemanticsNode& other) = default;
@@ -22,4 +24,8 @@ bool SemanticsNode::HasFlag(SemanticsFlags flag) {
2224
return (flags & static_cast<int32_t>(flag)) != 0;
2325
}
2426

27+
bool SemanticsNode::IsPlatformViewNode() const {
28+
return platformViewId > kMinPlatfromViewId;
29+
}
30+
2531
} // namespace blink

lib/ui/semantics/semantics_node.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ struct SemanticsNode {
8181
bool HasAction(SemanticsAction action);
8282
bool HasFlag(SemanticsFlags flag);
8383

84+
// Whether this node is for embeded platform views.
85+
bool IsPlatformViewNode() const;
86+
8487
int32_t id = 0;
8588
int32_t flags = 0;
8689
int32_t actions = 0;

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@
165165
composition_order_.push_back(view_id);
166166
}
167167

168+
NSObject<FlutterPlatformView>* FlutterPlatformViewsController::GetPlatformViewByID(int view_id) {
169+
if (views_.empty()) {
170+
return nil;
171+
}
172+
return views_[view_id].get();
173+
}
174+
168175
std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
169176
std::vector<SkCanvas*> canvases;
170177
for (size_t i = 0; i < composition_order_.size(); i++) {

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ class FlutterPlatformViewsController {
5858

5959
void PrerollCompositeEmbeddedView(int view_id);
6060

61+
// Returns the `FlutterPlatformView` object associated with the view_id.
62+
//
63+
// If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or
64+
// a `FlutterPlatformView` object asscociated with the view_id cannot be found, the method returns
65+
// nil.
66+
NSObject<FlutterPlatformView>* GetPlatformViewByID(int view_id);
67+
6168
std::vector<SkCanvas*> GetCurrentCanvases();
6269

6370
SkCanvas* CompositeEmbeddedView(int view_id, const flow::EmbeddedViewParams& params);

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ namespace shell {
2727
class AccessibilityBridge;
2828
} // namespace shell
2929

30+
@class FlutterPlatformViewSemanticsContainer;
31+
3032
/**
3133
* A node in the iOS semantics tree.
3234
*/
@@ -71,6 +73,11 @@ class AccessibilityBridge;
7173
*/
7274
@property(nonatomic, strong) NSMutableArray<SemanticsObject*>* children;
7375

76+
/**
77+
* Used if this SemanticsObject is for a platform view.
78+
*/
79+
@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer;
80+
7481
- (BOOL)nodeWillCauseLayoutChange:(const blink::SemanticsNode*)node;
7582

7683
#pragma mark - Designated initializers
@@ -108,12 +115,31 @@ class AccessibilityBridge;
108115
@interface FlutterSemanticsObject : SemanticsObject
109116
@end
110117

118+
/**
119+
* Designated to act as an accessibility container of a platform view.
120+
*
121+
* This object does not take any accessibility actions on its own, nor has any accessibility
122+
* label/value/trait/hint... on its own. The accessibility data will be handled by the platform
123+
* view.
124+
*
125+
* See also:
126+
* * `SemanticsObject` for the other type of semantics objects.
127+
* * `FlutterSemanticsObject` for default implementation of `SemanticsObject`.
128+
*/
129+
@interface FlutterPlatformViewSemanticsContainer : UIAccessibilityElement
130+
131+
- (instancetype)init __attribute__((unavailable("Use initWithAccessibilityContainer: instead")));
132+
133+
@end
134+
111135
namespace shell {
112136
class PlatformViewIOS;
113137

114138
class AccessibilityBridge final {
115139
public:
116-
AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view);
140+
AccessibilityBridge(UIView* view,
141+
PlatformViewIOS* platform_view,
142+
FlutterPlatformViewsController* platform_views_controller);
117143
~AccessibilityBridge();
118144

119145
void UpdateSemantics(blink::SemanticsNodeUpdates nodes,
@@ -129,6 +155,10 @@ class AccessibilityBridge final {
129155

130156
fml::WeakPtr<AccessibilityBridge> GetWeakPtr();
131157

158+
FlutterPlatformViewsController* GetPlatformViewsController() const {
159+
return platform_views_controller_;
160+
};
161+
132162
void clearState();
133163

134164
private:
@@ -139,6 +169,7 @@ class AccessibilityBridge final {
139169

140170
UIView* view_;
141171
PlatformViewIOS* platform_view_;
172+
FlutterPlatformViewsController* platform_views_controller_;
142173
fml::scoped_nsobject<NSMutableDictionary<NSNumber*, SemanticsObject*>> objects_;
143174
fml::scoped_nsprotocol<FlutterBasicMessageChannel*> accessibility_channel_;
144175
fml::WeakPtrFactory<AccessibilityBridge> weak_factory_;

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

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#import <UIKit/UIKit.h>
1212

1313
#include "flutter/fml/logging.h"
14+
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
1415
#include "flutter/shell/platform/darwin/ios/platform_view_ios.h"
1516

1617
namespace {
@@ -127,6 +128,7 @@ - (void)dealloc {
127128
[_children release];
128129
_parent = nil;
129130
_container.get().semanticsObject = nil;
131+
[_platformViewSemanticsContainer release];
130132
[super dealloc];
131133
}
132134

@@ -152,6 +154,9 @@ - (BOOL)nodeWillCauseScroll:(const blink::SemanticsNode*)node {
152154
}
153155

154156
- (BOOL)hasChildren {
157+
if (_node.IsPlatformViewNode()) {
158+
return YES;
159+
}
155160
return [self.children count] != 0;
156161
}
157162

@@ -165,6 +170,7 @@ - (BOOL)isAccessibilityElement {
165170
// We enforce in the framework that no other useful semantics are merged with these nodes.
166171
if ([self node].HasFlag(blink::SemanticsFlags::kScopesRoute))
167172
return false;
173+
168174
return ([self node].flags != 0 &&
169175
[self node].flags != static_cast<int32_t>(blink::SemanticsFlags::kIsHidden)) ||
170176
![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() ||
@@ -396,6 +402,25 @@ - (UIAccessibilityTraits)accessibilityTraits {
396402

397403
@end
398404

405+
@implementation FlutterPlatformViewSemanticsContainer
406+
407+
// Method declared as unavailable in the interface
408+
- (instancetype)init {
409+
[self release];
410+
[super doesNotRecognizeSelector:_cmd];
411+
return nil;
412+
}
413+
414+
- (instancetype)initWithAccessibilityContainer:(id)container {
415+
FML_CHECK(container);
416+
if (self = [super initWithAccessibilityContainer:container]) {
417+
self.isAccessibilityElement = NO;
418+
}
419+
return self;
420+
}
421+
422+
@end
423+
399424
@implementation SemanticsObjectContainer {
400425
SemanticsObject* _semanticsObject;
401426
fml::WeakPtr<shell::AccessibilityBridge> _bridge;
@@ -426,7 +451,12 @@ - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
426451
#pragma mark - UIAccessibilityContainer overrides
427452

428453
- (NSInteger)accessibilityElementCount {
429-
return [[_semanticsObject children] count] + 1;
454+
NSInteger count = [[_semanticsObject children] count] + 1;
455+
// Need to create an additional child that acts as accessibility container for the platform view.
456+
if (_semanticsObject.node.IsPlatformViewNode()) {
457+
count++;
458+
}
459+
return count;
430460
}
431461

432462
- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
@@ -435,7 +465,16 @@ - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
435465
if (index == 0) {
436466
return _semanticsObject;
437467
}
468+
469+
// Return the additional child acts as a container of platform view. The
470+
// platformViewSemanticsContainer was created and cached in the updateSemantics path.
471+
if (_semanticsObject.node.IsPlatformViewNode() && index == [self accessibilityElementCount] - 1) {
472+
FML_CHECK(_semanticsObject.platformViewSemanticsContainer != nil);
473+
return _semanticsObject.platformViewSemanticsContainer;
474+
}
475+
438476
SemanticsObject* child = [_semanticsObject children][index - 1];
477+
439478
if ([child hasChildren])
440479
return [child accessibilityContainer];
441480
return child;
@@ -444,6 +483,12 @@ - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
444483
- (NSInteger)indexOfAccessibilityElement:(id)element {
445484
if (element == _semanticsObject)
446485
return 0;
486+
487+
// FlutterPlatformViewSemanticsContainer is always the last element of its parent.
488+
if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
489+
return [self accessibilityElementCount] - 1;
490+
}
491+
447492
NSMutableArray<SemanticsObject*>* children = [_semanticsObject children];
448493
for (size_t i = 0; i < [children count]; i++) {
449494
SemanticsObject* child = children[i];
@@ -485,9 +530,12 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
485530

486531
namespace shell {
487532

488-
AccessibilityBridge::AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view)
533+
AccessibilityBridge::AccessibilityBridge(UIView* view,
534+
PlatformViewIOS* platform_view,
535+
FlutterPlatformViewsController* platform_views_controller)
489536
: view_(view),
490537
platform_view_(platform_view),
538+
platform_views_controller_(platform_views_controller),
491539
objects_([[NSMutableDictionary alloc] init]),
492540
weak_factory_(this),
493541
previous_route_id_(0),
@@ -525,7 +573,7 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
525573
layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
526574
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
527575
[object setSemanticsNode:&node];
528-
const NSUInteger newChildCount = node.childrenInTraversalOrder.size();
576+
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
529577
NSMutableArray* newChildren =
530578
[[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
531579
for (NSUInteger i = 0; i < newChildCount; ++i) {
@@ -555,6 +603,20 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
555603
}
556604
object.accessibilityCustomActions = accessibilityCustomActions;
557605
}
606+
607+
if (object.node.IsPlatformViewNode()) {
608+
shell::FlutterPlatformViewsController* controller = GetPlatformViewsController();
609+
if (controller) {
610+
object.platformViewSemanticsContainer = [[FlutterPlatformViewSemanticsContainer alloc]
611+
initWithAccessibilityContainer:[object accessibilityContainer]];
612+
UIView* platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view];
613+
if (platformView) {
614+
object.platformViewSemanticsContainer.accessibilityElements = @[ platformView ];
615+
}
616+
}
617+
} else if (object.platformViewSemanticsContainer) {
618+
[object.platformViewSemanticsContainer release];
619+
}
558620
}
559621

560622
SemanticsObject* root = objects_.get()[@(kRootNodeId)];

shell/platform/darwin/ios/platform_view_ios.mm

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050

5151
if (accessibility_bridge_) {
5252
accessibility_bridge_.reset(
53-
new AccessibilityBridge(static_cast<FlutterView*>(owner_controller_.get().view), this));
53+
new AccessibilityBridge(static_cast<FlutterView*>(owner_controller_.get().view), this,
54+
[owner_controller.get() platformViewsController]));
5455
}
5556
// Do not call `NotifyCreated()` here - let FlutterViewController take care
5657
// of that when its Viewport is sized. If `NotifyCreated()` is called here,
@@ -96,7 +97,8 @@
9697
}
9798
if (enabled && !accessibility_bridge_) {
9899
accessibility_bridge_ = std::make_unique<AccessibilityBridge>(
99-
static_cast<FlutterView*>(owner_controller_.get().view), this);
100+
static_cast<FlutterView*>(owner_controller_.get().view), this,
101+
[owner_controller_.get() platformViewsController]);
100102
} else if (!enabled && accessibility_bridge_) {
101103
accessibility_bridge_.reset();
102104
}

0 commit comments

Comments
 (0)