Skip to content

Commit

Permalink
Add a11y support for embedded iOS platform view (#8156)
Browse files Browse the repository at this point in the history
Follow up the framework change in #29304.
Inject the accessibility element tree in the semantic node if the node is for platform views.

#29302
  • Loading branch information
Chris Yang committed Mar 26, 2019
1 parent f64ee01 commit fd7d7fa
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 6 deletions.
6 changes: 6 additions & 0 deletions lib/ui/semantics/semantics_node.cc
Expand Up @@ -8,6 +8,8 @@

namespace blink {

constexpr int32_t kMinPlatfromViewId = -1;

SemanticsNode::SemanticsNode() = default;

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

bool SemanticsNode::IsPlatformViewNode() const {
return platformViewId > kMinPlatfromViewId;
}

} // namespace blink
3 changes: 3 additions & 0 deletions lib/ui/semantics/semantics_node.h
Expand Up @@ -81,6 +81,9 @@ struct SemanticsNode {
bool HasAction(SemanticsAction action);
bool HasFlag(SemanticsFlags flag);

// Whether this node is for embeded platform views.
bool IsPlatformViewNode() const;

int32_t id = 0;
int32_t flags = 0;
int32_t actions = 0;
Expand Down
Expand Up @@ -165,6 +165,13 @@
composition_order_.push_back(view_id);
}

NSObject<FlutterPlatformView>* FlutterPlatformViewsController::GetPlatformViewByID(int view_id) {
if (views_.empty()) {
return nil;
}
return views_[view_id].get();
}

std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
std::vector<SkCanvas*> canvases;
for (size_t i = 0; i < composition_order_.size(); i++) {
Expand Down
Expand Up @@ -58,6 +58,13 @@ class FlutterPlatformViewsController {

void PrerollCompositeEmbeddedView(int view_id);

// Returns the `FlutterPlatformView` object associated with the view_id.
//
// If the `FlutterPlatformViewsController` does not contain any `FlutterPlatformView` object or
// a `FlutterPlatformView` object asscociated with the view_id cannot be found, the method returns
// nil.
NSObject<FlutterPlatformView>* GetPlatformViewByID(int view_id);

std::vector<SkCanvas*> GetCurrentCanvases();

SkCanvas* CompositeEmbeddedView(int view_id, const flow::EmbeddedViewParams& params);
Expand Down
Expand Up @@ -27,6 +27,8 @@ namespace shell {
class AccessibilityBridge;
} // namespace shell

@class FlutterPlatformViewSemanticsContainer;

/**
* A node in the iOS semantics tree.
*/
Expand Down Expand Up @@ -71,6 +73,11 @@ class AccessibilityBridge;
*/
@property(nonatomic, strong) NSMutableArray<SemanticsObject*>* children;

/**
* Used if this SemanticsObject is for a platform view.
*/
@property(strong, nonatomic) FlutterPlatformViewSemanticsContainer* platformViewSemanticsContainer;

- (BOOL)nodeWillCauseLayoutChange:(const blink::SemanticsNode*)node;

#pragma mark - Designated initializers
Expand Down Expand Up @@ -108,12 +115,31 @@ class AccessibilityBridge;
@interface FlutterSemanticsObject : SemanticsObject
@end

/**
* Designated to act as an accessibility container of a platform view.
*
* This object does not take any accessibility actions on its own, nor has any accessibility
* label/value/trait/hint... on its own. The accessibility data will be handled by the platform
* view.
*
* See also:
* * `SemanticsObject` for the other type of semantics objects.
* * `FlutterSemanticsObject` for default implementation of `SemanticsObject`.
*/
@interface FlutterPlatformViewSemanticsContainer : UIAccessibilityElement

- (instancetype)init __attribute__((unavailable("Use initWithAccessibilityContainer: instead")));

@end

namespace shell {
class PlatformViewIOS;

class AccessibilityBridge final {
public:
AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view);
AccessibilityBridge(UIView* view,
PlatformViewIOS* platform_view,
FlutterPlatformViewsController* platform_views_controller);
~AccessibilityBridge();

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

fml::WeakPtr<AccessibilityBridge> GetWeakPtr();

FlutterPlatformViewsController* GetPlatformViewsController() const {
return platform_views_controller_;
};

void clearState();

private:
Expand All @@ -139,6 +169,7 @@ class AccessibilityBridge final {

UIView* view_;
PlatformViewIOS* platform_view_;
FlutterPlatformViewsController* platform_views_controller_;
fml::scoped_nsobject<NSMutableDictionary<NSNumber*, SemanticsObject*>> objects_;
fml::scoped_nsprotocol<FlutterBasicMessageChannel*> accessibility_channel_;
fml::WeakPtrFactory<AccessibilityBridge> weak_factory_;
Expand Down
68 changes: 65 additions & 3 deletions shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm
Expand Up @@ -11,6 +11,7 @@
#import <UIKit/UIKit.h>

#include "flutter/fml/logging.h"
#include "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
#include "flutter/shell/platform/darwin/ios/platform_view_ios.h"

namespace {
Expand Down Expand Up @@ -127,6 +128,7 @@ - (void)dealloc {
[_children release];
_parent = nil;
_container.get().semanticsObject = nil;
[_platformViewSemanticsContainer release];
[super dealloc];
}

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

- (BOOL)hasChildren {
if (_node.IsPlatformViewNode()) {
return YES;
}
return [self.children count] != 0;
}

Expand All @@ -165,6 +170,7 @@ - (BOOL)isAccessibilityElement {
// We enforce in the framework that no other useful semantics are merged with these nodes.
if ([self node].HasFlag(blink::SemanticsFlags::kScopesRoute))
return false;

return ([self node].flags != 0 &&
[self node].flags != static_cast<int32_t>(blink::SemanticsFlags::kIsHidden)) ||
![self node].label.empty() || ![self node].value.empty() || ![self node].hint.empty() ||
Expand Down Expand Up @@ -396,6 +402,25 @@ - (UIAccessibilityTraits)accessibilityTraits {

@end

@implementation FlutterPlatformViewSemanticsContainer

// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}

- (instancetype)initWithAccessibilityContainer:(id)container {
FML_CHECK(container);
if (self = [super initWithAccessibilityContainer:container]) {
self.isAccessibilityElement = NO;
}
return self;
}

@end

@implementation SemanticsObjectContainer {
SemanticsObject* _semanticsObject;
fml::WeakPtr<shell::AccessibilityBridge> _bridge;
Expand Down Expand Up @@ -426,7 +451,12 @@ - (instancetype)initWithSemanticsObject:(SemanticsObject*)semanticsObject
#pragma mark - UIAccessibilityContainer overrides

- (NSInteger)accessibilityElementCount {
return [[_semanticsObject children] count] + 1;
NSInteger count = [[_semanticsObject children] count] + 1;
// Need to create an additional child that acts as accessibility container for the platform view.
if (_semanticsObject.node.IsPlatformViewNode()) {
count++;
}
return count;
}

- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
Expand All @@ -435,7 +465,16 @@ - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
if (index == 0) {
return _semanticsObject;
}

// Return the additional child acts as a container of platform view. The
// platformViewSemanticsContainer was created and cached in the updateSemantics path.
if (_semanticsObject.node.IsPlatformViewNode() && index == [self accessibilityElementCount] - 1) {
FML_CHECK(_semanticsObject.platformViewSemanticsContainer != nil);
return _semanticsObject.platformViewSemanticsContainer;
}

SemanticsObject* child = [_semanticsObject children][index - 1];

if ([child hasChildren])
return [child accessibilityContainer];
return child;
Expand All @@ -444,6 +483,12 @@ - (nullable id)accessibilityElementAtIndex:(NSInteger)index {
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == _semanticsObject)
return 0;

// FlutterPlatformViewSemanticsContainer is always the last element of its parent.
if ([element isKindOfClass:[FlutterPlatformViewSemanticsContainer class]]) {
return [self accessibilityElementCount] - 1;
}

NSMutableArray<SemanticsObject*>* children = [_semanticsObject children];
for (size_t i = 0; i < [children count]; i++) {
SemanticsObject* child = children[i];
Expand Down Expand Up @@ -485,9 +530,12 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {

namespace shell {

AccessibilityBridge::AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view)
AccessibilityBridge::AccessibilityBridge(UIView* view,
PlatformViewIOS* platform_view,
FlutterPlatformViewsController* platform_views_controller)
: view_(view),
platform_view_(platform_view),
platform_views_controller_(platform_views_controller),
objects_([[NSMutableDictionary alloc] init]),
weak_factory_(this),
previous_route_id_(0),
Expand Down Expand Up @@ -525,7 +573,7 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
layoutChanged = layoutChanged || [object nodeWillCauseLayoutChange:&node];
scrollOccured = scrollOccured || [object nodeWillCauseScroll:&node];
[object setSemanticsNode:&node];
const NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSUInteger newChildCount = node.childrenInTraversalOrder.size();
NSMutableArray* newChildren =
[[[NSMutableArray alloc] initWithCapacity:newChildCount] autorelease];
for (NSUInteger i = 0; i < newChildCount; ++i) {
Expand Down Expand Up @@ -555,6 +603,20 @@ - (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction {
}
object.accessibilityCustomActions = accessibilityCustomActions;
}

if (object.node.IsPlatformViewNode()) {
shell::FlutterPlatformViewsController* controller = GetPlatformViewsController();
if (controller) {
object.platformViewSemanticsContainer = [[FlutterPlatformViewSemanticsContainer alloc]
initWithAccessibilityContainer:[object accessibilityContainer]];
UIView* platformView = [controller->GetPlatformViewByID(object.node.platformViewId) view];
if (platformView) {
object.platformViewSemanticsContainer.accessibilityElements = @[ platformView ];
}
}
} else if (object.platformViewSemanticsContainer) {
[object.platformViewSemanticsContainer release];
}
}

SemanticsObject* root = objects_.get()[@(kRootNodeId)];
Expand Down
6 changes: 4 additions & 2 deletions shell/platform/darwin/ios/platform_view_ios.mm
Expand Up @@ -50,7 +50,8 @@

if (accessibility_bridge_) {
accessibility_bridge_.reset(
new AccessibilityBridge(static_cast<FlutterView*>(owner_controller_.get().view), this));
new AccessibilityBridge(static_cast<FlutterView*>(owner_controller_.get().view), this,
[owner_controller.get() platformViewsController]));
}
// Do not call `NotifyCreated()` here - let FlutterViewController take care
// of that when its Viewport is sized. If `NotifyCreated()` is called here,
Expand Down Expand Up @@ -96,7 +97,8 @@
}
if (enabled && !accessibility_bridge_) {
accessibility_bridge_ = std::make_unique<AccessibilityBridge>(
static_cast<FlutterView*>(owner_controller_.get().view), this);
static_cast<FlutterView*>(owner_controller_.get().view), this,
[owner_controller_.get() platformViewsController]);
} else if (!enabled && accessibility_bridge_) {
accessibility_bridge_.reset();
}
Expand Down

0 comments on commit fd7d7fa

Please sign in to comment.