我有一个 Flutter 应用程序和一个自定义输入字段。在其中,可以添加文本和标签。我有两个问题:
这是完整的代码:
import 'package:flutter/material.dart';
import 'package:tagfield_demo/tag_textfield.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<TagTextFieldState> _key = GlobalKey();
@override
Widget build(BuildContext context) {
Future.delayed(const Duration(milliseconds: 100)).then((val) {
setState(() {
print(val);
});
});
return TextFieldTapRegion(
child: Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: TagTextField(
key: _key,
initialText: "",
tagTextBuilder: (BuildContext context, String tag) {
switch (tag) {
case '[Amount]':
return 'Amount';
case '[Location]':
return 'Location';
}
return tag;
},
),
),
Row(
children: [
TextButton(
child: const Text("Add Location tag"),
onPressed: () {
if (_key.currentState != null) {
_key.currentState!.addTag('[Location]');
}
},
),
TextButton(
child: const Text("Add Amount tag"),
onPressed: () {
if (_key.currentState != null) {
_key.currentState!.addTag('[Amount]');
}
},
)
],
)
],
),
),
),
);
}
}
自定义标签输入字段:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
class TagTextField extends StatefulWidget {
final String initialText;
final String Function(BuildContext context, String tag)? tagTextBuilder;
final String? hint;
const TagTextField({
super.key,
this.initialText = '',
this.tagTextBuilder,
this.hint,
});
@override
State<TagTextField> createState() => TagTextFieldState();
}
class TagTextFieldState extends State<TagTextField> {
late String _text;
late List<Widget> _children;
late List<TextEditingController?> _textControllers;
late List<GlobalKey<_TagState>?> _tagKeys;
late List<FocusNode?> _focusNodes;
final FocusNode _mainFocusNode = FocusNode(); // Add this focus node
@override
void initState() {
super.initState();
_text = widget.initialText;
_children = [];
_textControllers = [];
_focusNodes = [];
_tagKeys = [];
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
_refreshChildren();
});
}
@override
void dispose() {
for (var node in _focusNodes) {
if (node != null) {
node.dispose();
}
}
for (var controller in _textControllers) {
if (controller != null) {
controller.dispose();
}
}
for (var tag in _tagKeys) {
if (tag != null && tag.currentState != null) {
tag.currentState!.dispose();
}
}
_mainFocusNode.dispose(); // Dispose the main focus node
super.dispose();
}
addTag(String tag) {
//Adds tag on cursor position
int idx = 0;
int position = 0;
for (var node in _focusNodes) {
if (node != null) {
var controller = _textControllers[idx];
if (node.hasFocus) {
var cursorPos = controller!.selection.base.offset;
position += cursorPos;
break;
} else {
position += controller!.text.length;
}
} else {
var tag = _tagKeys[idx];
if (tag != null && tag.currentState != null) {
position += tag.currentState!.getTag().length;
}
}
idx++;
}
//Add tag in text
_text = _text.replaceRange(position, position, tag);
for (var node in _focusNodes) {
if (node != null) {
node.unfocus();
}
}
setState(() {
_refreshChildren();
});
}
getText() {
int idx = 0;
_text = '';
for (var controller in _textControllers) {
if (controller != null) {
_text += controller.text;
} else {
var tag = _tagKeys[idx];
if (tag != null && tag.currentState != null) {
_text += tag.currentState!.getTag();
}
}
idx++;
}
return _text;
}
void _onFocusChange(FocusNode focusNode) {
if (focusNode.hasFocus) {
for (var node in _focusNodes) {
if (node != null && node != focusNode) {
node.unfocus();
}
}
}
}
bool _onKeyEvent(KeyEvent event, FocusNode focusNode) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.backspace) {
final idx = _focusNodes.indexOf(focusNode);
if (idx > 0 && _textControllers[idx]!.selection.base.offset == 0) {
final prevIdx = idx - 1;
if (_textControllers[prevIdx] == null && _tagKeys[prevIdx] != null) {
final tag = _tagKeys[prevIdx]!.currentState!.getTag();
_text = _text.replaceRange(
_tagKeys[prevIdx]!.currentState!.widget.startPos,
_tagKeys[prevIdx]!.currentState!.widget.startPos + tag.length,
'');
for (var node in _focusNodes) {
if (node != null) {
node.unfocus();
}
}
setState(() {
_refreshChildren();
});
}
}
return true;
}
return false;
}
_createTextField({String? text, bool showHint = false, bool isLast = false}) {
_textControllers.add(TextEditingController(text: text));
_textControllers.last!.addListener(() {
setState(() {
getText();
});
});
_focusNodes.add(FocusNode());
_focusNodes.last!.addListener(() {
_onFocusChange(_focusNodes.last!);
});
// Add key event listener
_focusNodes.last!.addListener(() {
if (_focusNodes.last!.hasFocus) {
HardwareKeyboard.instance.addHandler((event) {
return _onKeyEvent(event, _focusNodes.last!);
});
} else {
HardwareKeyboard.instance.removeHandler((event) {
return _onKeyEvent(event, _focusNodes.last!);
});
}
});
//Add empty elements, so indexes match
_tagKeys.add(null);
Widget textField = TextField(
focusNode: _focusNodes.last,
controller: _textControllers.last,
decoration: InputDecoration(
hintText: showHint ? widget.hint : null,
border: InputBorder.none,
),
);
if (showHint || isLast) {
textField = Expanded(
child: TextField(
focusNode: _focusNodes.last,
controller: _textControllers.last,
minLines: 1,
maxLines: null,
decoration: InputDecoration(
hintText: showHint ? widget.hint : null,
border: InputBorder.none,
),
),
);
} else {
textField = IntrinsicWidth(child: textField);
}
_children.add(
Baseline(
baseline: 20.0,
baselineType: TextBaseline.alphabetic,
child: textField,
),
);
}
_createTag(String tag, int startPos) {
if (_children.isEmpty) {
_createTextField();
}
String label = tag;
if (widget.tagTextBuilder != null) {
label = widget.tagTextBuilder!(context, tag);
}
//Add empty elements, so indexes match
_textControllers.add(null);
_focusNodes.add(null);
_tagKeys.add(GlobalKey<_TagState>());
_children.add(
Baseline(
baseline: 20.0,
baselineType: TextBaseline.alphabetic,
child: Tag(
key: _tagKeys.last,
label: label,
tag: tag,
startPos: startPos,
onDelete: () {
_text = _text.replaceRange(startPos, startPos + tag.length, '');
for (var node in _focusNodes) {
if (node != null) {
node.unfocus();
}
}
setState(() {
_refreshChildren();
});
},
),
),
);
}
_refreshChildren() {
_children.clear();
_textControllers.clear();
_focusNodes.clear();
_tagKeys.clear();
final regex = RegExp(r'(\[(.*?)\])');
final matches = regex.allMatches(_text);
var currentIndex = 0;
if (_text.isEmpty) {
//Only one starting text field
_createTextField(showHint: true);
return;
}
for (final match in matches) {
final beforeText = _text.substring(currentIndex, match.start);
if (beforeText.isNotEmpty) {
_createTextField(text: beforeText);
} else {
_createTextField();
}
final tag = match.group(1);
_createTag(tag!, match.start);
currentIndex = match.end;
}
final afterText = _text.substring(currentIndex);
if (afterText.isNotEmpty) {
_createTextField(text: afterText, isLast: true);
} else {
_createTextField(isLast: true);
}
}
@override
Widget build(BuildContext context) {
return KeyboardListener(
focusNode: _mainFocusNode, // Set the main focus node
child: Container(
decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)),
child: Wrap(
children: _children,
),
),
onKeyEvent: (event) {
_onKeyEvent(event, _mainFocusNode);
},
);
}
}
class Tag extends StatefulWidget {
final String tag;
final String label;
final int startPos;
final VoidCallback onDelete;
const Tag(
{super.key,
required this.tag,
required this.label,
required this.startPos,
required this.onDelete});
@override
State<Tag> createState() => _TagState();
}
class _TagState extends State<Tag> {
String getTag() {
return widget.tag;
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF2A2C42),
borderRadius: BorderRadius.circular(90),
),
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.label.toUpperCase(),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 5),
SizedBox(
width: 20,
height: 20,
child: FloatingActionButton(
onPressed: widget.onDelete,
backgroundColor: const Color(0xFF2A2C42),
mini: true,
child: const Icon(Icons.close),
),
)
],
),
);
}
}
您需要删除
if/else
中的这个_createTextField
语句:
if (showHint || isLast)
并且总是用
textField
包裹 IntrinsicWidth
。
Widget textField = IntrinsicWidth(
child: TextField(
focusNode: _focusNodes.last,
controller: _textControllers.last,
maxLines: null,
decoration: InputDecoration(
hintText: showHint ? widget.hint : null,
border: InputBorder.none,
//suffixIcon: Icon(Icons.pending),
),
),
);
这可能是因为
TextField
小部件总是会自然地扩展到最大可用宽度空间,因此从宽度更大的新行开始。
有边框的容器会缩小,可以通过添加参数来修复:
width: double.infinity
现在唯一的问题是您需要按文本字段的最左角才能向上移动光标/焦点。因此,为了解决这个问题,我添加了一个
GestureDetector
,以请求在点击容器内的任意位置时将焦点放在最后一个 focusNode
上。
return KeyboardListener(
focusNode: _mainFocusNode, // Set the main focus node
child: Container(
width: double.infinity,
decoration: BoxDecoration(border: Border.all(color: Colors.blueAccent)),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
setState(() {
FocusScope.of(context).requestFocus(_focusNodes.last);
});
},
child: Wrap(
children: _children,
),
),
),
onKeyEvent...
DartPad 上的演示:https://dartpad.dev/?id=05d8f3eed9e6f541514477e52a2e3ccd