mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add focus support for platform view (flutter/engine#33093)
This commit is contained in:
parent
13731ca4e3
commit
5bbb2437e2
@ -991,6 +991,36 @@ static void SetEntryPoint(flutter::Settings* settings, NSString* entrypoint, NSS
|
||||
arguments:@[ @(client) ]];
|
||||
}
|
||||
|
||||
- (void)flutterTextInputViewDidResignFirstResponder:(FlutterTextInputView*)textInputView {
|
||||
// Platform view's first responder detection logic:
|
||||
//
|
||||
// All text input widgets (e.g. EditableText) are backed by a dummy UITextInput view
|
||||
// in the TextInputPlugin. When this dummy UITextInput view resigns first responder,
|
||||
// check if any platform view becomes first responder. If any platform view becomes
|
||||
// first responder, send a "viewFocused" channel message to inform the framework to un-focus
|
||||
// the previously focused text input.
|
||||
//
|
||||
// Caveat:
|
||||
// 1. This detection logic does not cover the scenario when a platform view becomes
|
||||
// first responder without any flutter text input resigning its first responder status
|
||||
// (e.g. user tapping on platform view first). For now it works fine because the TextInputPlugin
|
||||
// does not track the focused platform view id (which is different from Android implementation).
|
||||
//
|
||||
// 2. This detection logic assumes that all text input widgets are backed by a dummy
|
||||
// UITextInput view in the TextInputPlugin, which may not hold true in the future.
|
||||
|
||||
// Have to check in the next run loop, because iOS requests the previous first responder to
|
||||
// resign before requesting the next view to become first responder.
|
||||
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||
long platform_view_id = self.platformViewsController->FindFirstResponderPlatformViewId();
|
||||
if (platform_view_id == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_platformViewsChannel.get() invokeMethod:@"viewFocused" arguments:@(platform_view_id)];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Undo Manager Delegate
|
||||
|
||||
- (void)flutterUndoManagerPlugin:(FlutterUndoManagerPlugin*)undoManagerPlugin
|
||||
|
||||
@ -19,6 +19,20 @@
|
||||
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
|
||||
#import "flutter/shell/platform/darwin/ios/ios_surface_gl.h"
|
||||
|
||||
@implementation UIView (FirstResponder)
|
||||
- (BOOL)flt_hasFirstResponderInViewHierarchySubtree {
|
||||
if (self.isFirstResponder) {
|
||||
return YES;
|
||||
}
|
||||
for (UIView* subview in self.subviews) {
|
||||
if (subview.flt_hasFirstResponderInViewHierarchySubtree) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
@end
|
||||
|
||||
namespace flutter {
|
||||
|
||||
std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewLayerPool::GetLayer(
|
||||
@ -328,6 +342,15 @@ UIView* FlutterPlatformViewsController::GetPlatformViewByID(int view_id) {
|
||||
return [touch_interceptors_[view_id].get() embeddedView];
|
||||
}
|
||||
|
||||
long FlutterPlatformViewsController::FindFirstResponderPlatformViewId() {
|
||||
for (auto const& [id, root_view] : root_views_) {
|
||||
if ((UIView*)(root_view.get()).flt_hasFirstResponderInViewHierarchySubtree) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
|
||||
std::vector<SkCanvas*> canvases;
|
||||
for (size_t i = 0; i < composition_order_.size(); i++) {
|
||||
|
||||
@ -1105,4 +1105,36 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
|
||||
return pixel[3];
|
||||
}
|
||||
|
||||
- (void)testHasFirstResponderInViewHierarchySubtree_viewItselfBecomesFirstResponder {
|
||||
// For view to become the first responder, it must be a descendant of a UIWindow
|
||||
UIWindow* window = [[UIWindow alloc] init];
|
||||
UITextField* textField = [[UITextField alloc] init];
|
||||
[window addSubview:textField];
|
||||
|
||||
[textField becomeFirstResponder];
|
||||
XCTAssertTrue(textField.isFirstResponder);
|
||||
XCTAssertTrue(textField.flt_hasFirstResponderInViewHierarchySubtree);
|
||||
[textField resignFirstResponder];
|
||||
XCTAssertFalse(textField.isFirstResponder);
|
||||
XCTAssertFalse(textField.flt_hasFirstResponderInViewHierarchySubtree);
|
||||
}
|
||||
|
||||
- (void)testHasFirstResponderInViewHierarchySubtree_descendantViewBecomesFirstResponder {
|
||||
// For view to become the first responder, it must be a descendant of a UIWindow
|
||||
UIWindow* window = [[UIWindow alloc] init];
|
||||
UIView* view = [[UIView alloc] init];
|
||||
UIView* childView = [[UIView alloc] init];
|
||||
UITextField* textField = [[UITextField alloc] init];
|
||||
[window addSubview:view];
|
||||
[view addSubview:childView];
|
||||
[childView addSubview:textField];
|
||||
|
||||
[textField becomeFirstResponder];
|
||||
XCTAssertTrue(textField.isFirstResponder);
|
||||
XCTAssertTrue(view.flt_hasFirstResponderInViewHierarchySubtree);
|
||||
[textField resignFirstResponder];
|
||||
XCTAssertFalse(textField.isFirstResponder);
|
||||
XCTAssertFalse(view.flt_hasFirstResponderInViewHierarchySubtree);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@ -177,6 +177,10 @@ class FlutterPlatformViewsController {
|
||||
|
||||
void OnMethodCall(FlutterMethodCall* call, FlutterResult& result);
|
||||
|
||||
// Returns the platform view id if the platform view (or any of its descendant view) is the first
|
||||
// responder. Returns -1 if no such platform view is found.
|
||||
long FindFirstResponderPlatformViewId();
|
||||
|
||||
private:
|
||||
static const size_t kMaxLayerAllocations = 2;
|
||||
|
||||
@ -329,4 +333,9 @@ class FlutterPlatformViewsController {
|
||||
- (UIView*)embeddedView;
|
||||
@end
|
||||
|
||||
@interface UIView (FirstResponder)
|
||||
// Returns YES if a view or any of its descendant view is the first responder. Returns NO otherwise.
|
||||
@property(nonatomic, readonly) BOOL flt_hasFirstResponderInViewHierarchySubtree;
|
||||
@end
|
||||
|
||||
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_
|
||||
|
||||
@ -59,6 +59,7 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) {
|
||||
insertTextPlaceholderWithSize:(CGSize)size
|
||||
withClient:(int)client;
|
||||
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView removeTextPlaceholder:(int)client;
|
||||
- (void)flutterTextInputViewDidResignFirstResponder:(FlutterTextInputView*)textInputView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@ -39,6 +39,7 @@ const CGRect kSpacePanBounds = {{-2500, -2500}, {5000, 5000}};
|
||||
static NSString* const kShowMethod = @"TextInput.show";
|
||||
static NSString* const kHideMethod = @"TextInput.hide";
|
||||
static NSString* const kSetClientMethod = @"TextInput.setClient";
|
||||
static NSString* const kSetPlatformViewClientMethod = @"TextInput.setPlatformViewClient";
|
||||
static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
|
||||
static NSString* const kClearClientMethod = @"TextInput.clearClient";
|
||||
static NSString* const kSetEditableSizeAndTransformMethod =
|
||||
@ -1075,6 +1076,14 @@ static BOOL IsSelectionRectCloserToPoint(CGPoint point,
|
||||
return _textInputClient != 0;
|
||||
}
|
||||
|
||||
- (BOOL)resignFirstResponder {
|
||||
BOOL success = [super resignFirstResponder];
|
||||
if (success) {
|
||||
[self.textInputDelegate flutterTextInputViewDidResignFirstResponder:self];
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
// When scribble is available, the FlutterTextInputView will display the native toolbar unless
|
||||
// these text editing actions are disabled.
|
||||
@ -2071,6 +2080,10 @@ static BOOL IsSelectionRectCloserToPoint(CGPoint point,
|
||||
} else if ([method isEqualToString:kSetClientMethod]) {
|
||||
[self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
|
||||
result(nil);
|
||||
} else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
|
||||
// This method call has a `platformViewId` argument, but we do not need it for iOS for now.
|
||||
[self setPlatformViewTextInputClient];
|
||||
result(nil);
|
||||
} else if ([method isEqualToString:kSetEditingStateMethod]) {
|
||||
[self setTextInputEditingState:args];
|
||||
result(nil);
|
||||
@ -2187,6 +2200,16 @@ static BOOL IsSelectionRectCloserToPoint(CGPoint point,
|
||||
[self addToInputParentViewIfNeeded:_activeView];
|
||||
}
|
||||
|
||||
- (void)setPlatformViewTextInputClient {
|
||||
// No need to track the platformViewID (unlike in Android). When a platform view
|
||||
// becomes the first responder, simply hide this dummy text input view (`_activeView`)
|
||||
// for the previously focused widget.
|
||||
[self removeEnableFlutterTextInputViewAccessibilityTimer];
|
||||
_activeView.accessibilityEnabled = NO;
|
||||
[_activeView removeFromSuperview];
|
||||
[_inputHider removeFromSuperview];
|
||||
}
|
||||
|
||||
- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
|
||||
[self resetAllClientIds];
|
||||
// Hide all input views from autofill, only make those in the new configuration visible
|
||||
|
||||
@ -88,7 +88,7 @@ FLUTTER_ASSERT_ARC
|
||||
|
||||
textInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
|
||||
|
||||
viewController = [FlutterViewController new];
|
||||
viewController = [[FlutterViewController alloc] init];
|
||||
textInputPlugin.viewController = viewController;
|
||||
|
||||
// Clear pasteboard between tests.
|
||||
@ -167,7 +167,7 @@ FLUTTER_ASSERT_ARC
|
||||
#pragma mark - Tests
|
||||
- (void)testNoDanglingEnginePointer {
|
||||
__weak FlutterTextInputPlugin* weakFlutterTextInputPlugin;
|
||||
FlutterViewController* flutterViewController = [FlutterViewController new];
|
||||
FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
|
||||
__weak FlutterEngine* weakFlutterEngine;
|
||||
|
||||
FlutterTextInputView* currentView;
|
||||
@ -1825,7 +1825,7 @@ FLUTTER_ASSERT_ARC
|
||||
}
|
||||
|
||||
- (void)testFlutterTextInputPluginRetainsFlutterTextInputView {
|
||||
FlutterViewController* flutterViewController = [FlutterViewController new];
|
||||
FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
|
||||
FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
|
||||
myInputPlugin.viewController = flutterViewController;
|
||||
|
||||
@ -1858,7 +1858,7 @@ FLUTTER_ASSERT_ARC
|
||||
}
|
||||
|
||||
- (void)testFlutterTextInputPluginHostViewNotNil {
|
||||
FlutterViewController* flutterViewController = [FlutterViewController new];
|
||||
FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
|
||||
FlutterEngine* flutterEngine = [[FlutterEngine alloc] init];
|
||||
[flutterEngine runWithEntrypoint:nil];
|
||||
flutterEngine.viewController = flutterViewController;
|
||||
@ -1866,4 +1866,26 @@ FLUTTER_ASSERT_ARC
|
||||
XCTAssertNotNil([flutterEngine.textInputPlugin hostView]);
|
||||
}
|
||||
|
||||
- (void)testSetPlatformViewClient {
|
||||
FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
|
||||
FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];
|
||||
myInputPlugin.viewController = flutterViewController;
|
||||
|
||||
FlutterMethodCall* setClientCall = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"TextInput.setClient"
|
||||
arguments:@[ [NSNumber numberWithInt:123], self.mutablePasswordTemplateCopy ]];
|
||||
[myInputPlugin handleMethodCall:setClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
UIView* activeView = myInputPlugin.textInputView;
|
||||
XCTAssertNotNil(activeView.superview, @"activeView must be added to the view hierarchy.");
|
||||
FlutterMethodCall* setPlatformViewClientCall = [FlutterMethodCall
|
||||
methodCallWithMethodName:@"TextInput.setPlatformViewClient"
|
||||
arguments:@{@"platformViewId" : [NSNumber numberWithLong:456]}];
|
||||
[myInputPlugin handleMethodCall:setPlatformViewClientCall
|
||||
result:^(id _Nullable result){
|
||||
}];
|
||||
XCTAssertNil(activeView.superview, @"activeView must be removed from view hierarchy.");
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user