Flutter自动完成文本输入插件type_ahead_text_field的使用

发布于 1周前 作者 phonegap100 来自 Flutter

Flutter自动完成文本输入插件type_ahead_text_field的使用

核心功能

  1. 前缀触发回调

    • 创建回调函数,当用户输入指定前缀(例如 @#)时触发。此功能包括处理用户在输入更多内容时将光标移出包含匹配前缀的单词的情况。
  2. 动态数据建议

    • 根据检测到的前缀建议数据列表。例如,@ 用于朋友,# 用于热门标签。
  3. 多前缀检测

    • 启用单个 TextField 处理多种类型的前缀,如 @# 等。
  4. 基于前缀的建议

    • 根据检测到的前缀动态生成建议项。
  5. 上下文建议对话框

    • 将建议对话框放置在检测到的前缀位置。确保当 TextField 光标直接位于前缀之后时显示对话框。一旦控制器检测到前缀,可以获得 PrefixMatchState
  6. 响应式建议对话框

    • 随着用户输入调整建议对话框的位置,考虑 TextField 光标的偏移变化。对话框的 Y 偏移是可变的,而宽度保持不变(例如,类似于 Facebook 的建议对话框)。TypeAheadTextFieldController 还支持检查 X 偏移以进行更大程度的自定义。
  7. 滚动处理

    • 如果文本长度变得过长导致 TextField 滚动,则重新计算 X 偏移。这是通过提供 TextField 控制器并动态检索位置来实现的。
  8. 可定制的 TextSpan

    • 自定义 TextSpan 以匹配不同前缀的单词。这包括添加动态数据,如 UserModelTagModelCustomSpan 仅应用于已添加到批准数据的文本。
  9. OnRemove 回调

    • 监听 onRemove 回调以处理用户添加项目后删除文本的情况,允许根据已删除的内容进行适当的调整。

示例代码

以下是 flutter-type-ahead-text-field 插件的完整示例代码,展示了如何实现上述功能:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:type_ahead_text_field/type_ahead_text_field.dart';
import 'package:rxdart/rxdart.dart';

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

class MyApp extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  final String title;

  const MyHomePage({Key? key, required this.title}) : super(key: key);

  [@override](/user/override)
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late final TypeAheadTextFieldController controller;
  final List<SuggestedDataWrapper> data = [];
  OverlayEntry? overlayEntry;
  final GlobalKey<EditableTextState> tfKey = GlobalKey();
  final ValueNotifier<PrefixMatchState?> matchedState = ValueNotifier(null);
  final List<String> users = ['john', 'josh', 'lucas', 'don', 'will'];
  final List<String> tags = ['meme', 'challenge', 'city'];
  PrefixMatchState? filterState;
  GlobalKey suggestionWidgetKey = GlobalKey();
  bool readOnly = false;

  BehaviorSubject<PrefixMatchState?> bhMatchedState = BehaviorSubject();

  [@override](/user/override)
  void initState() {
    super.initState();
    // 初始化数据
    data.addAll(users.map((e) => SuggestedDataWrapper(id: e, prefix: '@')));
    data.addAll(tags.map((e) => SuggestedDataWrapper(id: e, prefix: '#')));

    // 创建控制器
    controller = TypeAheadTextFieldController(
      appliedPrefixes: {'@', '#'}, // 应用的前缀
      suggestibleData: data.toSet(), // 可建议的数据
      textFieldKey: tfKey, // TextField 的 GlobalKey
      edgePadding: EdgeInsets.all(6), // 边缘填充
      onRemove: (data) {
        WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
          setState(() {});
        });
      },
      customSpanBuilder: (SuggestedDataWrapper data) {
        return customSpan(data); // 自定义 TextSpan
      },
      onStateChanged: (PrefixMatchState? state) {
        if (state != null && (filterState == null || filterState != state)) {
          filterState = state;
          matchedState.value = state;
          bhMatchedState.add(state);
        }

        if (state != null) {
          if (overlayEntry == null) {
            showSuggestionDialog(); // 显示建议对话框
          }
        } else {
          removeOverlay(); // 移除建议对话框
        }
      },
    );
  }

  void showSuggestionDialog() {
    Size size = Size(200, 300); // 对话框大小

    overlayEntry = OverlayEntry(builder: (context) {
      return StreamBuilder<PrefixMatchState?>(
        stream: controller.matchStateListStream, // 监听匹配状态
        builder: (context, matchedState) {
          List<SuggestedDataWrapper>? filteredData =
              controller.matchedSuggestionListStream.value; // 获取过滤后的数据

          Offset? offset = matchedState.data == null
              ? null
              : controller.calculateGlobalOffset(
                  context: context,
                  localOffset: matchedState.data!.offset,
                  overlayContainerSize: size); // 计算全局偏移

          return offset != null && filteredData != null
              ? Stack(
                  children: [
                    AnimatedPositioned(
                      key: suggestionWidgetKey,
                      duration: Duration(milliseconds: 250),
                      left: offset.dx,
                      top: offset.dy,
                      child: Material(
                        color: Colors.transparent,
                        child: Card(
                          child: Container(
                            height: 300,
                            width: 200,
                            child: Column(
                              children: [
                                Container(
                                  height: 40,
                                  alignment: Alignment.topRight,
                                  child: IconButton(
                                    onPressed: () {
                                      removeOverlay(); // 关闭对话框
                                    },
                                    icon: Icon(Icons.close),
                                  ),
                                ),
                                Text('Filter: ${filterState?.text}'), // 显示过滤条件
                                Flexible(
                                  child: ConstrainedBox(
                                    constraints: BoxConstraints(minHeight: 200),
                                    child: ListView.builder(
                                      itemBuilder: (context, index) {
                                        var item = filteredData[index];
                                        return GestureDetector(
                                          child: ListTile(
                                            title: Text('${item.id}'), // 显示建议项
                                          ),
                                          onTap: () {
                                            controller.approveSelection(
                                                filterState!, item); // 确认选择
                                            removeOverlay();
                                            setState(() {});
                                          },
                                        );
                                      },
                                      itemCount: filteredData.length,
                                    ),
                                  ),
                                )
                              ],
                            ),
                          ),
                        ),
                      ),
                    ),
                  ],
                )
              : Container();
        },
      );
    });

    Overlay.of(context).insert(overlayEntry!); // 插入 Overlay
  }

  TextSpan customSpan(SuggestedDataWrapper data) {
    return TextSpan(
      text: data.id,
      style: TextStyle(color: Colors.blue), // 自定义样式
      recognizer: readOnly
          ? (TapGestureRecognizer()
            ..onTap = () {
              showAboutDialog(
                  context: context,
                  children: [Text('${data.prefix}${data.id}')]); // 长按显示详细信息
            })
          : null,
    );
  }

  void removeOverlay() {
    try {
      overlayEntry?.remove(); // 移除 Overlay
      overlayEntry = null;
    } catch (e) {}
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          children: [
            Container(
              child: Text(
                'Added data: ${controller.getApprovedData().map((e) => '${e.prefix}${e.id}').toString()}',
                style: TextStyle(color: Colors.blue), // 显示已添加的数据
              ),
              padding: EdgeInsets.only(bottom: 24, top: 24),
            ),
            Container(
              decoration: BoxDecoration(
                color: Colors.grey.shade200,
                borderRadius: BorderRadius.all(Radius.circular(20)),
              ),
              padding: EdgeInsets.all(20),
              margin: EdgeInsets.all(20),
              child: TextField(
                key: tfKey,
                readOnly: readOnly, // 是否只读
                scrollController: controller.scrollController, // 滚动控制器
                controller: controller, // 文本控制器
                maxLines: 6, // 最大行数
                decoration: InputDecoration.collapsed(hintText: 'Description'), // 提示文本
                cursorHeight: 14,
                cursorWidth: 2,
                onSubmitted: (s) {
                  removeOverlay(); // 提交时关闭建议对话框
                },
              ),
            ),
            ElevatedButton(
              onPressed: () {
                readOnly = !readOnly; // 切换编辑/保存模式
                setState(() {});
              },
              child: Text('${readOnly ? 'EDIT' : 'SAVE'}'),
            )
          ],
        ),
      ),
    );
  }

  [@override](/user/override)
  void dispose() {
    removeOverlay(); // 清理 Overlay
    super.dispose();
    matchedState.dispose(); // 清理 ValueNotifier
  }
}

