-
Notifications
You must be signed in to change notification settings - Fork 6k
Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement #43972
Conversation
3091c86
to
fb66944
Compare
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.
Overall looks great. I will ask @cbracken to also take a look.
@@ -164,4 +164,9 @@ FLUTTER_DARWIN_EXPORT | |||
- (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER; | |||
|
|||
@end | |||
|
|||
@interface UIView (FindFirstResponder) | |||
- (id)firstResponder; |
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.
Can you rename to flt_firstResponder
in case of naming conflict? (ObjC doesn't support namespace)
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.
Good idea! Went through and made the change.
@@ -45,6 +45,7 @@ | |||
static NSString* const kSetSelectionRectsMethod = @"Scribble.setSelectionRects"; | |||
static NSString* const kStartLiveTextInputMethod = @"TextInput.startLiveTextInput"; | |||
static NSString* const kUpdateConfigMethod = @"TextInput.updateConfig"; | |||
static NSString* const kOnPointerMoveMethod = @"TextInput.onPointerMove"; |
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.
Nit maybe kOnInteractiveKeyboardPointerMoveMethod = @"TextInput.onPointerMoveForInteractiveKeyboard"
, to be more specific.
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.
Just updated, I'll make sure to try and add Interactive Keyboard more in the future, definitely would help with clarity!
@@ -2272,11 +2289,57 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { | |||
} else if ([method isEqualToString:kUpdateConfigMethod]) { | |||
[self updateConfig:args]; | |||
result(nil); | |||
} else if ([method isEqualToString:kOnPointerMoveMethod]) { | |||
double pointerY = [args[@"pointerY"] doubleValue]; |
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.
use CGRect
rather than double
for UI stuff.
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.
With regards to the pointerY this only contains a single double, so I am not sure that this would classify as UI.
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.
sorry i meant CGFloat
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.
Gotcha, just updated the code!
5a5c269
to
a41e685
Compare
- (void)handlePointerMove:(double)pointerY { | ||
double screenHeight = [UIScreen mainScreen].bounds.size.height; | ||
double keyboardHeight = _keyboardRect.size.height; | ||
// Determines if pointer goes overtop of the keyboard |
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.
nits:
// Determines if pointer goes overtop of the keyboard | |
// Determines if pointer goes overtop of the keyboard. |
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.
Will do! Just slipped by me.
// Determines if pointer goes overtop of the keyboard | ||
if (screenHeight - keyboardHeight <= pointerY) { | ||
// If no screenshot has been taken | ||
if (_keyboardViewContainer.subviews.count == 0) { |
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.
Using _keyboardViewContainer.subviews.count == 0
doesn't seems to be safe and future proof. For example, what if someone added a new subview for _keyboardViewContainer
and didn't know this logic exist? Could you use a different indicator?
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.
Definitely a concern, the same thing occurs in the tests so there definitely is a need to update this approach. I went through and included a UIView that represents the screenshot. Then we can see if it the superview matches instead.
CGRect frameRect = _keyboardRect; | ||
frameRect.origin.y = pointerY; | ||
_keyboardViewContainer.frame = frameRect; |
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.
nits: could you refactor these lines into a method to match what you did in if
Maybe something like:
`[self setKeyboardContainerHeight:pointerY]
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.
Done! Always helpful to simplify code into sections to increase understandability.
@@ -60,6 +60,8 @@ @interface FlutterSecureTextInputView : FlutterTextInputView | |||
|
|||
@interface FlutterTextInputPlugin () | |||
@property(nonatomic, assign) FlutterTextInputView* activeView; | |||
@property(nonatomic, assign) UIView* keyboardViewContainer; |
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.
@property(nonatomic, assign) UIView* keyboardViewContainer; | |
@property(nonatomic, weak) UIView* keyboardViewContainer; |
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.
@cyanglaz i could be wrong, but iirc the standard practice is just assign
when exposing the property in test header (similar to the activeView
properly above). weak
is confusing since it's actually a strong
property.
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.
Oh. I didn't realize it is only exposing the property. I'm not aware of the standard practice tho but since activeView also used assign, I will take your word for it :)
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.
@cyanglaz and I just discussed offline - it should be readonly
here, since your unit test does not need the setter.
@property(nonatomic, readonly) UIView* keyboardViewContainer;
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.
Gotcha! That works for me, just went through and made the changes.
a41e685
to
3e41f37
Compare
3e41f37
to
314f3db
Compare
} else { | ||
result(FlutterMethodNotImplemented); | ||
} | ||
} | ||
|
||
- (void)handlePointerMove:(CGFloat)pointerY { | ||
double screenHeight = [UIScreen mainScreen].bounds.size.height; |
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.
We are moving away from using [UIScreen mainScreen]. I don't think we should block this PR because of this, but we will need to follow up to clean up later.
FYI @mossmana
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.
Is the pointerY based on the screen, or the flutter app?
Since it comes from the Framework, I assume it is based on the flutter app? So In scenarios (add to app, or app extension) when the flutter app does not fill the full screen, the calculation below that determines if the pointer goes overtop of the keyboard should be based on the flutter app's height instead of the screen height, correct?
Maybe instead of the screenHeight, we should use the height of the FlutterViewController's view?
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.
We are moving away from using [UIScreen mainScreen]. I don't think we should block this PR because of this, but we will need to follow up to clean up later. FYI @mossmana
@Matt2D While usually I would agree with not blocking the PR, in this particular case, this is a new introduction of [UIScreen mainScreen]. I would prefer that we don't add new instances. You can refer to flutter/flutter#125496 for how we are handling the deprecation in other places.
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.
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.
@cyanglaz in this case, since this is being used to determine the pointer position on the screen, this is relative to the screen rather than the flutter view.
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.
FYI this is the PR promised: #44303
After that gets landed, you can do something like this here:
UIScreen *screen = _viewController.flutterScreenIfViewLoaded;
if (!screen) {
return;
}
CGFloat screenHeight = screen.bounds.size.height;
// ...
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.
Thank you for the code snippet, I think the new api is much cleaner! Updated the code using the mentioned solution.
} | ||
|
||
- (void)takeScreenshot { | ||
UIView* keyboardSnap = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:YES]; |
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.
ditto about [UIScreen mainScreen]
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.
Updated with the fix provided by the new PR Huan did.
[UIApplication.sharedApplication.delegate.window.rootViewController.view | ||
addSubview:_keyboardViewContainer]; |
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.
Should it be FlutterViewController instead?
In add to app scenario, the rootViewController might not be FlutterViewController
, which might be covered by FlutterViewController
. So the keyboard might be shown "behind" the FlutterViewController
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.
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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely worth adding!
// Determines if pointer goes overtop of the keyboard. | ||
if (screenHeight - keyboardHeight <= pointerY) { | ||
// If no screenshot has been taken. | ||
if (_keyboardView.superview != nil) { |
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.
Did you mean if (_keyboardView.superview == nil)
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.
Yep you are right! Thanks for catching this.
CGRect frameRect = _keyboardRect; | ||
frameRect.origin.y = pointerY; | ||
_keyboardViewContainer.frame = frameRect; |
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.
CGRect frameRect = _keyboardRect;
if (screenHeight - keyboardHeight <= pointerY) {
frameRect.origin.y = pointerY;
}
_keyboardViewContainer.frame = frameRect;
Then in the handlePointerMove
, the logic can be simplified as
if (_keyboardView.superview != nil) {
[self setKeyboardContainerHeight:pointerY];
} else {
[self takeScreenshot];
[self hideKeyboardWithoutAnimation];
}
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:
- (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 (_keyboardView.superview != nil) {
// If screenshot is present.
[self setKeyboardContainerHeight:pointerY];
} else {
// If no screenshot has been taken.
if (screenHeight - keyboardHeight <= pointerY) {
//If pointer is above keyboard.
[self takeKeyboardScreenshotAndDisplay];
[self hideKeyboardWithoutAnimation];
}
}
}
- (void)setKeyboardContainerHeight:(CGFloat)pointerY {
//View must be loaded at this point.
UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
double screenHeight = screen.bounds.size.height;
double keyboardHeight = _keyboardRect.size.height;
CGRect frameRect = _keyboardRect;
if (screenHeight - keyboardHeight <= pointerY) {
frameRect.origin.y = pointerY;
}
_keyboardViewContainer.frame = frameRect;
}
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
if (_keyboardView.superview != nil) {
[self setKeyboardContainerHeight:pointerY];
} else if (screenHeight - keyboardHeight <= pointerY) {
[self takeScreenshot];
[self hideKeyboardWithoutAnimation];
}
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.
@@ -164,4 +164,9 @@ FLUTTER_DARWIN_EXPORT | |||
- (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin NS_DESIGNATED_INITIALIZER; | |||
|
|||
@end | |||
|
|||
@interface UIView (FindFirstResponder) | |||
- (id)flt_firstResponder; |
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.
optional nits: use a readonly property instead.
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.
Avoid underscore in obj-c identifiers. We're doing something similar in another outstanding PR on the macOS side and using flutter
as the prefix here:
https://github.com/flutter/engine/pull/43101/files#diff-edbbe0be10d99338c2496cad85a27776c79ec8b260825ee4d309b1e4d194d5b2R10
So we'd end up with flutterFirstResponder
here.
@cyanglaz @hellohuanlin preferences? We should probably be consistent across embedders.
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.
flutterFirstResponder
SGTM
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.
Will do! Thank you for the suggestion
- (void)handlePointerMove:(CGFloat)pointerY { | ||
double screenHeight = [UIScreen mainScreen].bounds.size.height; | ||
double keyboardHeight = _keyboardRect.size.height; | ||
// Determines if pointer goes overtop of the keyboard. |
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.
Maybe something like:
// Determines if pointer goes overtop of the keyboard. | |
// If the pointer is within the bounds of the keyboard. |
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.
(And maybe move the comment inside the conditional block)
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.
Makes sense, thank you!
[UIView setAnimationsEnabled:YES]; | ||
} | ||
|
||
- (void)takeScreenshot { |
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.
Consider renaming to something more specific for readability -- e.g. take KeyboardSnapshot
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.
Definitely, I also added add to screen to be even better for readability.
result:^(id _Nullable result){ | ||
}]; | ||
XCTAssert(textInputPlugin.keyboardView.superview != nil); | ||
CGFloat newHeight = 600; |
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.
Consider just inlining this into the call below, or move this up above and use in both the event above and the check below.
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.
Definitely!
result:^(id _Nullable result){ | ||
}]; | ||
XCTAssert(textInputPlugin.keyboardView.superview != nil); | ||
CGFloat newHeight = 600; |
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.
Similar comment here.
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.
Done
} | ||
} | ||
|
||
- (void)testInteractiveKeyboardScreenshotWillBeMovedUpAfterUserScroll { |
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.
Maybe rather than just "up" something like "back to original position"?
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.
Good idea! Done
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.
Overall looks great - just a few comments aside from those left by others!
…able (#44303) The existing `screenIfViewLoaded` and `windowSceneIfLoaded` functions are private helpers of `FlutterViewController` class. This PR moves the logic to a category of the `UIViewController`, so it can be reused for any `UIViewController`s. *List which issues are fixed by this PR. You must list at least one issue.* #43972 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
314f3db
to
881e7be
Compare
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.
LGTM
auto label is removed for flutter/engine, pr: 43972, due to - The status or check suite Linux mac_unopt has failed. Please fix the issues identified (or deflake) before re-applying this label. |
…131950) flutter/engine@a7bea86...c254deb 2023-08-04 skia-flutter-autoroll@skia.org Roll Skia from 85938bb68e75 to 45c0a830d805 (3 revisions) (flutter/engine#44397) 2023-08-04 55360120+Matt2D@users.noreply.github.com Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement (flutter/engine#43972) 2023-08-04 skia-flutter-autoroll@skia.org Roll Dart SDK from b3372378e487 to a0b59bac20fc (1 revision) (flutter/engine#44393) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jacksongardner@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…able (flutter#44303) The existing `screenIfViewLoaded` and `windowSceneIfLoaded` functions are private helpers of `FlutterViewController` class. This PR moves the logic to a category of the `UIViewController`, so it can be reused for any `UIViewController`s. *List which issues are fixed by this PR. You must list at least one issue.* flutter#43972 (comment) *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
…Movement (flutter#43972) This PR address the movement aspect of the flutter interactive keyboard. It handles pointer movement while a scroll view widget is visible, and the interactive behavior is chosen for keyboardDismissBehavior. This is a desired behavior of the keyboard that has not yet been implemented. Design Document: https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1 Issues Address: flutter/flutter#57609 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This PR address the movement aspect of the flutter interactive keyboard. It handles pointer movement while a scroll view widget is visible, and the interactive behavior is chosen for keyboardDismissBehavior. This is a desired behavior of the keyboard that has not yet been implemented.
Design Document:
https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1
Issues Address:
flutter/flutter#57609
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.