如何在按下按钮时从ListView中移除TextField?

4
当用户点击“清除图标”按钮时,如何删除TextField?(不仅仅是清除TextField的文本)
用户故事 用户单击一个按钮以添加玩家。(技术上,此按钮添加TextField) 用户可以在TextField中编写玩家的名称。 用户单击“清除图标”按钮以删除当前的TextField(相反于添加功能)。

enter image description here

new ListView.builder(
                     padding: EdgeInsets.all(0),
                      shrinkWrap: true,
                      physics: NeverScrollableScrollPhysics(),
                      itemCount: 5,
                      itemBuilder: (context, index) {
                        print(index);
                        return TextField(
                          maxLength: 20,
                          decoration: InputDecoration(
                            labelText: "Player ${index+1}",
                            counterText: "",
                            prefixIcon: const Icon(Icons.person),
                            suffixIcon: new IconButton(
                                icon: Icon(Icons.clear),
                                onPressed: () =>
                                  setState(() {
                                    this.dispose(); // -----Doesn't work----
                                  })
                                ),
                          ),
                        );
                      }
                    ),

例如,如果用户在玩家4上设置了“John”,并且用户单击“清除按钮”,则玩家4的文本字段将被删除。只剩下4个文本字段。

enter image description here

2个回答

11

我假设的事实:

  1. 您想能够从列表中删除(或添加)字段
  2. 当您删除字段时,要求剩余字段的值保持不变
  3. 列表可能大于5个

解决方案:

如果您希望上述所有内容都是真实的,那么您实际上需要跟踪 TextEditingController 的文本控制器而不是文本字段本身。这是因为文本字段的值实际上存储在 TextEditingController 中(如果您没有为每个小部件提供它,则会即时创建)。看一下这个例子:

import 'package:flutter/material.dart';

// needs to be StatefulWidget, so we can keep track of the count of the fields internally
class PlayerList extends StatefulWidget {
  const PlayerList({
    this.initialCount = 5,
  });

  // also allow for a dynamic number of starting players
  final int initialCount;

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

class _PlayerListState extends State<PlayerList> {
  int fieldCount = 0;
  int nextIndex = 0;
  // you must keep track of the TextEditingControllers if you want the values to persist correctly
  List<TextEditingController> controllers = <TextEditingController>[];

  // create the list of TextFields, based off the list of TextControllers
  List<Widget> _buildList() {
    int i;
    // fill in keys if the list is not long enough (in case we added one)
    if (controllers.length < fieldCount) {
      for (i = controllers.length; i < fieldCount; i++) {
        controllers.add(TextEditingController());
      }
    }

    i = 0;
    // cycle through the controllers, and recreate each, one per available controller
    return controllers.map<Widget>((TextEditingController controller) {
      int displayNumber = i + 1;
      i++;
      return TextField(
        controller: controller,
        maxLength: 20,
        decoration: InputDecoration(
          labelText: "Player $displayNumber",
          counterText: "",
          prefixIcon: const Icon(Icons.person),
          suffixIcon: IconButton(
            icon: Icon(Icons.clear),
            onPressed: () {
              // when removing a TextField, you must do two things:
              // 1. decrement the number of controllers you should have (fieldCount)
              // 2. actually remove this field's controller from the list of controllers
              setState(() {
                fieldCount--;
                controllers.remove(controller);
              });
            },
          ),
        ),
      );
    }).toList(); // convert to a list
  }


  @override
  Widget build(BuildContext context) {
    // generate the list of TextFields
    final List<Widget> children = _buildList();

    // append an 'add player' button to the end of the list
    children.add(
      GestureDetector(
        onTap: () {
          // when adding a player, we only need to inc the fieldCount, because the _buildList()
          // will handle the creation of the new TextEditingController
          setState(() {
            fieldCount++;
          });
        },
        child: Container(
          color: Colors.blue,
          child: Padding(
            padding: const EdgeInsets.all(16),
            child: Text(
              'add player',
              style: TextStyle(
                color: Colors.white,
              ),
            ),
          ),
        ),
      ),
    );

    // build the ListView
    return ListView(
      padding: EdgeInsets.all(0),
      shrinkWrap: true,
      physics: NeverScrollableScrollPhysics(),
      children: children,
    );
  }

