How to scroll dialog content to focused TextField in Flutter?

Viewed 129

I have a dialog with long content and many TextFields - at the top, middle and bottom. This is my test code

import 'package:flutter/material.dart';

class TempDialog extends StatefulWidget {

  TempDialog({Key? key}) : super(key: key);

  @override
  State<TempDialog> createState() => _TempDialogState();
}

class _TempDialogState extends State<TempDialog> {

  @override
  Widget build(BuildContext context) {
    var textController = TextEditingController(text: "");
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;
    return AlertDialog(
      content: Container(
        width: width,
        height: height,
        child: Scaffold(
          body: SingleChildScrollView(
            child: Column(
              children: [
                TextField(
                  textAlign: TextAlign.left,
                  decoration: InputDecoration(
                    contentPadding: EdgeInsets.all(10.0),
                    fillColor: Colors.green,
                    filled: true
                  ),
                  keyboardType: TextInputType.multiline,
                  maxLines: null,
                  minLines: 2,
                  controller: textController,
                ),
                SizedBox(height: 500,),
                TextField(
                  textAlign: TextAlign.left,
                  decoration: InputDecoration(
                    contentPadding: EdgeInsets.all(10.0),
                    fillColor: Colors.red,
                    filled: true
                  ),
                  keyboardType: TextInputType.multiline,
                  maxLines: null,
                  minLines: 2,
                  controller: textController,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class TempScreen extends StatefulWidget {

  TempScreen({Key? key}) : super(key: key);

  @override
  State<TempScreen> createState() => _TempScreenState();
}

class _TempScreenState extends State<TempScreen> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Temp screen"),
      ),
      body: Column(
        children: [
          TextButton(
              onPressed: (){
                showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return TempDialog();
                    }
                );
              },
              child: Text("Tap me"))
        ],
      ),
    );
  }
}

And this is the result:

enter image description here

As you see TextField that is at the bottom is not visible on focus - scrollview doesnt scroll to its position.

Could anyone say how to fix this issue. Please, note, that solution needs to support multiple TextFields (as I've said I have many of them).

EDIT 1

I tried to use scrollable positioned list. This is my code

import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

class TempDialog extends StatefulWidget {

  TempDialog({Key? key}) : super(key: key);

  @override
  State<TempDialog> createState() => _TempDialogState();
}

class _TempDialogState extends State<TempDialog> {

  final ItemScrollController itemScrollController = ItemScrollController();
  final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      content: Container(
         width: 300,
         height: 500,
        child: ScrollablePositionedList.builder(
        padding: EdgeInsets.only(top: 10),
        itemCount: 2,
        physics: ClampingScrollPhysics(),
        itemBuilder: (context, index) {
          return Focus(
            child: Padding(
              padding: EdgeInsets.only(bottom: 500),
              child: TextField(
                key: ValueKey("_k" + index.toString()),
                textAlign: TextAlign.left,
                decoration: InputDecoration(
                    contentPadding: EdgeInsets.all(10.0),
                    fillColor: Colors.red,
                    filled: true
                ),
                keyboardType: TextInputType.multiline,
                maxLines: null,
                minLines: 2,
                //controller: textController,
              ),
            ),
            onFocusChange: (hasFocus) {
              if (hasFocus) {
                // itemScrollController.jumpTo(index: index);
                itemScrollController.scrollTo(
                    index: index,
                    duration: Duration(seconds: 2),
                    curve: Curves.easeInOutCubic);
              }
            } ,
          );
        },
        itemScrollController: itemScrollController,
        itemPositionsListener: itemPositionsListener,
      ),
        ),
      );
  }
}

class TempScreen extends StatefulWidget {

  TempScreen({Key? key}) : super(key: key);

  @override
  State<TempScreen> createState() => _TempScreenState();
}

class _TempScreenState extends State<TempScreen> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Temp screen"),
      ),
      body: Column(
        children: [
          TextButton(
              onPressed: (){
                showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return TempDialog();
                    }
                );
              },
              child: Text("Tap me"))
        ],
      ),
    );
  }
}

