[go: nahoru, domu]

Skip to content
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

Open
wants to merge 74 commits into
base: master
Choose a base branch
from
Open

Conversation

justinmc
Copy link
Contributor
@justinmc justinmc commented May 21, 2024

Enables the Scribe feature, or Android stylus handwriting text input.

scribe

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)
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final FocusNode _focusNode1 = FocusNode();
  final FocusNode _focusNode2 = FocusNode();
  final FocusNode _focusNode3 = FocusNode();
  final TextEditingController _controller3 = TextEditingController(
    text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Scribe demo'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 74.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextField(
                focusNode: _focusNode1,
                autofocus: false,
              ),
              TextField(
                focusNode: _focusNode2,
              ),
              TextField(
                focusNode: _focusNode3,
                minLines: 4,
                maxLines: 4,
                controller: _controller3,
              ),
              TextButton(
                onPressed: () {
                  _focusNode1.unfocus();
                  _focusNode2.unfocus();
                  _focusNode3.unfocus();
                },
                child: const Text('Unfocus'),
              ),
              TextButton(
                onPressed: () {
                  _focusNode1.requestFocus();
                  SchedulerBinding.instance.addPostFrameCallback((Duration _) {
                    SystemChannels.textInput.invokeMethod('TextInput.hide');
                  });
                },
                child: const Text('Focus 1'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

@justinmc justinmc self-assigned this May 21, 2024
@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. labels May 21, 2024
@vistaar-mlopes
Copy link
vistaar-mlopes commented May 29, 2024

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?
I found your comment on using 2 native methods :
"Android's InputMethodManager class already has support for stylus handwriting. We'll need to tie these into the framework similarly to how it's done for iOS.

Some relevant methods:

1.InputMethodManager.startStylusHandwriting
2.View.setAutoHandwritingEnabled"
can I implement method channnel and access this native features?

Thanks.

@justinmc
Copy link
Contributor Author

@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).

@github-actions github-actions bot added f: material design flutter/packages/flutter/material repository. f: cupertino flutter/packages/flutter/cupertino repository labels Jun 3, 2024
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(
Copy link
Contributor Author

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.

@vistaar-mlopes
Copy link

@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?

@justinmc
Copy link
Contributor Author

@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.

@justinmc
Copy link
Contributor Author

@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.

@justinmc justinmc marked this pull request as ready for review October 23, 2024 17:56
@justinmc justinmc changed the title (WIP) Scribe Android handwriting text input Scribe Android handwriting text input Oct 23, 2024
Copy link
Contributor
@LongCatIsLooong LongCatIsLooong left a 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 {
Copy link
Contributor

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._() {
Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.
Copy link
Contributor

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?

Copy link
Contributor Author
@justinmc justinmc Oct 25, 2024

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
Copy link
Contributor

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")?

Copy link
Contributor Author

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) {
Copy link
Contributor

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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void initState() {
super.initState();
if (widget.enabled) {
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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.
Copy link
Contributor

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)?

Copy link
Contributor
@LongCatIsLooong LongCatIsLooong left a 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:

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 {
Copy link
Contributor

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`.
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

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?

Copy link
Contributor Author

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() {
Copy link
Contributor

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?

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: [] instead of ``?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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(
Copy link
Contributor

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?

Copy link
Contributor Author

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems f: cupertino flutter/packages/flutter/cupertino repository f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support stylus handwriting input on Android
3 participants