fix ios layout change cause the accessibility focus to jump randomly. (flutter/engine#20167)

* fix ios layout change cause the accessibility focus to jump randomly.

* format

* addressing review comments
This commit is contained in:
chunhtai 2020-08-05 16:32:59 -07:00 committed by GitHub
parent 9c268aa0c0
commit 3ea7cae3d3
6 changed files with 225 additions and 5 deletions

View File

@ -453,6 +453,7 @@ flutter::SemanticsAction GetSemanticsActionForScrollDirection(
- (void)accessibilityElementDidBecomeFocused {
if (![self isAccessibilityBridgeAlive])
return;
[self bridge]->AccessibilityFocusDidChange([self uid]);
if ([self node].HasFlag(flutter::SemanticsFlags::kIsHidden) ||
[self node].HasFlag(flutter::SemanticsFlags::kIsHeader)) {
[self bridge]->DispatchSemanticsAction([self uid], flutter::SemanticsAction::kShowOnScreen);

View File

@ -33,6 +33,7 @@ class MockAccessibilityBridge : public AccessibilityBridgeIos {
SemanticsActionObservation observation(id, action);
observations.push_back(observation);
}
void AccessibilityFocusDidChange(int32_t id) override {}
FlutterPlatformViewsController* GetPlatformViewsController() const override { return nil; }
std::vector<SemanticsActionObservation> observations;

View File

@ -61,6 +61,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
void DispatchSemanticsAction(int32_t id,
flutter::SemanticsAction action,
std::vector<uint8_t> args) override;
void AccessibilityFocusDidChange(int32_t id) override;
UIView<UITextInput>* textInputView() override;
@ -83,6 +84,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos {
FlutterViewController* view_controller_;
PlatformViewIOS* platform_view_;
FlutterPlatformViewsController* platform_views_controller_;
int32_t last_focused_semantics_object_id_;
fml::scoped_nsobject<NSMutableDictionary<NSNumber*, SemanticsObject*>> objects_;
fml::scoped_nsprotocol<FlutterBasicMessageChannel*> accessibility_channel_;
fml::WeakPtrFactory<AccessibilityBridge> weak_factory_;

View File

@ -41,6 +41,7 @@ AccessibilityBridge::AccessibilityBridge(FlutterViewController* view_controller,
: view_controller_(view_controller),
platform_view_(platform_view),
platform_views_controller_(platform_views_controller),
last_focused_semantics_object_id_(0),
objects_([[NSMutableDictionary alloc] init]),
weak_factory_(this),
previous_route_id_(0),
@ -66,6 +67,10 @@ UIView<UITextInput>* AccessibilityBridge::textInputView() {
return [[platform_view_->GetOwnerViewController().get().engine textInputPlugin] textInputView];
}
void AccessibilityBridge::AccessibilityFocusDidChange(int32_t id) {
last_focused_semantics_object_id_ = id;
}
void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
flutter::CustomAccessibilityActionUpdates actions) {
BOOL layoutChanged = NO;
@ -188,12 +193,16 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
[lastAdded routeFocusObject]);
}
} else if (layoutChanged) {
// TODO(goderbauer): figure out which node to focus next.
ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil);
// Tries to refocus the previous focused semantics object to avoid random jumps.
ios_delegate_->PostAccessibilityNotification(
UIAccessibilityLayoutChangedNotification,
[objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
}
if (scrollOccured) {
// TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5")
ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @"");
// Tries to refocus the previous focused semantics object to avoid random jumps.
ios_delegate_->PostAccessibilityNotification(
UIAccessibilityPageScrolledNotification,
[objects_.get() objectForKey:@(last_focused_semantics_object_id_)]);
}
}

View File

@ -24,6 +24,13 @@ class AccessibilityBridgeIos {
virtual void DispatchSemanticsAction(int32_t id,
flutter::SemanticsAction action,
std::vector<uint8_t> args) = 0;
/**
* A callback that is called after the accessibility focus has moved to a new
* SemanticObject.
*
* The input id is the uid of the newly focused SemanticObject.
*/
virtual void AccessibilityFocusDidChange(int32_t id) = 0;
virtual FlutterPlatformViewsController* GetPlatformViewsController() const = 0;
};

View File

@ -306,7 +306,6 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
flutter::SemanticsNode route_node;
route_node.id = 1;
route_node.label = label;
route_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
route_node.label = "route";
@ -327,6 +326,207 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesLayoutChangeWithNilIfLastFocusIsRemoved {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode route_node;
route_node.id = 1;
route_node.label = "route";
first_update[route_node.id] = route_node;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
// Simulates the focusing on the node 1.
bridge->AccessibilityFocusDidChange(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the removal of the node 1
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
second_update[root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
NSNull* focusObject = accessibility_notifications[0][@"argument"];
// The node 1 was removed, so the bridge will set the focus object to nil.
XCTAssertEqual(focusObject, [NSNull null]);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesLayoutChangeWithLastFocused {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
first_update[node_one.id] = node_one;
flutter::SemanticsNode node_two;
node_two.id = 2;
node_two.label = "route2";
first_update[node_two.id] = node_two;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1, 2};
root_node.childrenInHitTestOrder = {1, 2};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 0ul);
// Simulates the focusing on the node 1.
bridge->AccessibilityFocusDidChange(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the removal of the node 2.
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.label = "root";
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
second_update[root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Since we have focused on the node 1 right before the layout changed, the bridge should refocus
// the node 1.
XCTAssertEqual([focusObject uid], 1);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityLayoutChangedNotification);
}
- (void)testAnnouncesScrollChangeWithLastFocused {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view=*/mockFlutterView,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates first_update;
flutter::SemanticsNode node_one;
node_one.id = 1;
node_one.label = "route1";
node_one.scrollPosition = 0.0;
first_update[node_one.id] = node_one;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.label = "root";
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
first_update[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/first_update, /*actions=*/actions);
// The first update will trigger a scroll announcement, but we are not interested in it.
[accessibility_notifications removeAllObjects];
// Simulates the focusing on the node 1.
bridge->AccessibilityFocusDidChange(1);
flutter::SemanticsNodeUpdates second_update;
// Simulates the scrolling on the node 1.
flutter::SemanticsNode new_node_one;
new_node_one.id = 1;
new_node_one.label = "route1";
new_node_one.scrollPosition = 1.0;
second_update[new_node_one.id] = new_node_one;
bridge->UpdateSemantics(/*nodes=*/second_update, /*actions=*/actions);
SemanticsObject* focusObject = accessibility_notifications[0][@"argument"];
// Since we have focused on the node 1 right before the scrolling, the bridge should refocus the
// node 1.
XCTAssertEqual([focusObject uid], 1);
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityPageScrolledNotification);
}
- (void)testAnnouncesIgnoresRouteChangesWhenModal {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");