Flutter光标自动补全选项插件cursor_autocomplete_options的使用
Flutter光标自动补全选项插件cursor_autocomplete_options的使用
Flutter已经有一个自动完成小部件。但是它并不符合我们在桌面和Web版本上通常看到的UI模式。这是因为自动完成部分位于文本框下方,并且我们无法通过自动完成API更改这一点。 为了满足这一需求,此插件被创建出来。目的是在光标指示器正下方显示一个选项列表视图,每个自动完成功能选项都以列表磁贴的形式显示在一个覆盖层中。
🌟 测试实时演示!
您可以在在线Web演示中测试该插件: 测试插件
Getting started(开始使用)
First, import the package(首先导入包)
import 'package:cursor_autocomplete_options/cursor_autocomplete_options.dart';
Setting variables(设置变量)
在包含文本框的小部件内,该小部件需要是状态化的,以便在使用后释放资源。我们将创建以下小部件:
late final OptionsController<String> optionsController;
final FocusNode textfieldFocusNode = FocusNode();
final TextEditingController textEditingController = TextEditingController();
⚠️ 重要!
我们需要为文本框创建一个焦点节点,因为OptionsController需要能够重新聚焦文本框,而不仅仅是列表磁贴。此外,应用程序需要知道文本框的位置,以便计算字符应放置的位置。因此,请不要忘记将textfield focus node
和textEditingController
添加到您的TextFormField
小部件中。
After that, in the init state we will initialize the OptionsController(接下来,在初始化状态中我们将初始化OptionsController)
[@override](/user/override)
void initState() {
super.initState();
optionsController = OptionsController<String>(
textfieldFocusNode: textfieldFocusNode,
textEditingController: textEditingController,
context: context,
selectInCursorParser: (option) {
return option;
},
);
}
⚠️ 重要!
如果您的上下文经常更新,那么您在OptionsController的context
参数中使用的上下文可能会变得过时并且不再有效。因此,您需要更新它。您可以通过在状态化小部件的build
方法中调用控制器的updateContext(context)
函数来实现这一点。这在您的小部件从未重建且上下文从不被废弃的情况下不是问题。
[@override](/user/override)
Widget build(BuildContext context) {
// 在状态化小部件的build方法中添加此行,文本框将包含在此处:
optionsController.updateContext(context);
return ... // 您的小部件
}
About generic type <T> of OptionsController(关于OptionsController的泛型类型<T>)
OptionsController接收一个泛型T值。这可以是任何模型,但如果您只想在光标中添加文本,则可以使用<String>作为泛型类型。
⚠️ 重要!
如果<T>不同于String,您需要传递optionAsString
字段将其转换为字符串。因此,如果您不使用<String>作为泛型类型,则此参数成为必需。否则,如果泛型类型为<String>,则无需传递此参数,因为该包已经知道它是字符串。
Now, you can use the textfield with the values we created(现在,您可以使用我们创建的文本框)
TextFormField(
focusNode: textfieldFocusNode, // 不要忘记!
controller: textEditingController, // 不要忘记!
style: const TextStyle(height: 1, fontSize: 18), // 高度参数必须为1。
...
),
⚠️ 重要!
您需要在文本框中初始化一个名为style
的字段,并将其height
参数设置为1。不能是其他值。
文本样式中的其他值,如fontSize,可以是你喜欢的任何值。
⚠️ 重要!
不要忘记在最后释放控制器和文本框。注意:您不需要释放文本框焦点节点,因为OptionsController.dispose()
会为您处理。请参见以下示例:
[@override](/user/override)
void dispose() {
super.dispose();
// 不要忘记释放!
optionsController.dispose();
textEditingController.dispose();
}
Usage(使用)
您可以随时触发在光标位置显示对话框。记住,文本框必须处于焦点状态,以便该包识别光标及其位置。为此,该包将使用文本框的焦点节点来查看是否已聚焦。
要触发带有选项的覆盖层显示,您将在OptionsController控制器上调用showOptionsMenu()
函数,并传递您想要给用户的选项列表。此函数将接收建议参数,即类型为<T>的项目列表,与您的OptionController<T>相同。
在触发showOptionsMenu()
函数后,它将显示带有选项的对话框,然后在用户选择选项后,应用将触发两个函数onTextAddedCallback
和selectInCursorParser
(如果它们不是null)。
例如,当用户在文本框中输入“#”时,可以触发它。但这完全取决于你何时触发它。以下是这种情况的一个示例:
TextFormField(
... // 其他属性
onChanged: (value) {
if (value.isEmpty) return;
final cursorPositionIndex =
textEditingController.selection.base.offset;
final typedValue = value[cursorPositionIndex - 1];
final isTypedCaracterHashtag = typedValue == '#';
if (isTypedCaracterHashtag) {
optionsController.showOptionsMenu(suggestion);
}
},
)
Customize the card widget(自定义卡片小部件)
想自定义您的卡片小部件吗?
使用showOptionsMenuWithWrapperBuilder
并创建一个包装器,上面有带选项的ListView。
Manipulating selections(操作选择)
您可以使用两个主要功能onTextAddedCallback
和selectInCursorParser
来操纵和确定将对选择执行的操作。
onTextAddedCallback(onTextAddedCallback)
将为您提供完全控制选定选项的操作。因此,如果您愿意,您可以自行处理文本框的值。
selectInCursorParser(selectInCursorParser)
这是一个预先构建的选项,建立在onTextAddedCallback之上,用于在当前光标位置插入此函数的返回值。还可以通过传递cursorIndexChangeQuantity
在返回负载中更改光标位置。有关更多信息,请参阅InsertInCursorPayload
。
Configurations(配置)
Change card size(更改卡片大小)
在OptionsController
内部,您可以使用字段overlayCardHeight
和overlayCardWeight
来操作选项列表视图的宽度和高度。
Change item tile widget(更改项磁贴小部件)
在使用showOptionsMenuWithWrapperBuilder
和showOptionsMenu
函数时,您可以指定tileBuilder
。
这样,您可以使用自定义小部件来显示所选选项。
⚠️ 重要:
使用焦点节点为用户提供视觉反馈,所选磁贴的高度可以通过tileHeight
参数进行编辑。
Debounce(防抖)
您可以在选项覆盖层触发时配置防抖。默认值为300毫秒。
Controll when to close dialog(控制何时关闭对话框)
也许,您不想在onTextAddedCallback
或selectInCursorParser
中执行登录后关闭对话框。因此,您可以将willAutomaticallyCloseDialogAfterSelection
字段设置为false,这样对话框在选择值后将停止关闭。
完整示例
import 'package:cursor_autocomplete_options/cursor_autocomplete_options.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
textSelectionTheme: const TextSelectionThemeData(
selectionHandleColor: Colors.transparent,
),
),
home: const Example(),
);
}
}
enum SelectedType {
simple,
complex;
}
class Example extends StatefulWidget {
const Example({super.key});
[@override](/user/override)
State<Example> createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
late final OptionsController<String, String> optionsController;
final FocusNode textfieldFocusNode = FocusNode();
final TextEditingController textEditingController = TextEditingController();
[@override](/user/override)
void initState() {
super.initState();
optionsController = OptionsController<String, String>(
textfieldFocusNode: textfieldFocusNode,
textEditingController: textEditingController,
context: context,
selectInCursorParser: (option) {
return InsertInCursorPayload(text: option);
},
);
}
[@override](/user/override)
void dispose() {
super.dispose();
// 不要忘记释放!
optionsController.dispose();
textEditingController.dispose();
}
AlignmentOptions alignmentOption = AlignmentOptions.center;
double width = 300;
int maxLines = 5;
// final Set<SelectedType> selectedTypes = {SelectedType.simple};
[@override](/user/override)
Widget build(BuildContext context) {
optionsController.updateContext(context);
return Scaffold(
appBar: AppBar(
title: const Text('Flutter textfield sugestions demo'),
leadingWidth: 310,
leading: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 16),
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) {
return Dialog(
child: ChangeFields(
value: width.round(),
textfieldLines: maxLines,
onSave: (newWidth, maxLines) {
setState(() {
width = newWidth;
this.maxLines = maxLines;
});
},
),
);
},
);
},
icon: const Icon(Icons.settings),
),
const SizedBox(width: 16),
// SegmentedButton<SelectedType>(
// segments: SelectedType.values
// .map(
// (t) =>
// ButtonSegment(
// value: t,
// label: Text(t.name),
// ),
// )
// .toList(),
// selected: selectedTypes,
// onSelectionChanged: (p0) {
// setState(() {
// selectedTypes.clear();
// selectedTypes.addAll(p0);
// });
// },
// ),
],
),
actions: [
DropdownButton<AlignmentOptions>(
value: alignmentOption,
items: AlignmentOptions.values.map((option) {
return DropdownMenuItem<AlignmentOptions>(
value: option,
child: Text(option.text),
);
}).toList(),
onChanged: (selectedValue) {
if (selectedValue == null) return;
setState(() {
alignmentOption = selectedValue;
});
},
),
],
),
body: Align(
alignment: alignmentOption.alignment,
child: Container(
color: Colors.grey[200],
width: width,
child: Builder(builder: (context) {
return TextFormField(
focusNode: textfieldFocusNode,
controller: textEditingController,
decoration: const InputDecoration(
hintText: 'Type something and use "#" anytime to show options',
),
style: const TextStyle(height: 1, fontSize: 18),
maxLines: maxLines,
onChanged: (value) {
if (value.isEmpty) return;
final cursorPositionIndex =
textEditingController.selection.base.offset;
final typedValue = value[cursorPositionIndex - 1];
final isTypedCaracterHashtag = typedValue == '#';
if (isTypedCaracterHashtag) {
optionsController.showSimpleOptions(
children: complexSuggestion,
folderOptionAsString: (option) {
return option;
},
fileOptionAsString: (option) {
return option;
},
);
}
},
);
}),
),
),
);
}
}
final List<StructuredDataType<String, String>> complexSuggestion = [
const FolderStructure(item: 'Folder 1', children: [
FolderStructure(item: 'Folder 1.1', children: [
FolderStructure(item: 'Folder 2.1.1', children: [
FileStructureOptions(item: 'File 2.1.1'),
FileStructureOptions(item: 'File 2.1.2'),
FileStructureOptions(item: 'File 2.1.3'),
]),
FileStructureOptions(item: 'File 1.1.1'),
FileStructureOptions(item: 'File 1.1.2'),
FileStructureOptions(item: 'File 1.1.3'),
]),
FolderStructure(item: 'Folder 1.2', children: [
FileStructureOptions(item: 'File 1.2.1'),
FileStructureOptions(item: 'File 1.2.2'),
FileStructureOptions(item: 'File 1.2.3'),
]),
FolderStructure(item: 'Folder 1.3', children: [
FileStructureOptions(item: 'File 1.3.1'),
FileStructureOptions(item: 'File 1.3.2'),
FileStructureOptions(item: 'File 1.3.3'),
]),
]),
const FolderStructure(item: 'Folder 2', children: [
FolderStructure(item: 'Folder 2.1', children: [
FileStructureOptions(item: 'File 2.1.1'),
FileStructureOptions(item: 'File 2.1.2'),
FileStructureOptions(item: 'File 2.1.3'),
]),
FolderStructure(item: 'Folder 2.2', children: [
FileStructureOptions(item: 'File 2.2.1'),
FileStructureOptions(item: 'File 2.2.2'),
FileStructureOptions(item: 'File 2.2.3'),
]),
FolderStructure(item: 'Folder 2.3', children: [
FileStructureOptions(item: 'File 2.3.1'),
FileStructureOptions(item: 'File 2.3.2'),
FileStructureOptions(item: 'File 2.3.3'),
]),
]),
const FolderStructure(item: 'Folder 3', children: [
FolderStructure(item: 'Folder 3.1', children: [
FileStructureOptions(item: 'File 3.1.1'),
FileStructureOptions(item: 'File 3.1.2'),
FileStructureOptions(item: 'File 3.1.3'),
]),
FolderStructure(item: 'Folder 3.2', children: [
FileStructureOptions(item: 'File 3.2.1'),
FileStructureOptions(item: 'File 3.2.2'),
FileStructureOptions(item: 'File 3.2.3'),
]),
FolderStructure(item: 'Folder 3.3', children: [
FileStructureOptions(item: 'File 3.3.1'),
FileStructureOptions(item: 'File 3.3.2'),
FileStructureOptions(item: 'File 3.3.3'),
]),
]),
const FileStructureOptions(item: 'File 1'),
const FileStructureOptions(item: 'File 2'),
const FileStructureOptions(item: 'File 3'),
const FileStructureOptions(item: 'File 4'),
const FileStructureOptions(item: 'File 5'),
const FileStructureOptions(item: 'File 6'),
];
final List<String> simpleSuggestion = [
'Floor',
'Bar',
'Manana',
'Lerolero',
'Idensa',
'Yaha',
'Tysaki',
'Ruyma',
'Rolmuro',
'Ehuka',
'Yah',
];
enum AlignmentOptions {
center(Alignment.center, '- Center'),
topLeft(Alignment.topLeft, '↖ Top left'),
topCenter(Alignment.topCenter, '↑ Top center'),
topRight(Alignment.topRight, '↗ Top right'),
centerLeft(Alignment.centerLeft, '← Center left'),
centerRight(Alignment.centerRight, '→ Center right'),
bottomLeft(Alignment.bottomLeft, '↙ Bottom left'),
bottomCenter(Alignment.bottomCenter, '↓ Bottom center'),
bottomRight(Alignment.bottomRight, '↘ Bottom right');
final Alignment alignment;
final String text;
const AlignmentOptions(this.alignment, this.text);
}
class ChangeFields extends StatefulWidget {
final int value;
final int textfieldLines;
final void Function(double newWidth, int maxLines) onSave;
const ChangeFields({
super.key,
required this.value,
required this.textfieldLines,
required this.onSave,
});
[@override](/user/override)
State<ChangeFields> createState() => _ChangeFieldsState();
}
class _ChangeFieldsState extends State<ChangeFields> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late TextEditingController widthEC;
[@override](/user/override)
void initState() {
super.initState();
widthEC = TextEditingController(text: '${widget.value}');
maxLines = widget.textfieldLines;
}
[@override](/user/override)
void dispose() {
widthEC.dispose();
super.dispose();
}
late int maxLines;
[@override](/user/override)
Widget build(BuildContext context) {
return Container(
width: 300,
height: 210,
margin: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Width of textfield',
style: TextStyle(fontSize: 20),
),
const SizedBox(height: 4),
Form(
key: _formKey,
child: TextFormField(
controller: widthEC,
validator: (value) {
final valueParsed = int.tryParse(value ?? 'a');
return valueParsed == null || valueParsed < 150
? 'Need to be a number bigger then 150'
: null;
},
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
autovalidateMode: AutovalidateMode.onUserInteraction,
),
),
const SizedBox(height: 8),
Row(
children: [
const Text(
'Max lines',
style: TextStyle(fontSize: 20),
),
const Spacer(),
SizedBox(
width: 50,
child: DropdownButton<int>(
value: maxLines,
items: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((option) {
return DropdownMenuItem<int>(
value: option,
child: Text('$option'),
);
}).toList(),
onChanged: (selectedValue) {
if (selectedValue == null) return;
setState(() {
maxLines = selectedValue;
});
},
),
),
],
),
const Spacer(),
ElevatedButton.icon(
onPressed: () {
final parsedWidth = double.tryParse(widthEC.text);
if (parsedWidth == null) return;
widget.onSave(parsedWidth, maxLines);
Navigator.pop(context);
},
icon: const Icon(Icons.save),
label: const Text('Save'),
),
],
),
);
}
}
更多关于Flutter光标自动补全选项插件cursor_autocomplete_options的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter光标自动补全选项插件cursor_autocomplete_options的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,下面是一个关于如何使用 cursor_autocomplete_options
插件的 Flutter 代码示例。这个插件允许你在文本字段中实现光标自动补全选项的功能。为了简化示例,假设你已经将 cursor_autocomplete_options
插件添加到了你的 pubspec.yaml
文件中。
首先,确保你的 pubspec.yaml
文件中包含以下依赖:
dependencies:
flutter:
sdk: flutter
cursor_autocomplete_options: ^最新版本号 # 请替换为最新的版本号
然后,运行 flutter pub get
来获取依赖。
接下来是一个完整的 Flutter 应用示例,展示了如何使用 cursor_autocomplete_options
插件:
import 'package:flutter/material.dart';
import 'package:cursor_autocomplete_options/cursor_autocomplete_options.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 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',
];
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Cursor Autocomplete Options Demo'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AutocompleteTextField<String>(
controller: _controller,
decoration: InputDecoration(
labelText: 'Type something...',
border: OutlineInputBorder(),
),
optionsBuilder: (String query) {
if (query.isEmpty) {
return [];
}
return suggestions.where((String suggestion) {
return suggestion.toLowerCase().contains(query.toLowerCase());
}).toList();
},
onSelected: (String value) {
// 当用户选择一个选项时,这里可以处理选择事件
print('Selected: $value');
},
fieldViewBuilder: (
BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onEditingComplete,
) {
return TextField(
controller: textEditingController,
focusNode: focusNode,
onEditingComplete: onEditingComplete,
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
);
},
),
],
),
),
);
}
}
在这个示例中,我们创建了一个 AutocompleteTextField
小部件,它允许用户在文本字段中输入内容,并根据输入内容显示建议列表。当用户选择一个建议时,会在控制台中打印所选的值。
关键点解释:
- optionsBuilder: 这个函数根据用户的输入生成建议列表。在这里,我们简单地过滤了包含用户输入字符串的建议。
- onSelected: 当用户选择一个建议时,这个函数会被调用。你可以在这里处理选择事件,比如更新状态或发送数据。
- fieldViewBuilder: 这个函数用于自定义文本字段的外观。在这里,我们简单地返回了一个标准的
TextField
。
这个示例应该能帮助你理解如何在 Flutter 应用中使用 cursor_autocomplete_options
插件来实现光标自动补全选项的功能。如果你有更多具体需求或遇到问题,请查阅该插件的官方文档或进一步提问。