-
Notifications
You must be signed in to change notification settings - Fork 27.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Scribe Android handwriting text input #148784
base: master
Are you sure you want to change the base?
Conversation
hi. @justinmc I have Samsung galaxy s9 ultra tab, I wanted to implement the above feature. I went through above PR and till it gets approved, can you please suggest a temporary solution that I can implement using native android? Some relevant methods: 1.InputMethodManager.startStylusHandwriting Thanks. |
@vistaar-mlopes I'm not sure whether you could get this working by writing a plugin. The bare minimum thing you'd need to do is to call startStylusHandwriting on the InputMethodManager. If you couldn't do that with a plugin, you would need to build a custom engine and framework. By the way, if you end up trying this feature out using your Samsung device, could you let me know if it works? I'm assuming/hoping that other Android devices will work the same as what I'm using (Pixel Tablet). |
Currently I have a hacked version of OverflowBox that does the same thing. Need to modify it to allow the margin to overflow.
I can draw outside of the bounds nicely, but receiving gestures is another story. The parent will stop hit testing when it sees that an event is outside of its boundaries, so I can never receive it. Instead, maybe I should first look into the hover icon. How will I do that?
|
||
@override | ||
Widget build(BuildContext context) { | ||
return Listener( |
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.
Note to self: I can listen directly to the global pointer binding to avoid the problem with clipping out events that I want.
@justinmc Recently I got a Lenovo tab. I tried to scribble on the text field and it worked. I typed hello and it was there in the text field. whereas I tried the same on the Samsung tab with S-pen it didn't work. Now I'm not sure where the issue is. Is it a flutter issue or is it device dependent? |
@vistaar-mlopes Interesting! You tried this on master, not on this branch, right? I'm not sure how those other devices do handwriting. Maybe they have their own implementations since I believe Android hasn't supported this officially until recently. |
@vistaar-mlopes If you could try this branch out with your Samsung and Lenovo tablets I'd be interested to see if it solves your problem! Now no engine change is needed, just this framework branch. |
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.
(looked at widgets and services)
/// built-in text fields support handwriting input. | ||
/// * [SystemChannels.scribe], which is the [MethodChannel] used by this | ||
/// class, and which has a list of the methods that this class handles. | ||
class Scribe { |
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.
final class
since everything seems to be static.
/// * [SystemChannels.scribe], which is the [MethodChannel] used by this | ||
/// class, and which has a list of the methods that this class handles. | ||
class Scribe { | ||
Scribe._() { |
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.
Who calls this private constructor?
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.
Also how's linter not complaining if this is not used.
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 this is leftover from when I was handling gesture calls, before I moved it to a separate branch. I'll just remove all this. Not sure about the linter! Maybe the linter can't handle constructors?
/// A convenience method to check if the device currently supports Scribe | ||
/// stylus handwriting input. | ||
/// | ||
/// Call this before calling [startStylusHandwriting] to make sure it's |
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.
How often does the caller have to check if the feature is available? Can I assume the return value won't change while the app is running?
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 must be called each time before calling startStylusHandwriting. I'll update the docs.
} | ||
|
||
/// Returns true if the InputMethodManager supports Scribe stylus handwriting | ||
/// input. |
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.
Do false
and null
mean the same thing?
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.
The nullability is a limitation of invokeMethod (docs don't explain it super well either but maybe it's just that the codec can't enforce non-nullability?). I will explain in the docs here.
|
||
/// Tell Android to begin receiving stylus handwriting input. | ||
/// | ||
/// This is typically called after detecting a [PointerDownEvent] indicating |
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.
From the Android documentation this can't be called when there is no active input connection it seems (maybe I'm reading the doc wrong, it says "This will always be preceded by onStartInput(android.view.inputmethod.EditorInfo, boolean) for the EditorInfo and InputConnection for which stylus handwriting is being requested")?
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 call, I tried calling it with no active TextInputConnection and it was ignored. I'll update the docs to explain this.
|
||
/// Returns a new Rect whose size has changed by the given padding while | ||
/// remaining centered. | ||
static Rect _pad(Rect rect, EdgeInsets padding) { |
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.
/// [Rect] in global coordinates. | ||
static Rect _localToGlobalRect(Rect rect, RenderBox renderBox) { | ||
return Rect.fromPoints( | ||
renderBox.localToGlobal(rect.topLeft), |
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.
https://main-api.flutter.dev/flutter/painting/MatrixUtils/transformRect.html is probably more idiomatic.
void initState() { | ||
super.initState(); | ||
if (widget.enabled) { | ||
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); |
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.
What prevents 2 overlapping text fields from competing for stylus point down events? Also, the stylus hitting an invisible text field (in an inactive route, for example) would also trigger scribble it seems?
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.
Also why can't this be a regular gesture recognizer / detector? Is it because of the padding?
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.
I will follow up on this one on Monday.
What prevents 2 overlapping text fields from competing for stylus point down events?
Good catch, I just tried this out and it looks like the behavior to the user is that the field lowest in the tree will win if I write directly in between the two fields, where their paddings are overlapping. If I get really accurate with the stylus I can write just in the bottom of the upper field but the lower field will receive the text, but for the most part the behavior is probably fine in this ambiguous situation.
However, both fields do receive the event. I'll need to follow up and do something to prevent that.
3 TextFields right next to each other
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: controller,
stylusHandwritingEnabled: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
TextField(
stylusHandwritingEnabled: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
TextField(
stylusHandwritingEnabled: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
),
],
),
Also, the stylus hitting an invisible text field (in an inactive route, for example) would also trigger scribble it seems?
Should I check ModalRoute.of(context)
or something like that? Anything else I should check?
Also why can't this be a regular gesture recognizer / detector? Is it because of the padding?
Yes, this was my solution to needing to listen to an area outside of the size of the field.
return _ScribbleFocusable( | ||
focusNode: focusNode, | ||
editableKey: editableKey, | ||
enabled: enabled, |
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.
enabled
is always true here right?
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.
Yes, and it is more readable if I pass true
here 👍
} | ||
|
||
// A stylus event that starts on a selection handle does not start | ||
// handwriting, it moves the handle. |
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.
I wish this logic lives in the TextSelectionHandle (or whatever it is called) since we have different handle classes for android and iOS (I think)?
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.
Regarding hit-testing, how important is it to support _handwritingPadding
for how?
Can this be implemented using gesture recognizers, by either:
- move the logic to TextField like other gesture recognizers
- ignore the padding for now, and implement Opt a subtree out of hitTestSelf culling #75747 to allow the RenderBox to accept out-of-bounds events for the subtree
So we don't have to replicate the hit testing logic and z-index hittest behavior logic?
/// built-in text fields support handwriting input. | ||
/// * [SystemChannels.scribe], which is the [MethodChannel] used by this | ||
/// class, and which has a list of the methods that this class handles. | ||
final class Scribe { |
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: abstract final class
to indicate the class is just a namespace?
/// | ||
/// The return type is nullable due to the way | ||
/// [MethodChannel.invokeMethod](https://main-api.flutter.dev/flutter/services/MethodChannel/invokeMethod.html) | ||
/// works, but a successful call will never resolve to `null`. |
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.
Would it be possible to do .then<bool>((bool? value) => value ?? false)
or .then<bool>((bool? value) => value ?? throw SomeError)
then?
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 call, I should do this checking and not the user.
/// Returns true if the InputMethodManager supports Scribe stylus handwriting | ||
/// input, false otherwise. | ||
/// | ||
/// Call this each time before calling [startStylusHandwriting] to make sure |
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.
If this is the case shouldn't this API be incorporated into startStylusHandwriting
then? Since they are always paired?
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 sounds like it should, but that's not the way that Android designed their API for whatever reason. I want to provide an accurate reproduction of the Android API in case our users need it.
/// which is the corresponding API on Android. | ||
/// * [EditableText.stylusHandwritingEnabled], which controls whether | ||
/// Flutter's built-in text fields support handwriting input. | ||
static Future<void> startStylusHandwriting() { |
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.
so the IME is responsible for determining when to stop?
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.
Yes. It seems like it's a timeout from the last screen touch.
/// | ||
/// The following methods are defined for this channel: | ||
/// | ||
/// * `Scribe.startStylusHandwriting`: Indicates that stylus input has been |
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: [] instead of ``?
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.
Not sure why I didn't do that in the first place...
@@ -1975,6 +2001,9 @@ class EditableText extends StatefulWidget { | |||
/// {@macro flutter.widgets.magnifier.intro} | |||
final TextMagnifierConfiguration magnifierConfiguration; | |||
|
|||
/// The default value for [stylusHandwritingEnabled]. | |||
static const bool defaultStylusHandwritingEnabled = true; |
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.
Why is the default exposed?
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.
In order to reference it from other files that also need the same default, instead of duplicating it.
|
||
class _ScribeState extends State<_Scribe> { | ||
// The handwriting bounds padding of EditText in Android API 34. | ||
static const EdgeInsets _handwritingPadding = EdgeInsets.symmetric( |
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 this a fixed size or just the size of the input decorator?
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.
This is the fixed amount of padding that native Android uses to detect Scribe events outside of the field.
But, I also am seeing the magnifier appear for swipes that go from down to up. That seems to be a bug on master that shouldn't happen, and fixing it might improve the feel of scribe.
Enables the Scribe feature, or Android stylus handwriting text input.
This PR only implements basic handwriting input. Other features will be done in subsequent PRs:
I created and fixed issue about stylus hovering while working on this: #148810
Original PR for iOS Scribble, the iOS version of this feature: #75472
FYI @fbcouch
Depends on flutter/engine#52943(merged).Fixes #115607
Example code I'm using to test this feature (but any TextField works)