mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
9c268aa0c0
commit
3ea7cae3d3
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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_;
|
||||
|
||||
@ -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_)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user