【问题标题】:How to open DropdownButton when other widget is tapped, in Flutter?如何在 Flutter 中点击其他小部件时打开 DropdownButton?
【发布时间】:2019-12-23 01:25:17
【问题描述】:

当点击其他小部件时,我需要以编程方式打开/显示DropdownButton 的选项列表。我知道这可能不是 UI 最佳实践,但我需要这种行为:

例如,在类似下面的结构中,我可能需要点击Text("every") 来打开相邻的DropdownButton 的下拉列表,其行为类似于在HTML 中单击<select> 的标签。

Row(children: [
  Padding(
    padding: const EdgeInsets.only(right: 16),
    child: Text('every'),
  ),
  Expanded(
    child: DropdownButton<String>(
      value: _data['every'],
      onChanged: (String val) => setState(() => _data['every'] = val),
      items: _every_options.map<DropdownMenuItem<String>>(
        (String value) {
          return DropdownMenuItem<String>(
            value: value,
            child: Text(value),
          );
        },
      ).toList(),
      isExpanded: true,
    ),
  ),
]);

注意:我需要 此问题的一般解决方案,而不仅仅是如何使 Text 在下面的树。它可能需要通过可能更远的按钮等触发才能打开。

【问题讨论】:

  • DropdownButton 小部件无法实现。如果维护人员同意这是一个有用的补充,你也许可以在 PR 中将该功能添加到 Flutter 存储库中。

标签: flutter dart


【解决方案1】:

这是(众多)设计的 API 限制之一...

完成您想要的最简单的方法,无需修改 SDK,复制 dropdown.dart,并创建您自己的版本,比如说 custom_dropdown.dart,并将代码粘贴到那里...

在第 546 行,将类重命名为 CustomDropdownButton,并在第 660 和 663 行将 _DropdownButtonState 重命名为 CustomDropdownButtonState,(我们需要将状态类暴露在文件之外)。

现在你可以用它做任何你想做的事, 尽管您对 _handleTap() 感兴趣,但可以打开覆盖菜单选项。

不要公开 _handleTap() 并重构代码,而是添加另一个方法,例如:

(line 726)
void callTap() => _handleTap();

现在,更改您的代码以使用您的 DropdownButton 而不是 Flutter 的 DropdownButton,关键是“设置密钥”(全局):P

// 一些有状态的小部件实现。

  Map<String, String> _data;
  List<String> _every_options;
  // we need the globalKey to access the State.
  final GlobalKey dropdownKey = GlobalKey();

  @override
  void initState() {
    _every_options = List.generate(10, (i) => "item $i");
    _data = {'every': _every_options.first};
    simulateClick();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Row(children: [
        Padding(
          padding: const EdgeInsets.only(right: 16),
          child: Text('every'),
        ),
        Expanded(
          child: CustomDropdownButton<String>(
            key: dropdownKey,
            value: _data['every'],
            onChanged: (String val) => setState(() => _data['every'] = val),
            items: _every_options
                .map((str) => DropdownMenuItem(
                      value: str,
                      child: Text(str),
                    ))
                .toList(),
            isExpanded: true,
          ),
        ),
      ]),
    );
  }

  void simulateClick() {
    Timer(Duration(seconds: 2), () {
      // here's the "magic" to retrieve the state... not very elegant, but works.
      CustomDropdownButtonState state = dropdownKey.currentState;
      state.callTap();
    });
  }

