未验证 提交 b3a18f51 编写于 作者: C chunhtai 提交者: GitHub

unhide uitextinput when focused (#23776)

上级 b9785159
......@@ -71,6 +71,7 @@ FLUTTER_DARWIN_EXPORT
@property(nonatomic, copy) UITextContentType textContentType API_AVAILABLE(ios(10.0));
@property(nonatomic, assign) id<FlutterTextInputDelegate> textInputDelegate;
@property(nonatomic, assign) UIAccessibilityElement* backingTextInputAccessibilityObject;
@end
#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTPLUGIN_H_
......@@ -7,10 +7,14 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#include "flutter/fml/logging.h"
#include "flutter/fml/platform/darwin/string_range_sanitization.h"
static const char _kTextAffinityDownstream[] = "TextAffinity.downstream";
static const char _kTextAffinityUpstream[] = "TextAffinity.upstream";
// A delay before enabling the accessibility of FlutterTextInputView after
// it is activated.
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
// The "canonical" invalid CGRect, similar to CGRectNull, used to
// indicate a CGRect involved in firstRectForRange calculation is
......@@ -424,6 +428,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
@property(nonatomic, readonly) CATransform3D editableTransform;
@property(nonatomic, assign) CGRect markedRect;
@property(nonatomic) BOOL isVisibleToAutofill;
@property(nonatomic, assign) BOOL accessibilityEnabled;
- (void)setEditableTransform:(NSArray*)matrix;
@end
......@@ -462,6 +467,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
_keyboardType = UIKeyboardTypeDefault;
_returnKeyType = UIReturnKeyDone;
_secureTextEntry = NO;
_accessibilityEnabled = NO;
if (@available(iOS 11.0, *)) {
_smartQuotesType = UITextSmartQuotesTypeYes;
_smartDashesType = UITextSmartDashesTypeYes;
......@@ -1106,16 +1112,52 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
[self replaceRange:_selectedTextRange withText:@""];
}
- (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
UIAccessibilityPostNotification(notification, target);
}
- (void)accessibilityElementDidBecomeFocused {
if ([self accessibilityElementIsFocused]) {
// For most of the cases, this flutter text input view should never
// receive the focus. If we do receive the focus, we make the best effort
// to send the focus back to the real text field.
FML_DCHECK(_backingTextInputAccessibilityObject);
[self postAccessibilityNotification:UIAccessibilityScreenChangedNotification
target:_backingTextInputAccessibilityObject];
}
}
- (BOOL)accessibilityElementsHidden {
return !_accessibilityEnabled;
}
@end
/**
* Hides `FlutterTextInputView` from iOS accessibility system so it
* does not show up twice, once where it is in the `UIView` hierarchy,
* and a second time as part of the `SemanticsObject` hierarchy.
*
* This prevents the `FlutterTextInputView` from receiving the focus
* due to swipping gesture.
*
* There are other cases the `FlutterTextInputView` may receive
* focus. One example is during screen changes, the accessibility
* tree will undergo a dramatic structural update. The Voiceover may
* decide to focus the `FlutterTextInputView` that is not involved
* in the structural update instead. If that happens, the
* `FlutterTextInputView` will make a best effort to direct the
* focus back to the `SemanticsObject`.
*/
@interface FlutterTextInputViewAccessibilityHider : UIView {
}
@end
@implementation FlutterTextInputViewAccessibilityHider {
}
- (BOOL)accessibilityElementsHidden {
// We are hiding this accessibility element.
// There are 2 accessible elements involved in text entry in 2 different parts of the view
// hierarchy. This `FlutterTextInputView` is injected at the top of key window. We use this as a
// `UITextInput` protocol to bridge text edit events between Flutter and iOS.
//
// We also create ur own custom `UIAccessibilityElements` tree with our `SemanticsObject` to
// mimic the semantics tree from Flutter. We want the text field to be represented as a
// `TextInputSemanticsObject` in that `SemanticsObject` tree rather than in this
// `FlutterTextInputView` bridge which doesn't appear above a text field from the Flutter side.
return YES;
}
......@@ -1128,9 +1170,12 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
@property(nonatomic, readonly)
NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
@property(nonatomic, assign) FlutterTextInputView* activeView;
@property(nonatomic, strong) FlutterTextInputViewAccessibilityHider* inputHider;
@end
@implementation FlutterTextInputPlugin
@implementation FlutterTextInputPlugin {
NSTimer* _enableFlutterTextInputViewAccessibilityTimer;
}
@synthesize textInputDelegate = _textInputDelegate;
......@@ -1142,6 +1187,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
_reusableInputView.secureTextEntry = NO;
_autofillContext = [[NSMutableDictionary alloc] init];
_activeView = _reusableInputView;
_inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init];
}
return self;
......@@ -1150,11 +1196,19 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
- (void)dealloc {
[self hideTextInput];
[_reusableInputView release];
[_inputHider release];
[_autofillContext release];
[super dealloc];
}
- (void)removeEnableFlutterTextInputViewAccessibilityTimer {
if (_enableFlutterTextInputViewAccessibilityTimer) {
[_enableFlutterTextInputViewAccessibilityTimer invalidate];
[_enableFlutterTextInputViewAccessibilityTimer release];
_enableFlutterTextInputViewAccessibilityTimer = nil;
}
}
- (UIView<UITextInput>*)textInputView {
return _activeView;
}
......@@ -1207,11 +1261,38 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
- (void)showTextInput {
_activeView.textInputDelegate = _textInputDelegate;
[self addToInputParentViewIfNeeded:_activeView];
// Adds a delay to prevent the text view from receiving accessibility
// focus in case it is activated during semantics updates.
//
// One common case is when the app navigates to a page with an auto
// focused text field. The text field will activate the FlutterTextInputView
// with a semantics update sent to the engine. The voiceover will focus
// the newly attached active view while performing accessibility update.
// This results in accessibility focus stuck at the FlutterTextInputView.
if (!_enableFlutterTextInputViewAccessibilityTimer) {
_enableFlutterTextInputViewAccessibilityTimer =
[[NSTimer scheduledTimerWithTimeInterval:kUITextInputAccessibilityEnablingDelaySeconds
target:self
selector:@selector(enableActiveViewAccessibility:)
userInfo:nil
repeats:NO] retain];
}
[_activeView becomeFirstResponder];
}
- (void)enableActiveViewAccessibility:(NSTimer*)time {
if (_activeView.isFirstResponder) {
_activeView.accessibilityEnabled = YES;
}
[self removeEnableFlutterTextInputViewAccessibilityTimer];
}
- (void)hideTextInput {
[self removeEnableFlutterTextInputViewAccessibilityTimer];
_activeView.accessibilityEnabled = NO;
[_activeView resignFirstResponder];
[_activeView removeFromSuperview];
[_inputHider removeFromSuperview];
}
- (void)triggerAutofillSave:(BOOL)saveEntries {
......@@ -1356,7 +1437,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
}
// The UIView to add FlutterTextInputViews to.
- (UIView*)textInputParentView {
- (UIView*)keyWindow {
UIWindow* keyWindow = [UIApplication sharedApplication].keyWindow;
NSAssert(keyWindow != nullptr,
@"The application must have a key window since the keyboard client "
......@@ -1364,12 +1445,17 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
return keyWindow;
}
// The UIView to add FlutterTextInputViews to.
- (NSArray<UIView*>*)textInputViews {
return _inputHider.subviews;
}
// Removes every installed input field, unless it's in the current autofill
// context. May remove the active view too if includeActiveView is YES.
// When clearText is YES, the text on the input fields will be set to empty before
// they are removed from the view hierarchy, to avoid triggering autofill save.
- (void)cleanUpViewHierarchy:(BOOL)includeActiveView clearText:(BOOL)clearText {
for (UIView* view in self.textInputParentView.subviews) {
for (UIView* view in self.textInputViews) {
if ([view isKindOfClass:[FlutterTextInputView class]] &&
(includeActiveView || view != _activeView)) {
FlutterTextInputView* inputView = (FlutterTextInputView*)view;
......@@ -1390,7 +1476,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
// Changes the visibility of every FlutterTextInputView currently in the
// view hierarchy.
- (void)changeInputViewsAutofillVisibility:(BOOL)newVisibility {
for (UIView* view in self.textInputParentView.subviews) {
for (UIView* view in self.textInputViews) {
if ([view isKindOfClass:[FlutterTextInputView class]]) {
FlutterTextInputView* inputView = (FlutterTextInputView*)view;
inputView.isVisibleToAutofill = newVisibility;
......@@ -1401,7 +1487,7 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
// Resets the client id of every FlutterTextInputView in the view hierarchy
// to 0. Called when a new text input connection will be established.
- (void)resetAllClientIds {
for (UIView* view in self.textInputParentView.subviews) {
for (UIView* view in self.textInputViews) {
if ([view isKindOfClass:[FlutterTextInputView class]]) {
FlutterTextInputView* inputView = (FlutterTextInputView*)view;
[inputView setTextInputClient:0];
......@@ -1410,9 +1496,12 @@ static FlutterAutofillType autofillTypeOf(NSDictionary* configuration) {
}
- (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView {
UIView* parentView = self.textInputParentView;
if (inputView.superview != parentView) {
[parentView addSubview:inputView];
if (![inputView isDescendantOfView:_inputHider]) {
[_inputHider addSubview:inputView];
}
UIView* parentView = self.keyWindow;
if (_inputHider.superview != parentView) {
[parentView addSubview:_inputHider];
}
}
......
......@@ -20,6 +20,30 @@ FLUTTER_ASSERT_ARC
- (void)setMarkedRect:(CGRect)markedRect;
- (void)updateEditingState;
- (BOOL)isVisibleToAutofill;
@end
@interface FlutterTextInputViewSpy : FlutterTextInputView
@property(nonatomic, assign) UIAccessibilityNotifications receivedNotification;
@property(nonatomic, assign) id receivedNotificationTarget;
@property(nonatomic, assign) BOOL isAccessibilityFocused;
- (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target;
@end
@implementation FlutterTextInputViewSpy {
}
- (void)postAccessibilityNotification:(UIAccessibilityNotifications)notification target:(id)target {
self.receivedNotification = notification;
self.receivedNotificationTarget = target;
}
- (BOOL)accessibilityElementIsFocused {
return _isAccessibilityFocused;
}
@end
@interface FlutterSecureTextInputView : FlutterTextInputView
......@@ -33,7 +57,7 @@ FLUTTER_ASSERT_ARC
NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
- (void)collectGarbageInputViews;
- (UIView*)textInputParentView;
- (NSArray<UIView*>*)textInputViews;
@end
@interface FlutterTextInputPluginTest : XCTestCase
......@@ -71,6 +95,22 @@ FLUTTER_ASSERT_ARC
}];
}
- (void)setTextInputShow {
FlutterMethodCall* setClientCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.show"
arguments:@[]];
[textInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];
}
- (void)setTextInputHide {
FlutterMethodCall* setClientCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.hide"
arguments:@[]];
[textInputPlugin handleMethodCall:setClientCall
result:^(id _Nullable result){
}];
}
- (NSMutableDictionary*)mutableTemplateCopy {
if (!_template) {
_template = @{
......@@ -88,7 +128,7 @@ FLUTTER_ASSERT_ARC
}
- (NSArray<FlutterTextInputView*>*)installedInputViews {
return [textInputPlugin.textInputParentView.subviews
return (NSArray<FlutterTextInputView*>*)[textInputPlugin.textInputViews
filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self isKindOfClass: %@",
[FlutterTextInputView class]]];
}
......@@ -743,4 +783,39 @@ FLUTTER_ASSERT_ARC
[self commitAutofillContextAndVerify];
}
#pragma mark - Accessibility - Tests
- (void)testUITextInputAccessibilityNotHiddenWhenShowed {
// Send show text input method call.
[self setTextInputShow];
// Find all the FlutterTextInputViews we created.
NSArray<FlutterTextInputView*>* inputFields = self.installedInputViews;
// The input view should not be hidden.
XCTAssertEqual([inputFields count], 1u);
// Send hide text input method call.
[self setTextInputHide];
inputFields = self.installedInputViews;
// The input view should be hidden.
XCTAssertEqual([inputFields count], 0u);
}
- (void)testFlutterTextInputViewDirectFocusToBackingTextInput {
FlutterTextInputViewSpy* inputView = [[FlutterTextInputViewSpy alloc] init];
inputView.textInputDelegate = engine;
UIView* container = [[UIView alloc] init];
UIAccessibilityElement* backing =
[[UIAccessibilityElement alloc] initWithAccessibilityContainer:container];
inputView.backingTextInputAccessibilityObject = backing;
// Simulate accessibility focus.
inputView.isAccessibilityFocused = YES;
[inputView accessibilityElementDidBecomeFocused];
XCTAssertEqual(inputView.receivedNotification, UIAccessibilityScreenChangedNotification);
XCTAssertEqual(inputView.receivedNotificationTarget, backing);
}
@end
......@@ -198,10 +198,14 @@ static const UIAccessibilityTraits UIAccessibilityTraitUndocumentedEmptyLine = 0
- (void)setSemanticsNode:(const flutter::SemanticsNode*)node {
[super setSemanticsNode:node];
_inactive_text_input.text = @(node->value.data());
FlutterTextInputView* textInput = (FlutterTextInputView*)[self bridge]->textInputView();
if ([self node].HasFlag(flutter::SemanticsFlags::kIsFocused)) {
textInput.backingTextInputAccessibilityObject = self;
// The text input view must have a non-trivial size for the accessibility
// system to send text editing events.
[self bridge]->textInputView().frame = CGRectMake(0.0, 0.0, 1.0, 1.0);
textInput.frame = CGRectMake(0.0, 0.0, 1.0, 1.0);
} else if (textInput.backingTextInputAccessibilityObject == self) {
textInput.backingTextInputAccessibilityObject = nil;
}
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册