12

So, I have a login page with two TextFields, and then a RaisedButton for login at the very bottom. When I tap on the email field and the keyboard pops up, I would like for the SingleChildScrollView (the parent of everything on the page) to scroll to the maxScrollExtent.

Things I have tried that haven't worked:

  • Taking advantage of Scaffold's ability to do this automatically (Scaffold is the parent widget of everything in the app)
  • Using this tutorial in which a helper widget is created. Also uses WidgetBindingsObserver, but the tutorial as a whole did not work for me. I wonder if WidgetBindingsObserver could still be helpful, however.

What almost works:

  • Attaching a FocusNode to the TextForm, then attaching a listener in initState() which will animate to the maxScrollExtent when it has focus.

By almost, here's what I mean (excuse the GIF discoloration):

enter image description here

As you can see, it doesn't work the first time it focuses so I have to tap the password field, then retap the email field for it to animate. I have tried adding a delay (even up to 500ms) so that the viewport has time to fully resize before doing this, but that didn't work either.

If you recognize this login theme, that's because I adapted it from here. The file is pretty lengthy, but here are the relevant bits:

@override
  void initState() {
    super.initState();
    scrollController = ScrollController();
    focusNode = FocusNode();

    focusNode.addListener(() {
      if (focusNode.hasFocus) {
        scrollController.animateTo(scrollController.position.maxScrollExtent,
            duration: Duration(milliseconds: 500), curve: Curves.ease);
      }
    });

    _emailFieldController = TextEditingController();
    _passFieldController = TextEditingController();

    _emailFieldController.addListener(() {
      _emailText = _emailFieldController.text;
    });

    _passFieldController.addListener(() {
      _passText = _passFieldController.text;
    });
  }
 @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      controller: scrollController,
      child: Container(
        height: MediaQuery.of(context).size.height,
        decoration: BoxDecoration(
          color: Colors.white,
          image: DecorationImage(
            colorFilter: ColorFilter.mode(
                Colors.black.withOpacity(0.05), BlendMode.dstATop),
            image: AssetImage('assets/images/mountains.jpg'),
            fit: BoxFit.cover,
          ),
        ),
        child: new Column(
          children: <Widget>[
            // this is where all other widgets in the file are

Container(
              width: MediaQuery.of(context).size.width,
              margin: const EdgeInsets.only(left: 40.0, right: 40.0, top: 10.0),
              alignment: Alignment.center,
              decoration: BoxDecoration(
                border: Border(
                  bottom: BorderSide(
                      color: Colors.deepPurple,
                      width: 0.5,
                      style: BorderStyle.solid),
                ),
              ),
              padding: const EdgeInsets.only(left: 0.0, right: 10.0),
              child: Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.start,
                children: <Widget>[
                  Expanded(
                    child: TextField(
                      controller: _emailFieldController,
                      keyboardType: TextInputType.emailAddress,
                      focusNode: focusNode,
                      obscureText: false,
                      textAlign: TextAlign.left,
                      decoration: InputDecoration(
                        border: InputBorder.none,
                        hintText: 'coolname@bestemail.com',
                        hintStyle: TextStyle(color: Colors.grey),
                      ),
                    ),
                  ),
                ],
              ),
            ),

Any guidance would be greatly appreciated. Thank you!

Chandler Davis
  • 362
  • 1
  • 4
  • 12

1 Answers1

15

Use addPostFrameCallback to listen after the widget was built.

          _onLayoutDone(_){
              FocusScope.of(context).requestFocus(focusNode);
          } 

          @override
          void initState() {
            //... your stuff

            WidgetsBinding.instance.addPostFrameCallback(_onLayoutDone);
            super.initState();
          }

UPDATE

I see the error, the first time you use scrollController.position.maxScrollExtent the value is 0, after you tap on password textField and you change the focus to email, now the maxScrollExtent is different because the keyboard is open.

If you want to make it work, do a logic to calculate the space and set the value directly.

If you use

 scrollController.animateTo(180.0,
        duration: Duration(milliseconds: 500), curve: Curves.ease);

It should work.

diegoveloper
  • 93,875
  • 20
  • 236
  • 194
  • Tried this as described and with FocusNode.addListener() inside of it, but no change unfortunately. – Chandler Davis Apr 15 '19 at 22:49
  • don't use FocusNode.addListener() inside _onLayoutDone , just call directly to scrollController.animateTo... – diegoveloper Apr 15 '19 at 22:51
  • or just call : FocusScope.of(context).requestFocus(focusNode); inside _onLayoutDone – diegoveloper Apr 15 '19 at 22:52
  • would be great if you can share part of your code, so I could test on my own – diegoveloper Apr 15 '19 at 22:53
  • No problem. [Here is the link to the file in the repo](https://github.com/photonfighter/path_io/blob/development/lib/widgets/pages/login_page.dart). All of my current work is in the development branch. Thank you for your help! – Chandler Davis Apr 15 '19 at 22:57
  • Ah, I see! I've been doing some debugging with your solution implemented and discovered that the callback only gets called when the widgets originally render (i.e. when the user clicks the 'Log In' button) but not when the text field gets focus and the scrollable resizes. Also, I'm assuming I should but the scrollController.animateTo(...) inside of the callback? – Chandler Davis Apr 16 '19 at 00:16
  • The scroll animation should be inside your focus listen method – diegoveloper Apr 16 '19 at 00:20