【讨论】:

  • 非常感谢您的详细解答!现在,我厌倦了不得不以这种方式维护这个 DropdownButton 的克隆......你知道是否可能没有“更黑客”的方式来实现这一点,也许是在另一个小部件上触发/播放点击事件小部件?我是 Flutter 的初学者,但我知道其他 UI 框架总是有一些 hacky 解决方法来触发假点击/点击/等。
  • @NeuronQ 据我所知,只能通过颤振测试框架实现,所以不可行。即使 Flutter 有点新,它也非常稳定,即使是从很早的版本开始,据我所知,API 并没有改变。我怀疑你将不得不“维护”这个修改后的克隆版本......“黑客”的方式是不将小部件复制到你自己的类,并修改 sdk 代码......就像 ctr+click 一样简单在您的 IDE 中添加小部件并添加该 callTap() 函数并重命名不带 _... 的状态,但是当您进行颤振升级时,它将被覆盖。
  • @roipeker 谢谢,这个解决方案对我帮助很大。还有一点需要注意的是,你必须从自定义的 dart 文件中删除很多东西,除非你想在其中创建每个类的自定义版本,因为 dart 会因为同一个类有多个来源而对你大喊大叫。
  • 这更像是一个快速修复。但这意味着您必须自己管理这个小部件。
【解决方案2】:

另一个答案是最好的方法,但根据 cmets 中 OP 的要求,这里有两种非常“hacky”的方法来实现这一点,但没有实现自定义小部件。

1.使用GlobalKey直接访问DropdownButton小部件树

如果我们查看DropdownButton 的源代码,我们可以注意到它使用GestureDetector 来处理水龙头。但是,它不是DropdownButton 的直接后代,我们不能依赖其他小部件的树结构,因此找到检测器的唯一合理稳定的方法是递归地进行搜索。

一个例子值得一千次解释:

class DemoDropdown extends StatefulWidget {  
  @override
  InputDropdownState createState() => DemoDropdownState();
}

class DemoDropdownState<T> extends State<DemoDropdown> {
  /// This is the global key, which will be used to traverse [DropdownButton]s widget tree
  GlobalKey _dropdownButtonKey;

  void openDropdown() {
    GestureDetector detector;
    void searchForGestureDetector(BuildContext element) {
      element.visitChildElements((element) {
        if (element.widget != null && element.widget is GestureDetector) {
          detector = element.widget;
          return false;

        } else {
          searchForGestureDetector(element);
        }

        return true;
      });
    }

    searchForGestureDetector(_dropdownButtonKey.currentContext);
    assert(detector != null);

    detector.onTap();
  }

  @override
  Widget build(BuildContext context) {
    final dropdown = DropdownButton<int>(
      key: _dropdownButtonKey,
      items: [
        DropdownMenuItem(value: 1, child: Text('1')),
        DropdownMenuItem(value: 2, child: Text('2')),
        DropdownMenuItem(value: 3, child: Text('3')),
      ],
      onChanged: (int value) {},
    );

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: <Widget>[
        Offstage(child: dropdown),
        FlatButton(onPressed: openDropdown, child: Text('CLICK ME')),
      ],
    );
  }
}

2。使用Actions.invoke

Flutter 的最新功能之一是 Actions(我不确定它是什么意思,我今天才在 flutter upgrade 之后才注意到它),DropdownButton 使用它来对不同的.. . 好吧,行动。

因此,触发按钮的一种更简单的方法是找到Actions 小部件的上下文并调用必要的操作。

这种方法有两个优点:首先,Actions 小部件在树中有点高,因此遍历树不会像 GestureDetector 那样长,其次,Actions 似乎是比手势检测更通用的机制,因此它不太可能在未来从 DropdownButton 中消失。

// The rest of the code is the same
void openDropdown() {
  _dropdownButtonKey.currentContext.visitChildElements((element) {
    if (element.widget != null && element.widget is Semantics) {
      element.visitChildElements((element) {
        if (element.widget != null && element.widget is Actions) {
          element.visitChildElements((element) {
            Actions.invoke(element, Intent(ActivateAction.key));
            return false;
          });
        }
      });
    }
  });
}

【讨论】:

  • 第一个解决方案效果很好。我无法使第二个解决方案起作用,因为 Intent 不再收到参数。
  • 试过第一个,效果很好!这应该部署为一个包。
猜你喜欢
  • 1970-01-01
  • 2021-10-30
  • 2020-05-26
  • 2020-11-20
  • 2021-06-07
  • 2023-03-11
  • 2020-03-18
  • 1970-01-01
  • 2019-07-05
相关资源
最近更新 更多