更多关于Flutter自动完成文本输入插件type_ahead_text_field的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter自动完成文本输入插件type_ahead_text_field的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter中使用type_ahead_text_field插件来实现自动完成文本输入的示例代码。这个插件可以帮助你在用户输入时提供建议列表,从而提高用户体验。

首先,确保你已经在pubspec.yaml文件中添加了type_ahead_text_field依赖:

dependencies:
  flutter:
    sdk: flutter
  type_ahead_text_field: ^2.0.0  # 请根据需要调整版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Dart文件中使用TypeAheadTextField组件。以下是一个完整的示例代码:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'TypeAheadTextField Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final List<String> suggestions = [
    'Apple',
    'Banana',
    'Cherry',
    'Date',
    'Elderberry',
    'Fig',
    'Grape',
    'Honeydew',
    'Indian Fig',
    'Jackfruit',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TypeAheadTextField Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: TypeAheadField<String>(
          textFieldConfiguration: TextFieldConfiguration(
            autofocus: true,
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              labelText: 'Search...',
              suffixIcon: Icon(Icons.search),
            ),
          ),
          suggestionsCallback: (pattern) async {
            return suggestions.where((suggestion) =>
              suggestion.toLowerCase().contains(pattern.toLowerCase())
            ).toList();
          },
          onSuggestionSelected: (suggestion) {
            // 当用户选择一个建议时的回调
            print("User selected: $suggestion");
          },
        ),
      ),
    );
  }
}

代码解释

  1. 导入必要的包

    • package:flutter/material.dart 用于Flutter的基础UI组件。
    • package:type_ahead_text_field/type_ahead_text_field.dart 用于TypeAheadTextField组件。
  2. 创建主应用

    • MyApp 类定义了应用的入口,并设置了主题。
  3. 创建主页

    • MyHomePage 是一个有状态的组件,用于管理UI的状态。
    • 在状态类中,定义了一个包含建议的字符串列表 suggestions
  4. 构建UI

    • 使用 ScaffoldAppBar 创建基本的页面布局。
    • 使用 Padding 添加一些内边距。
    • 使用 TypeAheadField 组件来实现自动完成文本输入功能。
      • textFieldConfiguration 用于配置文本字段的外观和行为。
      • suggestionsCallback 是一个异步函数,根据用户输入的模式返回匹配的建议列表。
      • onSuggestionSelected 是一个回调函数,当用户选择一个建议时执行。

这个示例展示了如何使用type_ahead_text_field插件来创建一个具有自动完成功能的文本输入字段。你可以根据需要对建议列表、UI样式和回调函数进行进一步的自定义。

回到顶部