2

I have a ListView that includes a card with 2 text fields. If an entry is made on the second TextField of the Row then I want the focus to go to the second TextField on the next ListView row (essentially to stay in the same "column" of the display). Currently, if I use TextInputAction.next the focus will go to the first field on the next ListView row. I have tried adding a FocusNode to the ListView, but have not been able to "link" it to a specific item in the ListView. Is FocusNode the correct method to use or is there an easy way to skip the first field when the next action is performed. In addition, if the next row already includes a value in the second TextField, I would like the focus to go to the next empty ListView in that same "column".

Here is stripped down snippet of code:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(title: 'Next ListView Item'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  //this could include any number of players 
  List<String> players = [
    'Player 1',
    'Player 2',
    'Player 3',
    'Player 4',
    'Player 5',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: ListView.builder(
          itemCount: players.length,
          itemBuilder: (context, index) {
            return PlayerCard(
              player: players[index],
            );
          },
        ));
  }
}

class PlayerCard extends StatefulWidget {
  final String player;

  PlayerCard({this.player});

  @override
  _PlayerCardState createState() => _PlayerCardState();
}

class _PlayerCardState extends State<PlayerCard> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Card(
        child: Row(children: [
          Expanded(flex: 7, child: Text('${widget.player}')),
          Expanded(
            child: TextField(
              inputFormatters: [
                LengthLimitingTextInputFormatter(2),
                FilteringTextInputFormatter.digitsOnly,
              ],
              textInputAction: TextInputAction.next,
            ),
          ),
          Spacer(),
          Expanded(
              child: TextField(
            inputFormatters: [
              LengthLimitingTextInputFormatter(1),
              FilteringTextInputFormatter.digitsOnly,
            ],
            onChanged: (value) {
              FocusManager.instance.primaryFocus.nextFocus();
            },
            textInputAction: TextInputAction.next,
          )),
        ]),
      ),
    );
  }
}

Perhaps this screen shot will give a better example of what I'm trying to accomplish

E Run
  • 57
  • 7

1 Answers1

1

I was able to tweak another answer I found and apply it here. You need to keep moving the focus until you find a blank text field.

Cite: the other answer I tweaked is here: https://stackoverflow.com/a/63005046/3705348

class _PlayerCardState extends State<PlayerCard> {
  @override
  Widget build(BuildContext context) {
    return Container(
      child: Card(
        child: Row(children: [
          Expanded(flex: 7, child: Text('${widget.player}')),
          Expanded(
            child: TextField(
              inputFormatters: [
                LengthLimitingTextInputFormatter(2),
                FilteringTextInputFormatter.digitsOnly,
              ],
              textInputAction: TextInputAction.next,
            ),
          ),
          Spacer(),
          Expanded(
              child: TextField(
            inputFormatters: [
              LengthLimitingTextInputFormatter(1),
              FilteringTextInputFormatter.digitsOnly,
            ],
            onChanged: (value) {
              // Call extension method here instead of just moving focus
              context.nextBlankTextFocus();
            },
            textInputAction: TextInputAction.next,
          )),
        ]),
      ),
    );
  }
}

extension Utility on BuildContext {
  void nextBlankTextFocus() {
    var startingTextField;
    do {
      // Set starting text field so we can check if we've come back to where we started
      if (startingTextField == null) {
        startingTextField = FocusScope.of(this).focusedChild.context.widget;
      } else if (startingTextField ==
          FocusScope.of(this).focusedChild.context.widget) {
        // Back to where we started - stop as there are no more blank text fields
        break;
      }

      FocusScope.of(this).nextFocus();
    } while (FocusScope.of(this).focusedChild.context.widget is EditableText &&
        (FocusScope.of(this).focusedChild.context.widget as EditableText)
                .controller
                .text
                .trim()
                .length !=
            0);
  }
}
jaredbaszler
  • 3,941
  • 2
  • 32
  • 40
  • 1
    Nicely done! That works perfectly. Thank you for the prompt reply. I'm somewhat new here and have a "low" ranking which does not allow me to mark the answer as useful, but rest assured that it is. – E Run Oct 15 '20 at 15:04
  • 1
    @ERun - you've done a good job asking your first ever question on here. Thanks for providing code that was easily runnable so the community can more easily help you out. – jaredbaszler Oct 15 '20 at 15:16