Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement #43972
Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement #43972auto-submit[bot] merged 7 commits intoflutter:mainfrom
Conversation
3091c86 to
fb66944
Compare
hellohuanlin
left a comment
There was a problem hiding this comment.
Overall looks great. I will ask @cbracken to also take a look.
| @end | ||
|
|
||
| @interface UIView (FindFirstResponder) | ||
| - (id)firstResponder; |
There was a problem hiding this comment.
Can you rename to flt_firstResponder in case of naming conflict? (ObjC doesn't support namespace)
There was a problem hiding this comment.
Good idea! Went through and made the change.
| 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.
Nit maybe kOnInteractiveKeyboardPointerMoveMethod = @"TextInput.onPointerMoveForInteractiveKeyboard", to be more specific.
There was a problem hiding this comment.
Just updated, I'll make sure to try and add Interactive Keyboard more in the future, definitely would help with clarity!
| [self updateConfig:args]; | ||
| result(nil); | ||
| } else if ([method isEqualToString:kOnPointerMoveMethod]) { | ||
| double pointerY = [args[@"pointerY"] doubleValue]; |
There was a problem hiding this comment.
use CGRect rather than double for UI stuff.
There was a problem hiding this comment.
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.
sorry i meant CGFloat
There was a problem hiding this comment.
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.
nits:
| // Determines if pointer goes overtop of the keyboard | |
| // Determines if pointer goes overtop of the keyboard. |
There was a problem hiding this comment.
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.
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.
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.
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.
Done! Always helpful to simplify code into sections to increase understandability.
|
|
||
| @interface FlutterTextInputPlugin () | ||
| @property(nonatomic, assign) FlutterTextInputView* activeView; | ||
| @property(nonatomic, assign) UIView* keyboardViewContainer; |
There was a problem hiding this comment.
| @property(nonatomic, assign) UIView* keyboardViewContainer; | |
| @property(nonatomic, weak) UIView* keyboardViewContainer; |
There was a problem hiding this comment.
@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.
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.
@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.
Gotcha! That works for me, just went through and made the changes.
a41e685 to
3e41f37
Compare
3e41f37 to
314f3db
Compare
| } | ||
|
|
||
| - (void)handlePointerMove:(CGFloat)pointerY { | ||
| double screenHeight = [UIScreen mainScreen].bounds.size.height; |
There was a problem hiding this comment.
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.
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.
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.
@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.
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.
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.
ditto about [UIScreen mainScreen]
There was a problem hiding this comment.
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.
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.
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.
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.
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.
Did you mean if (_keyboardView.superview == nil)
There was a problem hiding this comment.
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.
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.
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.
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.
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.
| @end | ||
|
|
||
| @interface UIView (FindFirstResponder) | ||
| - (id)flt_firstResponder; |
There was a problem hiding this comment.
optional nits: use a readonly property instead.
There was a problem hiding this comment.
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.
flutterFirstResponder SGTM
There was a problem hiding this comment.
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.
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.
(And maybe move the comment inside the conditional block)
There was a problem hiding this comment.
Makes sense, thank you!
| [UIView setAnimationsEnabled:YES]; | ||
| } | ||
|
|
||
| - (void)takeScreenshot { |
There was a problem hiding this comment.
Consider renaming to something more specific for readability -- e.g. take KeyboardSnapshot
There was a problem hiding this comment.
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.
Consider just inlining this into the call below, or move this up above and use in both the event above and the check below.
| result:^(id _Nullable result){ | ||
| }]; | ||
| XCTAssert(textInputPlugin.keyboardView.superview != nil); | ||
| CGFloat newHeight = 600; |
| } | ||
| } | ||
|
|
||
| - (void)testInteractiveKeyboardScreenshotWillBeMovedUpAfterUserScroll { |
There was a problem hiding this comment.
Maybe rather than just "up" something like "back to original position"?
cbracken
left a comment
There was a problem hiding this comment.
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
|
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. |
flutter/engine@a7bea86...c254deb 2023-08-04 [email protected] Roll Skia from 85938bb68e75 to 45c0a830d805 (3 revisions) (flutter/engine#44397) 2023-08-04 [email protected] Flutter iOS Interactive Keyboard: Take Screenshot and Handle Pointer Movement (flutter/engine#43972) 2023-08-04 [email protected] 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 [email protected],[email protected],[email protected] 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.