  @override
  void initState() {
    super.initState();

    // upon creation, copy the starting count to the current count
    fieldCount = widget.initialCount;
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  void didUpdateWidget(PlayerList oldWidget) {
    super.didUpdateWidget(oldWidget);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
  }
}

通过上述步骤,您可以:

  • 启动应用程序
  • 将玩家2更改为“bob”
  • 将玩家3更改为“steve”
  • 将玩家4更改为“charles”
  • 删除玩家3
  • 观察到玩家2是“bob”,而新的玩家3是“charles”

我认为这就是您在寻找的内容。


非常感谢您的帮助和详细解答 :) - Thibaut FAURE
我尝试做到这一点:当用户点击“添加玩家”按钮时,自动滚动到页面底部。我添加了一个ScrollController但不起作用。ScrollController _scrollController = new ScrollController(); if (controllers.length < fieldCount) { for (i = controllers.length; i < fieldCount; i++) { controllers.add(TextEditingController()); _scrollController.animateTo( 0.0, curve: Curves.easeOut, duration: const Duration(milliseconds: 300), ); } } - Thibaut FAURE
你的滚动控制器需要是一个不在每次重绘时重新创建的变量。你需要将它作为_PlayerListState类的属性,并仅在initState()函数中初始化它。然后在_buildList()函数中将其附加为ListView的控制器。接着,在onTap回调函数中执行_scrollController.animateTo(),这样就能正常工作了。如果你把它放在构建阶段期间的setState()方法内,则会出现错误。因此请将它移至onTap回调函数中。 - loushou
我不明白我该做什么:“然后将其作为_buildList()函数中ListView的控制器附加”,因为_buildList()函数中没有ListView。在_PlayerListState类中,我做了:ScrollController _scrollController = new ScrollController();。在initState()函数中,_scrollController = ScrollController();。在ListView中,controller: _scrollController, - Thibaut FAURE
1
回答很好,但是在 dispose() 覆盖函数中调用每个控制器的 .dispose() 方法不是一种良好的做法吗? - Brandon Pillay
3
如果一个控制器被移除了,它是否仍需要进行dispose()处理? - Brandon Pillay

0
您可以拥有一个属性来检查用户是否点击了该按钮,并根据值显示/隐藏TextField。以下我只使用布尔属性,如果用户单击X按钮,则将hideField设置为true,TextField将被替换为零大小的小部件。
 new ListView.builder(
                    padding: EdgeInsets.all(0),
                    shrinkWrap: true,
                    physics: NeverScrollableScrollPhysics(),
                    itemCount: 5,
                    itemBuilder: (context, index) {
                      print(index);
                      bool hideField = false; // I added it here

                      return hideField ? SizedBox.shrink() : TextField(
                        maxLength: 20,
                        decoration: InputDecoration(
                          labelText: "Player ${index + 1}",
                          counterText: "",
                          prefixIcon: const Icon(Icons.person),
                          suffixIcon: new IconButton(
                              icon: Icon(Icons.clear),
                              onPressed: () =>
                                  setState(() {
                                    hideField = true; // Now it works
                                  })
                          ),
                        ),
                      );
                    }
                )

这将有两个问题。1. 您只能拥有5个文本字段,2. 当您删除其中一个字段时,其他字段的值将无法按正确顺序保留。 - loushou
  1. 请注意,我只是在更新当前文本字段的状态。
  2. 其他值将保持不变,因为我没有重建整个ListView,而是用大小为零的小部件替换了TextField。
- Pedro Massango
将其插入模拟器中。我指出的两个问题是实际存在的问题。 - loushou

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接