How to scroll to bottom of SingleChildScrollView when TextField gets focus?

16,037

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.

Share:
16,037
Chandler Davis
Author by

Chandler Davis

Updated on July 18, 2022

Comments

  • Chandler Davis
    Chandler Davis almost 2 years

    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: '[email protected]',
                            hintStyle: TextStyle(color: Colors.grey),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
    

    Any guidance would be greatly appreciated. Thank you!

  • Chandler Davis
    Chandler Davis about 5 years
    Tried this as described and with FocusNode.addListener() inside of it, but no change unfortunately.
  • diegoveloper
    diegoveloper about 5 years
    don't use FocusNode.addListener() inside _onLayoutDone , just call directly to scrollController.animateTo...
  • diegoveloper
    diegoveloper about 5 years
    or just call : FocusScope.of(context).requestFocus(focusNode); inside _onLayoutDone
  • diegoveloper
    diegoveloper about 5 years
    would be great if you can share part of your code, so I could test on my own
  • Chandler Davis
    Chandler Davis about 5 years
    No problem. Here is the link to the file in the repo. All of my current work is in the development branch. Thank you for your help!
  • Chandler Davis
    Chandler Davis about 5 years
    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?
  • diegoveloper
    diegoveloper about 5 years
    The scroll animation should be inside your focus listen method