How to scroll to bottom of SingleChildScrollView when TextField gets focus?
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.
Chandler Davis
Updated on July 18, 2022Comments
-
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):
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 about 5 yearsTried this as described and with FocusNode.addListener() inside of it, but no change unfortunately.
-
diegoveloper about 5 yearsdon't use FocusNode.addListener() inside _onLayoutDone , just call directly to scrollController.animateTo...
-
diegoveloper about 5 yearsor just call : FocusScope.of(context).requestFocus(focusNode); inside _onLayoutDone
-
diegoveloper about 5 yearswould be great if you can share part of your code, so I could test on my own
-
Chandler Davis about 5 yearsNo 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 about 5 yearsAh, 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 about 5 yearsThe scroll animation should be inside your focus listen method