Move node implementation to Objective C (#2652)

* Move node implementation to Objective C

This changes AccessibilityBridge::Node (C++) to be AccessibilityNode (objc)
in order to have the nodes returned directly as ui accessibility elements.
This buys us a few things:

1) We can now return an element tree instead of a flat list, thus negating
   the need to maintain a global rect and global translation
2) We use less memory since we don't need to instantiate both nodes and
   their ensuing accessibility elements
3) accessibility elements can now implement the methods necessary to respond
   to events like user scrolling

https://github.com/flutter/flutter/issues/1664
This commit is contained in:
Todd Volkert 2016-05-06 11:26:01 -07:00
parent 7af714532d
commit fdc0b71889
2 changed files with 187 additions and 171 deletions

View File

@ -22,6 +22,36 @@
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/utils/SkMatrix44.h"
namespace sky {
namespace shell {
class AccessibilityBridge;
}
}
@interface AccessibilityNode : NSObject
/**
* The globally unique identifier for this node.
*/
@property(nonatomic, readonly) uint32_t uid;
/**
* The parent of this node in the node tree. Will be nil for the root node and
* during transient state changes.
*/
@property(nonatomic, readonly) AccessibilityNode* parent;
/**
* This node's children in the node tree.
*/
@property(nonatomic, readonly) NSArray<AccessibilityNode*>* children;
- (instancetype)init __attribute__((unavailable("Use initWithBridge instead")));
- (instancetype)initWithBridge:(sky::shell::AccessibilityBridge*)bridge
uid:(uint32_t)uid NS_DESIGNATED_INITIALIZER;
@end
namespace sky {
namespace shell {
@ -38,21 +68,18 @@ class AccessibilityBridge final : public semantics::SemanticsListener {
~AccessibilityBridge() override;
void UpdateSemanticsTree(mojo::Array<semantics::SemanticsNodePtr>) override;
AccessibilityNode* UpdateNode(const semantics::SemanticsNodePtr& node);
void RemoveNode(AccessibilityNode* node);
base::WeakPtr<AccessibilityBridge> AsWeakPtr();
FlutterView* getView() { return view_; }
private:
class Node;
scoped_refptr<Node> UpdateNode(const semantics::SemanticsNodePtr& node);
void RemoveNode(scoped_refptr<Node> node);
NSArray* CreateAccessibleElements() const NS_RETURNS_RETAINED;
// See class docs above about ownership relationship
FlutterView* view_;
semantics::SemanticsServerPtr semantics_server_;
std::map<long, scoped_refptr<Node>> nodes_;
NSMutableDictionary<NSNumber*, AccessibilityNode*>* nodes_;
mojo::StrongBinding<semantics::SemanticsListener> binding_;

View File

@ -4,15 +4,14 @@
#include "sky/shell/platform/ios/framework/Source/accessibility_bridge.h"
#include <UIKit/UIKit.h>
#import <UIKit/UIKit.h>
#include "mojo/public/cpp/application/connect.h"
namespace sky {
namespace shell {
namespace {
static const uint32_t RootNodeId = 0;
// Contains better abstractions than the raw Mojo data structure
struct Geometry {
Geometry& operator=(const semantics::SemanticGeometryPtr& other) {
@ -23,163 +22,163 @@ struct Geometry {
return *this;
}
SkMatrix44 transform;
SkMatrix44 transform =
SkMatrix44(SkMatrix44::Identity_Constructor::kIdentity_Constructor);
SkRect rect;
};
} // anonymous namespace
// Class that holds information about accessibility nodes, which are used
// to construct iOS accessibility elements
class AccessibilityBridge::Node final
: public base::RefCounted<AccessibilityBridge::Node> {
public:
static const uint32_t kUninitializedNodeId = -1;
@implementation AccessibilityNode {
sky::shell::AccessibilityBridge* _bridge;
Node(AccessibilityBridge*, const semantics::SemanticsNodePtr&);
void Update(const semantics::SemanticsNodePtr& node);
void PopulateAccessibleElements(NSMutableArray* accessibleElements);
uint32_t id_ = kUninitializedNodeId;
std::vector<scoped_refptr<Node>> children_;
Node* parent_ = nullptr;
private:
friend class base::RefCounted<Node>;
~Node();
void ValidateGlobalRect();
void ValidateGlobalTransform();
AccessibilityBridge* bridge_;
semantics::SemanticFlagsPtr flags_;
semantics::SemanticStringsPtr strings_;
Geometry geometry_;
std::unique_ptr<SkMatrix44> global_transform_;
std::unique_ptr<SkRect> global_rect_;
DISALLOW_COPY_AND_ASSIGN(Node);
};
AccessibilityBridge::Node::Node(AccessibilityBridge* bridge,
const semantics::SemanticsNodePtr& node)
: bridge_(bridge) {
Update(node);
semantics::SemanticFlagsPtr _flags;
semantics::SemanticStringsPtr _strings;
Geometry _geometry;
}
void AccessibilityBridge::Node::Update(
const semantics::SemanticsNodePtr& node) {
if (id_ == kUninitializedNodeId) {
id_ = node->id;
}
DCHECK(id_ == node->id);
#pragma mark - Override base class designated initializers
if (!node->flags.is_null()) {
flags_ = node->flags.Pass();
// Method declared as unavailable in the interface
- (instancetype)init {
[self release];
[super doesNotRecognizeSelector:_cmd];
return nil;
}
#pragma mark - Designated initializers
- (instancetype)initWithBridge:(sky::shell::AccessibilityBridge*)bridge
uid:(uint32_t)uid {
DCHECK(bridge != nil) << "bridge must be set";
DCHECK(uid >= RootNodeId);
self = [super init];
if (self) {
_bridge = bridge;
_uid = uid;
}
if (!node->strings.is_null()) {
strings_ = node->strings.Pass();
return self;
}
#pragma mark - Semantics node methods
- (void)update:(const semantics::SemanticsNodePtr&)mojoNode {
DCHECK(_uid == mojoNode->id);
if (!mojoNode->flags.is_null()) {
_flags = mojoNode->flags.Pass();
}
if (!node->geometry.is_null()) {
geometry_ = node->geometry.Pass();
if (!mojoNode->strings.is_null()) {
_strings = mojoNode->strings.Pass();
}
if (!node->children.is_null()) {
// Mark children for removal
for (scoped_refptr<Node> child : children_) {
DCHECK(child->parent_ != nullptr);
child->parent_ = nullptr;
if (!mojoNode->geometry.is_null()) {
_geometry = mojoNode->geometry;
}
if (!mojoNode->children.is_null()) {
// Mark existing children as orphans
NSArray* oldChildren = _children;
for (AccessibilityNode* child in oldChildren) {
DCHECK(child->_parent != nil);
child->_parent = nil;
}
// Set the new list of children
std::vector<scoped_refptr<Node>> children;
for (const semantics::SemanticsNodePtr& childNode : node->children) {
scoped_refptr<Node> child = bridge_->UpdateNode(childNode);
child->parent_ = this;
children.push_back(child);
NSMutableArray* children = [[NSMutableArray alloc] init];
_children = children;
for (const semantics::SemanticsNodePtr& mojoChild : mojoNode->children) {
AccessibilityNode* child = _bridge->UpdateNode(mojoChild);
child->_parent = self;
[children insertObject:child atIndex:0];
}
children.swap(children_);
// Remove those children that are still marked for removal
for (scoped_refptr<Node> child : children) {
if (child->parent_ == nullptr) {
bridge_->RemoveNode(child);
// Remove those children that are still marked as orphans
for (AccessibilityNode* child in oldChildren) {
if (child->_parent == nil) {
_bridge->RemoveNode(child);
}
}
}
global_transform_.release();
global_rect_.release();
}
void AccessibilityBridge::Node::ValidateGlobalTransform() {
if (global_transform_ != nullptr) {
return;
}
if (parent_ == nullptr) {
global_transform_.reset(new SkMatrix44(geometry_.transform));
} else {
parent_->ValidateGlobalTransform();
global_transform_.reset(
new SkMatrix44(geometry_.transform * *(parent_->global_transform_)));
[oldChildren release];
}
}
void AccessibilityBridge::Node::ValidateGlobalRect() {
if (global_rect_ != nullptr) {
return;
}
- (void)remove {
_parent = nil;
_bridge->RemoveNode(self);
}
ValidateGlobalTransform();
#pragma mark - UIAccessibility overrides
- (BOOL)isAccessibilityElement {
// Per UIAccessibilityContainer, accessibility containers MUST return
// NO to -isAccessibilityElement
return (_children.count == 0);
}
- (NSString*)accessibilityLabel {
return (_strings.is_null() || _strings->label.get().empty())
? nil
: @(_string->label.data());
}
- (UIAccessibilityTraits)accessibilityTraits {
// TODO(tvolkert): We need more semantic info in the mojom definition
// in order to distinguish buttons, links, sliders, etc.
return _flags->canBeTapped ? UIAccessibilityTraitButton
: UIAccessibilityTraitNone;
}
- (CGRect)accessibilityFrame {
SkPoint quad[4];
geometry_.rect.toQuad(quad);
_geometry.rect.toQuad(quad);
for (auto& point : quad) {
SkScalar vector[4] = {point.x(), point.y(), 0, 1};
global_transform_->mapScalars(vector);
_geometry.transform.mapScalars(vector);
point.set(vector[0], vector[1]);
}
global_rect_.reset(new SkRect());
global_rect_.get()->set(quad, 4);
SkRect result;
result.set(quad, 4);
return CGRectMake(result.x(), result.y(), result.width(), result.height());
}
void AccessibilityBridge::Node::PopulateAccessibleElements(
NSMutableArray* accessibleElements) {
if (!geometry_.rect.isEmpty()) {
UIAccessibilityElement* element = [[UIAccessibilityElement alloc]
initWithAccessibilityContainer:bridge_->view_];
element.isAccessibilityElement = YES;
ValidateGlobalRect();
element.accessibilityFrame =
CGRectMake(global_rect_->x(), global_rect_->y(), global_rect_->width(),
global_rect_->height());
if (flags_->canBeTapped) {
// TODO(tvolkert): What about links? We need semantic info in the mojom
// definition
element.accessibilityTraits = UIAccessibilityTraitButton;
}
if (!strings_->label.get().empty()) {
element.accessibilityLabel =
[NSString stringWithUTF8String:strings_->label.data()];
}
[accessibleElements insertObject:element atIndex:0];
[element release];
}
#pragma mark - UIAccessibilityElement protocol
for (scoped_refptr<Node> child : children_) {
child->PopulateAccessibleElements(accessibleElements);
}
- (id)accessibilityContainer {
return (_uid == RootNodeId) ? _bridge->getView() : _parent;
}
AccessibilityBridge::Node::~Node() {}
#pragma mark - UIAccessibilityContainer overrides
- (NSInteger)accessibilityElementCount {
return _children.count;
}
- (nullable id)accessibilityElementAtIndex:(NSInteger)index {
return (index < 0 || index >= (NSInteger)_children.count ? nil
: _children[index]);
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
return (_children == nil) ? NSNotFound : [_children indexOfObject:element];
}
#pragma mark - Misc
- (void)dealloc {
[_children release];
[super dealloc];
}
@end
#pragma mark - AccessibilityBridge impl
namespace sky {
namespace shell {
AccessibilityBridge::AccessibilityBridge(FlutterView* view,
mojo::ServiceProvider* serviceProvider)
@ -188,17 +187,19 @@ AccessibilityBridge::AccessibilityBridge(FlutterView* view,
mojo::InterfaceHandle<semantics::SemanticsListener> listener;
binding_.Bind(&listener);
semantics_server_->AddSemanticsListener(listener.Pass());
nodes_ = [[NSMutableDictionary alloc] init];
}
void AccessibilityBridge::UpdateSemanticsTree(
mojo::Array<semantics::SemanticsNodePtr> nodes) {
for (const semantics::SemanticsNodePtr& node : nodes) {
UpdateNode(node);
mojo::Array<semantics::SemanticsNodePtr> mojoNodes) {
for (const semantics::SemanticsNodePtr& mojoNode : mojoNodes) {
auto node = UpdateNode(mojoNode);
if (mojoNode->id == RootNodeId && view_.accessibilityElements == nil) {
view_.accessibilityElements = @[ node ];
}
}
NSArray* accessibleElements = CreateAccessibleElements();
view_.accessibilityElements = accessibleElements;
[accessibleElements release];
DCHECK(view_.accessibilityElements != nil);
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
nil);
@ -208,45 +209,33 @@ base::WeakPtr<AccessibilityBridge> AccessibilityBridge::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
scoped_refptr<AccessibilityBridge::Node> AccessibilityBridge::UpdateNode(
const semantics::SemanticsNodePtr& node) {
scoped_refptr<Node> persistentNode;
const auto& iter = nodes_.find(node->id);
if (iter == nodes_.end()) {
persistentNode = new Node(this, node);
nodes_[node->id] = persistentNode;
} else {
persistentNode = iter->second;
persistentNode->Update(node);
AccessibilityNode* AccessibilityBridge::UpdateNode(
const semantics::SemanticsNodePtr& mojoNode) {
AccessibilityNode* node = nodes_[@(mojoNode->id)];
if (node == nil) {
node = [[AccessibilityNode alloc] initWithBridge:this uid:mojoNode->id];
DCHECK(nodes_ != nil);
nodes_[@(mojoNode->id)] = node;
[node release];
}
DCHECK(persistentNode != nullptr);
return persistentNode;
[node update:mojoNode];
return node;
}
void AccessibilityBridge::RemoveNode(scoped_refptr<Node> node) {
DCHECK(nodes_.find(node->id_) != nodes_.end());
DCHECK(nodes_.at(node->id_)->parent_ == nullptr);
nodes_.erase(node->id_);
for (scoped_refptr<Node>& child : node->children_) {
child->parent_ = nullptr;
RemoveNode(child);
void AccessibilityBridge::RemoveNode(AccessibilityNode* node) {
[node retain];
DCHECK(nodes_[@(node.uid)] != nil);
DCHECK(nodes_[@(node.uid)].parent == nil);
[nodes_ removeObjectForKey:@(node.uid)];
for (AccessibilityNode* child in node.children) {
[child remove];
}
[node release];
}
NSArray* AccessibilityBridge::CreateAccessibleElements() const
NS_RETURNS_RETAINED {
NSMutableArray* accessibleElements = [[NSMutableArray alloc] init];
for (const auto& iter : nodes_) {
// TODO(tvolkert): There should only ever be 1 root. Keep a reference
// to it so we don't have to look for it here.
if (iter.second->parent_ == nullptr) {
iter.second->PopulateAccessibleElements(accessibleElements);
}
}
return accessibleElements;
AccessibilityBridge::~AccessibilityBridge() {
[nodes_ release];
}
AccessibilityBridge::~AccessibilityBridge() {}
} // namespace shell
} // namespace sky