Skip to content

Conversation

@gaaclarke
Copy link
Member

@gaaclarke gaaclarke commented Mar 6, 2020

Description

Created an option when creating ListViews and ScrollViews that can control how a ScrollView can automatically dismiss the keyboard (a la UIScrollViewKeyboardDismissMode ).

Related Issues

#21814

Tests

I added the following tests:

Replace this with a list of the tests that you added as part of this PR. A change in behaviour with no test covering it
will likely get reverted accidentally sooner or later. PRs must include tests for all changed/updated/fixed behaviors. See Test Coverage.

Checklist

Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes ([x]). This will ensure a smooth and quick review process.

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
  • I signed the CLA.
  • I read and followed the Flutter Style Guide, including Features we expect every widget to implement.
  • I read the Tree Hygiene wiki page, which explains my responsibilities.
  • I updated/added relevant documentation (doc comments with ///).
  • All existing and new tests are passing.
  • The analyzer (flutter analyze --flutter-repo) does not report any problems on my PR.
  • I am willing to follow-up on review comments in a timely manner.

Breaking Change

Did any tests fail when you ran them? Please read Handling breaking changes.

  • No, no existing tests failed, so this is not a breaking change.
  • Yes, this is a breaking change. If not, delete the remainder of this section.

@fluttergithubbot fluttergithubbot added framework flutter/packages/flutter repository. See also f: labels. work in progress; do not review labels Mar 6, 2020
@gaaclarke
Copy link
Member Author

@gspencergoog Can you check out what I'm doing here to make sure there isn't a different way we'd want to do this? Basically we want to make it easy for people to specify how a keyboard should be dismissed based on a scrollview's interaction. It's the equivalent to this: https://developer.apple.com/documentation/uikit/uiscrollviewkeyboarddismissmode/uiscrollviewkeyboarddismissmodeondrag?language=objc

: scrollable;

if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
final FocusNode dismissNode = FocusNode();
Copy link
Contributor

@gspencergoog gspencergoog Mar 6, 2020

Choose a reason for hiding this comment

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

You don't want to recreate this FocusNode every build, because you need to dispose of it, which (sadly) means you'd need to make this a stateful widget.

But... What's your goal here? Do you just want to know if a context doesn't have focus? If so, you could use something like this, and keep it stateless.

  return Focus(
    canRequestFocus: false,
    child: Builder(builder: (BuildContext context) {
      return NotificationListener<ScrollUpdateNotification>(
        child: scrollableResult,
        onNotification: (ScrollUpdateNotification notification) {
          final FocusNode node = Focus.of(context);
          if (notification.dragDetails != null && !node.hasFocus) {
            node.requestFocus();
          }
          return true;
        },
      );
    }),
  );

Copy link
Member Author

Choose a reason for hiding this comment

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

For the focus node, I'm using that to dismiss the keyboard when scrolling happens. I don't want to spam the Focus every time a scroll event happens so I keep a reference to the dismissNode to make sure that I only requestFocus once. It's fine if the FocusNode gets disposed, it's just a way to filter out duplicate events, if it never worked everything would still look the same.

Copy link
Contributor

Choose a reason for hiding this comment

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

Where are you disposing it?

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't realize we were talking about literally calling .dispose(). I'll rework it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Started using unfocus instead so no FocusNode instance is needed.

@gaaclarke
Copy link
Member Author

@gspencergoog I just wanted to get a gut check that we are fine adding a parameter to ScrollView for the keyboard dismiss behavior. In the future I'd like to add a third option which is "interactive" to duplicate iOS's native behavior.

if (notification.dragDetails != null && focusScope.focusedChild != dismissNode) {
focusScope.requestFocus(dismissNode);
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably shouldn't return true here -eating scroll notifications this low in the tree is likely to break people further up who also want to know about scrolling.

@gaaclarke gaaclarke marked this pull request as ready for review March 9, 2020 17:54
@gaaclarke gaaclarke requested a review from gspencergoog March 9, 2020 17:54
enum ScrollViewKeyboardDismissBehavior {
/// `manual` means there is no automatic dimissal of the on-screen keyboard.
/// It is up to the client to dismiss the keyboard.
manual,
Copy link
Member Author

Choose a reason for hiding this comment

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

Apple calls this behavior "none", I chose "manual".

onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
if (notification.dragDetails != null &&
(focusScope.focusedChild != null &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need this to write this differently from focusScope.hasFocus? And if that's the case, then why wouldn't this work?:

if (notification.dragDetails != null) {
  focusScope.unfocus();
}

Basically, what's the reason for the extra conditions?

Copy link
Member Author

Choose a reason for hiding this comment

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

notifcation.dragDetails != null is to make sure the scrolling is the result of a drag, these events get fired too when the scrollview is scrolled as the result of a keyboard showing.

focusScope.focusedChild != null is because I'm going to read from the focusedChild and I don't want to read from a null pointer.

focusScope.focusedChild.hasFocus makes sure I don't spam focusScope.unfocus() and that it only gets called once for all the scrolling events that get sent.

Copy link
Member Author

@gaaclarke gaaclarke Mar 9, 2020

Choose a reason for hiding this comment

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

Could I just use focusScole.hasFocus and avoid talking to the focusedChild?

Copy link
Member Author

Choose a reason for hiding this comment

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

I switched it over to focusScope.hasFocus. It wasn't like this because i found the other way to do it first and it worked.

@gaaclarke
Copy link
Member Author

@gspencergoog Oops, i just realized that changing from a Draft to a PR but it didn't remove the "WIP" label. I'm ready to merge this when approved.

return NotificationListener<ScrollUpdateNotification>(
child: scrollableResult,
onNotification: (ScrollUpdateNotification notification) {
final FocusScopeNode focusScope = FocusScope.of(context);
Copy link
Contributor

@gspencergoog gspencergoog Mar 11, 2020

Choose a reason for hiding this comment

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

Is there a reason you want to see if the scope is focused, rather than the nearest focus node? It's possible that this ScrollView isn't the focused child of the scope: if there's another child of the scope (i.e. a sibling of the ScrollView), and it's focused, then focusScope.hasFocus would return true, but children of this ScrollView wouldn't have focus.

If you expect the ScrollView to be enclosed in a Focus widget, you could use Focus.of(context).hasFocus. If you don't expect that (which is probably the case), you can introduce a focus node here (probably using a Focus widget with canRequestFocus set to false) and test to see if that has focus, which would mean that one of the children of the ScrollView has focus. You can keep the widget stateless by using a Builder to let you look up the node that the Focus introduces, as in my previous comment.

Copy link
Member Author

@gaaclarke gaaclarke Mar 11, 2020

Choose a reason for hiding this comment

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

Here is what I would like to write:

if (Keyboard.isVisible) {
  Keyboard.hide();
}

If the scrollview has a sibling that is a TextField that is focused, we want this scrolling action to dismiss his keyboard too. (I think that is the case you are mentioning).

Copy link
Contributor

Choose a reason for hiding this comment

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

OK, in that case, then looking at the scope is appropriate.

@gaaclarke
Copy link
Member Author

@gspencergoog can I get an LGTM?

Copy link
Contributor

@gspencergoog gspencergoog left a comment

Choose a reason for hiding this comment

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

32384589-a60f0e74-c078-11e7-9bc1-e5b5287aea9d

@gaaclarke gaaclarke merged commit f8e9a4f into flutter:master Mar 13, 2020
@lock
Copy link

lock bot commented Apr 2, 2020

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

@lock lock bot locked and limited conversation to collaborators Apr 2, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants