Flutter TextField 自动完成覆盖

Posted

技术标签:

【中文标题】Flutter TextField 自动完成覆盖【英文标题】:Flutter TextField autocomplete overlay 【发布时间】:2017-09-24 22:25:54 【问题描述】:

我正在努力创建带有自动完成覆盖的 TextField。 我有带有 TextFields 的表单,我想根据 TextField 中输入的内容显示建议。

类似这样的: TextField autocomplete 我不确定小部件的层次结构应该是什么样子才能实现在其他小部件上方显示建议框。我应该使用 Stack 小部件、OverflowBox 小部件还是其他东西?

感谢层次结构示例的任何帮助。

【问题讨论】:

【参考方案1】:

我已经实现了一个包,flutter_typeahead 来做到这一点。在这个包中,我使用Overlay.of(context).insert,它允许我将建议列表插入叠加层,使其浮动在所有其他小部件之上。我还写了this article详细解释了如何做到这一点

【讨论】:

这是完美的。非常感谢。 @AkramChauhan flutter_typehead 与 bloc 一起使用吗?你能给我们提供一个如何用 bloc 制作的样品吗? 这个包很不错,但是,如果文本字段的一侧有边距,是否可以使覆盖扩展到整个屏幕宽度?例如,文本字段旁边的自定义取消按钮。 这仍然是 2021 年的首选解决方案吗?仍然没有官方的 Flutter 替代品可用? @SePröbläm 目前有一个包正在开发中:github.com/flutter/flutter/pull/62927,虽然不如 flutter_typeahead 成熟【参考方案2】:

我使用 Stack 为我的应用程序实现。一个容器中的 TextFormField 和另一个容器中的 ListTiles 并在文本输入字段的容器上作为用户类型覆盖 listtile。你可以看看 my app。

以下示例应用使用建议作为来自 api 的用户类型,并显示在用户可以通过点击选择的列表中。

Screenshot 1 Screenshot 2 Screenshot 3

代码示例:

import 'package:flutter/material.dart';
import 'package:search_suggestions/suggestions_page.dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget 
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    return new MaterialApp(
      title: 'Suggestions Demo',
      debugShowCheckedModeBanner: false,
      theme: new ThemeData(
        brightness: Brightness.light,
        primarySwatch: Colors.orange,
      ),
      home: new SuggestionsPage(),
    );
  

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'dart:io';
import 'dart:async';

class SuggestionsPage extends StatefulWidget 
  SuggestionsPage(Key key) : super(key: key);
  @override
  _SuggestionsPageState createState() => new _SuggestionsPageState();


class _SuggestionsPageState extends State<SuggestionsPage> 
  static const JsonCodec JSON = const JsonCodec();

  final key = new GlobalKey<ScaffoldState>();
  final TextEditingController _searchQueryController =
      new TextEditingController();
  final FocusNode _focusNode = new FocusNode();

  bool _isSearching = true;
  String _searchText = "";
  List<String> _searchList = List();
  bool _onTap = false;
  int _onTapTextLength = 0;

  _SuggestionsPageState() 
    _searchQueryController.addListener(() 
      if (_searchQueryController.text.isEmpty) 
        setState(() 
          _isSearching = false;
          _searchText = "";
          _searchList = List();
        );
       else 
        setState(() 
          _isSearching = true;
          _searchText = _searchQueryController.text;
          _onTap = _onTapTextLength == _searchText.length;
        );
      
    );
  

  @override
  void initState() 
    super.initState();
    _isSearching = false;
  

  @override
  Widget build(BuildContext context) 
    return new Scaffold(
      key: key,
      appBar: buildAppbar(context),
      body: buildBody(context),
    );
  

  Widget getFutureWidget() 
    return new FutureBuilder(
        future: _buildSearchList(),
        initialData: List<ListTile>(),
        builder:
            (BuildContext context, AsyncSnapshot<List<ListTile>> childItems) 
          return new Container(
            color: Colors.white,
            height: getChildren(childItems).length * 48.0,
            width: MediaQuery.of(context).size.width,
            child: new ListView(
//            padding: new EdgeInsets.only(left: 50.0),
              children: childItems.data.isNotEmpty
                  ? ListTile
                      .divideTiles(
                          context: context, tiles: getChildren(childItems))
                      .toList()
                  : List(),
            ),
          );
        );
  

  List<ListTile> getChildren(AsyncSnapshot<List<ListTile>> childItems) 
    if (_onTap && _searchText.length != _onTapTextLength) _onTap = false;
    List<ListTile> childrenList =
        _isSearching && !_onTap ? childItems.data : List();
    return childrenList;
  

  ListTile _getListTile(String suggestedPhrase) 
    return new ListTile(
      dense: true,
      title: new Text(
        suggestedPhrase,
        style: Theme.of(context).textTheme.body2,
      ),
      onTap: () 
        setState(() 
          _onTap = true;
          _isSearching = false;
          _onTapTextLength = suggestedPhrase.length;
          _searchQueryController.text = suggestedPhrase;
        );
        _searchQueryController.selection = TextSelection
            .fromPosition(new TextPosition(offset: suggestedPhrase.length));
      ,
    );
  

  Future<List<ListTile>> _buildSearchList() async 
    if (_searchText.isEmpty) 
      _searchList = List();
      return List();
     else 
      _searchList = await _getSuggestion(_searchText) ?? List();
//        ..add(_searchText);

      List<ListTile> childItems = new List();
      for (var value in _searchList) 
        if (!(value.contains(" ") && value.split(" ").length > 2)) 
          childItems.add(_getListTile(value));
        
      
      return childItems;
    
  

  Future<List<String>> _getSuggestion(String hintText) async 
    String url = "SOME_TEST_API?s=$hintText&max=4";

    var response =
        await http.get(Uri.parse(url), headers: "Accept": "application/json");

    List decode = JSON.decode(response.body);
    if (response.statusCode != HttpStatus.OK || decode.length == 0) 
      return null;
    
    List<String> suggestedWords = new List();

    if (decode.length == 0) return null;

    decode.forEach((f) => suggestedWords.add(f["word"]));
//    String data = decode[0]["word"];

    return suggestedWords;
  

  Widget buildAppbar(BuildContext context) 
    return new AppBar(
      title: new Text('Suggestions Demo'),
    );
  

  Widget buildBody(BuildContext context) 
    return new SafeArea(
      top: false,
      bottom: false,
      child: new SingleChildScrollView(
        padding: const EdgeInsets.symmetric(horizontal: 16.0),
        child: new Stack(
          children: <Widget>[
            new Column(
              children: <Widget>[
                Container(
                  height: MediaQuery.of(context).size.height,
                  child: new Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: <Widget>[
                      const SizedBox(height: 80.0),
                      new TextFormField(
                        controller: _searchQueryController,
                        focusNode: _focusNode,
                        onFieldSubmitted: (String value) 
                          print("$value submitted");
                          setState(() 
                            _searchQueryController.text = value;
                            _onTap = true;
                          );
                        ,
                        onSaved: (String value) => print("$value saved"),
                        decoration: const InputDecoration(
                          border: const UnderlineInputBorder(),
                          filled: true,
                          icon: const Icon(Icons.search),
                          hintText: 'Type two words with space',
                          labelText: 'Seach words *',
                        ),
                      ),

                      const SizedBox(height: 40.0),
                      new Center(
                        child: new RaisedButton(
                            color: Colors.orangeAccent,
                            onPressed: () => print("Pressed"),
                            child: const Text(
                              '    Search    ',
                              style: const TextStyle(fontSize: 18.0),
                            )),
                      ),
                      const SizedBox(height: 200.0),
                    ],
                  ),
                ),
              ],
            ),
            new Container(
                alignment: Alignment.topCenter,
                padding: new EdgeInsets.only(
//                  top: MediaQuery.of(context).size.height * .18,
                    top: 136.0,
                    right: 0.0,
                    left: 38.0),
                child: _isSearching && (!_onTap) ? getFutureWidget() : null)
          ],
        ),
      ),
    );
  

【讨论】:

这是完美的解决方案 +1。谢谢@Ronin【参考方案3】:

您可以利用autocomplete_textfield 库来实现这一点。

基本用法

  ...

                SimpleAutoCompleteTextField(
                  key: key,
                  suggestions: [
                    "Apple",
                    "Armidillo",
                    "Actual",
                    "Actuary",
                    "America",
                    "Argentina",
                    "Australia",
                    "Antarctica",
                "Blueberry",],
                  decoration: InputDecoration(
                    filled: true,
                    fillColor: Colors.black12,
                    hintText: 'Dictionary'
                  ),
                ),

                ...  

您可以获取更多示例here。

另一种选择

你也可以使用flutter_typeahead

在使用 StreamBuilderfor BLoC 模式时,这对我来说效果更好。

【讨论】:

您是否找到了将验证器与 SimpleAutoCompleteTextField 一起使用的方法?我在电子邮件字段中使用它,我想在上面运行我的基本电子邮件验证器?【参考方案4】:

我可能会有一个高度固定的容器,其中包含一个其 crossAxisAlignment 设置为拉伸的列。列中的第一个子项将是文本字段。第二个将是一个 Expanded,其中包含一个 ListView 和一个自定义委托来提供孩子。然后,随着文本字段中的数据发生变化,您更新委托,以便更新子代。每个子项都是一个包含 InkWell 的 ListTile,当点击该 InkWell 时,会适当地填充文本字段。

【讨论】:

好的,谢谢。如果我想让建议高于其他小部件,我应该使用Overlay.of(context).insert(entryWithExpandedListView);吗? @guest3523 您是否已成功完成自动完成覆盖?我正在尝试为我的应用程序做同样的事情,到目前为止只有 3 天的颤振体验,我很难用它。

以上是关于Flutter TextField 自动完成覆盖的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Container 中垂直扩展 TextField 以覆盖 Flutter 中的所有可用空间

Flutter 项目实战 编辑框(TextField) 自定义 七

当 React Material UI 中的 TextField 中存在值时自定义自动完成 CSS

2020-11-18 解决Flutter TextField限制输入中文问题

Flutter TextField在键盘上溢出底部

Flutter中TextField的TextScaleFactor?