Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 0df0bcf

Browse files
committed
[platform_view]add focus support for platform view
1 parent f91ccb4 commit 0df0bcf

6 files changed

+94
-0
lines changed

shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,17 @@ - (void)flutterTextInputView:(FlutterTextInputView*)textInputView
986986
arguments:@[ @(client) ]];
987987
}
988988

989+
- (void)flutterTextInputViewDidResignFirstResponder:(FlutterTextInputView*)textInputView {
990+
dispatch_async(dispatch_get_main_queue(), ^(void) {
991+
long platform_view_id = self.platformViewsController->findFirstResponderPlatformViewId();
992+
if (platform_view_id == -1) {
993+
return;
994+
}
995+
996+
[_platformViewsChannel.get() invokeMethod:@"viewFocused" arguments:@(platform_view_id)];
997+
});
998+
}
999+
9891000
#pragma mark - Undo Manager Delegate
9901001

9911002
- (void)flutterUndoManagerPlugin:(FlutterUndoManagerPlugin*)undoManagerPlugin

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@
1919
#import "flutter/shell/platform/darwin/ios/ios_surface.h"
2020
#import "flutter/shell/platform/darwin/ios/ios_surface_gl.h"
2121

22+
@implementation UIView (FirstResponder)
23+
- (BOOL)hasFirstResponderInViewHierarchy {
24+
if (self.isFirstResponder) {
25+
return YES;
26+
}
27+
for (UIView* subview in self.subviews) {
28+
if (subview.hasFirstResponderInViewHierarchy) {
29+
return YES;
30+
}
31+
}
32+
return NO;
33+
}
34+
@end
35+
2236
namespace flutter {
2337

2438
std::shared_ptr<FlutterPlatformViewLayer> FlutterPlatformViewLayerPool::GetLayer(
@@ -328,6 +342,15 @@
328342
return [touch_interceptors_[view_id].get() embeddedView];
329343
}
330344

345+
long FlutterPlatformViewsController::findFirstResponderPlatformViewId() {
346+
for (auto const& [id, root_view] : root_views_) {
347+
if ([(UIView*)root_view.get() hasFirstResponderInViewHierarchy]) {
348+
return id;
349+
}
350+
}
351+
return -1;
352+
}
353+
331354
std::vector<SkCanvas*> FlutterPlatformViewsController::GetCurrentCanvases() {
332355
std::vector<SkCanvas*> canvases;
333356
for (size_t i = 0; i < composition_order_.size(); i++) {

shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,4 +1105,36 @@ - (int)alphaOfPoint:(CGPoint)point onView:(UIView*)view {
11051105
return pixel[3];
11061106
}
11071107

1108+
- (void)testHasFirstResponderInViewHierarchy_viewItselfBecomesFristResponder {
1109+
// For view to become the first responder, it must be a descendant of a UIWindow
1110+
UIWindow* window = [[UIWindow alloc] init];
1111+
UITextField* textField = [[UITextField alloc] init];
1112+
[window addSubview:textField];
1113+
1114+
[textField becomeFirstResponder];
1115+
XCTAssertTrue(textField.isFirstResponder);
1116+
XCTAssertTrue(textField.hasFirstResponderInViewHierarchy);
1117+
[textField resignFirstResponder];
1118+
XCTAssertFalse(textField.isFirstResponder);
1119+
XCTAssertFalse(textField.hasFirstResponderInViewHierarchy);
1120+
}
1121+
1122+
- (void)testHasFirstResponderInViewHierarchy_descendantViewBecomesFirstResponder {
1123+
// For view to become the first responder, it must be a descendant of a UIWindow
1124+
UIWindow* window = [[UIWindow alloc] init];
1125+
UIView* view = [[UIView alloc] init];
1126+
UIView* childView = [[UIView alloc] init];
1127+
UITextField* textField = [[UITextField alloc] init];
1128+
[window addSubview:view];
1129+
[view addSubview:childView];
1130+
[childView addSubview:textField];
1131+
1132+
[textField becomeFirstResponder];
1133+
XCTAssertTrue(textField.isFirstResponder);
1134+
XCTAssertTrue(view.hasFirstResponderInViewHierarchy);
1135+
[textField resignFirstResponder];
1136+
XCTAssertFalse(textField.isFirstResponder);
1137+
XCTAssertFalse(view.hasFirstResponderInViewHierarchy);
1138+
}
1139+
11081140
@end

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ class FlutterPlatformViewsController {
177177

178178
void OnMethodCall(FlutterMethodCall* call, FlutterResult& result);
179179

180+
// Returns the platform view id if the platform view (or any of its descendant view) is the first
181+
// responder. Returns -1 if no such platform view found.
182+
long findFirstResponderPlatformViewId();
183+
180184
private:
181185
static const size_t kMaxLayerAllocations = 2;
182186

@@ -329,4 +333,8 @@ class FlutterPlatformViewsController {
329333
- (UIView*)embeddedView;
330334
@end
331335

336+
@interface UIView (FirstResponder)
337+
@property(nonatomic, readonly) BOOL hasFirstResponderInViewHierarchy;
338+
@end
339+
332340
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERPLATFORMVIEWS_INTERNAL_H_

shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) {
5959
insertTextPlaceholderWithSize:(CGSize)size
6060
withClient:(int)client;
6161
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView removeTextPlaceholder:(int)client;
62+
- (void)flutterTextInputViewDidResignFirstResponder:(FlutterTextInputView*)textInputView;
6263

6364
@end
6465

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
static NSString* const kShowMethod = @"TextInput.show";
4040
static NSString* const kHideMethod = @"TextInput.hide";
4141
static NSString* const kSetClientMethod = @"TextInput.setClient";
42+
static NSString* const kSetPlatformViewClientMethod = @"TextInput.setPlatformViewClient";
4243
static NSString* const kSetEditingStateMethod = @"TextInput.setEditingState";
4344
static NSString* const kClearClientMethod = @"TextInput.clearClient";
4445
static NSString* const kSetEditableSizeAndTransformMethod =
@@ -1075,6 +1076,14 @@ - (BOOL)canBecomeFirstResponder {
10751076
return _textInputClient != 0;
10761077
}
10771078

1079+
- (BOOL)resignFirstResponder {
1080+
BOOL success = [super resignFirstResponder];
1081+
if (success) {
1082+
[self.textInputDelegate flutterTextInputViewDidResignFirstResponder:self];
1083+
}
1084+
return success;
1085+
}
1086+
10781087
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
10791088
// When scribble is available, the FlutterTextInputView will display the native toolbar unless
10801089
// these text editing actions are disabled.
@@ -2071,6 +2080,9 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
20712080
} else if ([method isEqualToString:kSetClientMethod]) {
20722081
[self setTextInputClient:[args[0] intValue] withConfiguration:args[1]];
20732082
result(nil);
2083+
} else if ([method isEqualToString:kSetPlatformViewClientMethod]) {
2084+
[self setPlatformViewTextInputClient:[args[@"platformViewId"] longValue]];
2085+
result(nil);
20742086
} else if ([method isEqualToString:kSetEditingStateMethod]) {
20752087
[self setTextInputEditingState:args];
20762088
result(nil);
@@ -2187,6 +2199,13 @@ - (void)triggerAutofillSave:(BOOL)saveEntries {
21872199
[self addToInputParentViewIfNeeded:_activeView];
21882200
}
21892201

2202+
- (void)setPlatformViewTextInputClient:(long)platformViewID {
2203+
[self removeEnableFlutterTextInputViewAccessibilityTimer];
2204+
_activeView.accessibilityEnabled = NO;
2205+
[_activeView removeFromSuperview];
2206+
[_inputHider removeFromSuperview];
2207+
}
2208+
21902209
- (void)setTextInputClient:(int)client withConfiguration:(NSDictionary*)configuration {
21912210
[self resetAllClientIds];
21922211
// Hide all input views from autofill, only make those in the new configuration visible

0 commit comments

Comments
 (0)