-
Notifications
You must be signed in to change notification settings - Fork 6k
Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement #43972
Changes from all commits
fa51055
2d8138b
881e7be
f07d337
ff6f3d5
09a5ef5
f3eadbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
// found in the LICENSE file. | ||
|
||
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" | ||
#import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" | ||
|
||
#import <Foundation/Foundation.h> | ||
#import <UIKit/UIKit.h> | ||
|
@@ -45,6 +46,8 @@ | |
static NSString* const kSetSelectionRectsMethod = @"Scribble.setSelectionRects"; | ||
static NSString* const kStartLiveTextInputMethod = @"TextInput.startLiveTextInput"; | ||
static NSString* const kUpdateConfigMethod = @"TextInput.updateConfig"; | ||
static NSString* const kOnInteractiveKeyboardPointerMoveMethod = | ||
@"TextInput.onPointerMoveForInteractiveKeyboard"; | ||
|
||
#pragma mark - TextInputConfiguration Field Names | ||
static NSString* const kSecureTextEntry = @"obscureText"; | ||
|
@@ -761,6 +764,7 @@ @interface FlutterTextInputView () | |
@property(nonatomic, assign) CGRect markedRect; | ||
@property(nonatomic) BOOL isVisibleToAutofill; | ||
@property(nonatomic, assign) BOOL accessibilityEnabled; | ||
@property(nonatomic, assign) int textInputClient; | ||
// The composed character that is temporarily removed by the keyboard API. | ||
// This is cleared at the start of each keyboard interaction. (Enter a character, delete a character | ||
// etc) | ||
|
@@ -2214,6 +2218,11 @@ @interface FlutterTextInputPlugin () | |
@property(nonatomic, retain) FlutterTextInputView* activeView; | ||
@property(nonatomic, retain) FlutterTextInputViewAccessibilityHider* inputHider; | ||
@property(nonatomic, readonly, weak) id<FlutterViewResponder> viewResponder; | ||
|
||
@property(nonatomic, strong) UIView* keyboardViewContainer; | ||
@property(nonatomic, strong) UIView* keyboardView; | ||
@property(nonatomic, strong) UIView* cachedFirstResponder; | ||
@property(nonatomic, assign) CGRect keyboardRect; | ||
@end | ||
|
||
@implementation FlutterTextInputPlugin { | ||
|
@@ -2222,18 +2231,29 @@ @implementation FlutterTextInputPlugin { | |
|
||
- (instancetype)initWithDelegate:(id<FlutterTextInputDelegate>)textInputDelegate { | ||
self = [super init]; | ||
|
||
if (self) { | ||
// `_textInputDelegate` is a weak reference because it should retain FlutterTextInputPlugin. | ||
_textInputDelegate = textInputDelegate; | ||
_autofillContext = [[NSMutableDictionary alloc] init]; | ||
_inputHider = [[FlutterTextInputViewAccessibilityHider alloc] init]; | ||
_scribbleElements = [[NSMutableDictionary alloc] init]; | ||
_keyboardViewContainer = [[UIView alloc] init]; | ||
|
||
[[NSNotificationCenter defaultCenter] addObserver:self | ||
selector:@selector(handleKeyboardWillShow:) | ||
name:UIKeyboardWillShowNotification | ||
object:nil]; | ||
} | ||
|
||
return self; | ||
} | ||
|
||
- (void)handleKeyboardWillShow:(NSNotification*)notification { | ||
NSDictionary* keyboardInfo = [notification userInfo]; | ||
NSValue* keyboardFrameEnd = [keyboardInfo valueForKey:UIKeyboardFrameEndUserInfoKey]; | ||
_keyboardRect = [keyboardFrameEnd CGRectValue]; | ||
} | ||
|
||
- (void)dealloc { | ||
[self hideTextInput]; | ||
} | ||
|
@@ -2295,11 +2315,67 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { | |
} else if ([method isEqualToString:kUpdateConfigMethod]) { | ||
[self updateConfig:args]; | ||
result(nil); | ||
} else if ([method isEqualToString:kOnInteractiveKeyboardPointerMoveMethod]) { | ||
CGFloat pointerY = (CGFloat)[args[@"pointerY"] doubleValue]; | ||
[self handlePointerMove:pointerY]; | ||
result(nil); | ||
} else { | ||
result(FlutterMethodNotImplemented); | ||
} | ||
} | ||
|
||
- (void)handlePointerMove:(CGFloat)pointerY { | ||
// View must be loaded at this point. | ||
UIScreen* screen = _viewController.flutterScreenIfViewLoaded; | ||
double screenHeight = screen.bounds.size.height; | ||
double keyboardHeight = _keyboardRect.size.height; | ||
if (screenHeight - keyboardHeight <= pointerY) { | ||
// If the pointer is within the bounds of the keyboard. | ||
if (_keyboardView.superview == nil) { | ||
// If no screenshot has been taken. | ||
[self takeKeyboardScreenshotAndDisplay]; | ||
[self hideKeyboardWithoutAnimation]; | ||
} else { | ||
[self setKeyboardContainerHeight:pointerY]; | ||
} | ||
} else { | ||
if (_keyboardView.superview != nil) { | ||
// Keeps keyboard at proper height. | ||
_keyboardViewContainer.frame = _keyboardRect; | ||
} | ||
} | ||
} | ||
|
||
- (void)setKeyboardContainerHeight:(CGFloat)pointerY { | ||
CGRect frameRect = _keyboardRect; | ||
frameRect.origin.y = pointerY; | ||
_keyboardViewContainer.frame = frameRect; | ||
} | ||
|
||
- (void)hideKeyboardWithoutAnimation { | ||
[UIView setAnimationsEnabled:NO]; | ||
_cachedFirstResponder = UIApplication.sharedApplication.keyWindow.flutterFirstResponder; | ||
[_cachedFirstResponder resignFirstResponder]; | ||
[UIView setAnimationsEnabled:YES]; | ||
} | ||
|
||
- (void)takeKeyboardScreenshotAndDisplay { | ||
// View must be loaded at this point | ||
UIScreen* screen = _viewController.flutterScreenIfViewLoaded; | ||
UIView* keyboardSnap = [screen snapshotViewAfterScreenUpdates:YES]; | ||
keyboardSnap = [keyboardSnap resizableSnapshotViewFromRect:_keyboardRect | ||
afterScreenUpdates:YES | ||
withCapInsets:UIEdgeInsetsZero]; | ||
_keyboardView = keyboardSnap; | ||
[_keyboardViewContainer addSubview:_keyboardView]; | ||
if (_keyboardViewContainer.superview == nil) { | ||
[UIApplication.sharedApplication.delegate.window.rootViewController.view | ||
addSubview:_keyboardViewContainer]; | ||
Comment on lines
+2372
to
+2373
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should it be FlutterViewController instead? In add to app scenario, the rootViewController might not be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is positioning a keyboard relative to the full screen -- regardless of whether the FlutterView is full screen, or a subview covering only part of the screen in an add-to-app scenario -- I think we want the root view controller don't we? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeah that's true. We would need to make sure to position the keyboard screenshot on the very top. Set ZIndex to maxInt maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely worth adding! |
||
} | ||
_keyboardViewContainer.layer.zPosition = NSIntegerMax; | ||
_keyboardViewContainer.frame = _keyboardRect; | ||
} | ||
|
||
- (void)setEditableSizeAndTransform:(NSDictionary*)dictionary { | ||
[_activeView setEditableTransform:dictionary[@"transform"]]; | ||
if ([_activeView isScribbleAvailable]) { | ||
|
@@ -2764,3 +2840,21 @@ - (BOOL)handlePress:(nonnull FlutterUIPressProxy*)press API_AVAILABLE(ios(13.4)) | |
return NO; | ||
} | ||
@end | ||
|
||
/** | ||
* Recursively searches the UIView's subviews to locate the First Responder | ||
*/ | ||
@implementation UIView (FindFirstResponder) | ||
- (id)flutterFirstResponder { | ||
if (self.isFirstResponder) { | ||
return self; | ||
} | ||
for (UIView* subView in self.subviews) { | ||
UIView* firstResponder = subView.flutterFirstResponder; | ||
if (firstResponder) { | ||
return firstResponder; | ||
} | ||
} | ||
return nil; | ||
} | ||
@end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor optional nits:
The logic of this method can also be used for the scenario where the pointer is not overtop the keyboard.
The frameRect's Y can be updated to be the max of pointerY and keyboard height.
Then in the
handlePointerMove
, the logic can be simplified asUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It turns out that for the change we still need to check the height for takeScreenshot since we don't want to take the screenshot too early:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I missed that part. So the caller part can probably be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline - the duplicate logic of
screenHeight - keyboardHeight <= pointerY
can get error prune.Another idea is to pass in an additional BOOL to the helper function, but that also makes the code unnecessarily complicate.
I think it makes sense to keep it as it is.