and this is the result:

enter image description here

As you see the problem is that when keyboard is shown it doesn't scroll to focused item.

2 Answers

I managed to achieve that (not by myself, but I can't recall where I found help) in a similar case. My widget structure is: AlertDialog -> Container (for width setting) -> StatefulBuilder. The last one, after a bit of preparation, returns this:

     return Form(
          key: _formKey,
          child: ScrollablePositionedList.builder(
            padding: EdgeInsets.only(top: 10),
            itemCount: childrenList.length,
            itemBuilder: (context, index) => childrenList[index],
            itemScrollController: itemScrollController,
            itemPositionsListener: itemPositionsListener,
          ));

where

  • childrenList contains the list of widgets, most of them being TextFormField
  • itemBuilder just takes the items in order
  • itemScrollController is just final ItemScrollController itemScrollController = ItemScrollController();
  • itemPositionsListener is just final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();

I don't think that there's anything else involved in the "auto-scrolling" thing, but I wrote this a while ago, so let me know if something's missing.

Your code works absolutely fine on both devices. I have added a gesture detector to the Container since the textfields were multi-lined so there was no option to lower the keyboard in iOS. Here is the code that I have used

import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: TempScreen());
  }
}

class TempDialog extends StatefulWidget {
  TempDialog({Key? key}) : super(key: key);

  @override
  State<TempDialog> createState() => _TempDialogState();
}

class _TempDialogState extends State<TempDialog> {
  @override
  Widget build(BuildContext context) {
    var textController = TextEditingController(text: "");
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;
    return AlertDialog(
      content: GestureDetector(
        onTap: () {
          FocusScope.of(context).requestFocus(FocusNode());
        },
        child: Container(
          width: width,
          height: height,
          child: Scaffold(
            body: SingleChildScrollView(
              child: Column(
                children: [
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.purple,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                  SizedBox(
                    height: 90,
                  ),
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.green,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                  SizedBox(
                    height: 90,
                  ),
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.blue,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                  SizedBox(
                    height: 90,
                  ),
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.red,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                  SizedBox(
                    height: 90,
                  ),
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.yellow,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                  SizedBox(
                    height: 90,
                  ),
                  TextField(
                    textAlign: TextAlign.left,
                    decoration: InputDecoration(
                        contentPadding: EdgeInsets.all(10.0),
                        fillColor: Colors.red,
                        filled: true),
                    keyboardType: TextInputType.multiline,
                    maxLines: null,
                    minLines: 2,
                    controller: textController,
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class TempScreen extends StatefulWidget {
  TempScreen({Key? key}) : super(key: key);

  @override
  State<TempScreen> createState() => _TempScreenState();
}

class _TempScreenState extends State<TempScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Temp screen"),
      ),
      body: Column(
        children: [
          TextButton(
              onPressed: () {
                showDialog(
                    context: context,
                    builder: (BuildContext context) {
                      return TempDialog();
                    });
              },
              child: Text("Tap me"))
        ],
      ),
    );
  }
}

preview1 preview2

EDIT

You can create a key and assign it to the textfield and you can use this key and scroll to that widget's position like this.

final dataKey = GlobalKey();
TextField(
   key: dataKey,
   textAlign: TextAlign.left,
   decoration: const InputDecoration(
        contentPadding: EdgeInsets.all(10.0),
        fillColor: Colors.red,
        filled: true),
   keyboardType: TextInputType.multiline,
   maxLines: null,
   minLines: 2,
   controller: textController,
   onTap: () {
         Scrollable.ensureVisible(dataKey.currentContext!);//here you can scroll to the respective widget referring the key. 
    },
 ),

Please note that if you have a lot of textfields, this may result in some performance issue.. But scrolling to the right widget will work.

Related