我想知道是否有人知道如何在 TextFormField 内使超链接可点击。
该场景是一个笔记应用程序,其中每个笔记的文本始终位于 TextFormField 内。我想让用户可以单击他们自己编写的链接,而无需摆脱 TextFormField。
我尝试搜索各种包,但我发现的只是简单的文本小部件,而不是 TextFormField。
受到我的其他答案的启发, 我想出了一个可能对您有所帮助的解决方案。它对我来说效果很好。
extended_text_field
包。它允许您自定义文本在文本字段中的显示方式。用法如下:
步骤1
创建自定义
TextFormField
:
class ClickableLinkTextFormField extends StatefulWidget {
// ...
@override
_ClickableLinkTextFormFieldState createState() => _ClickableLinkTextFormFieldState();
}
class _ClickableLinkTextFormFieldState extends State<ClickableLinkTextFormField> {
late TextEditingController _controller;
@override
Widget build(BuildContext context) {
return FormField<String>(
builder: (FormFieldState<String> state) {
return ExtendedTextField(
controller: _controller,
specialTextSpanBuilder: MySpecialTextSpanBuilder(),
// ...
);
},
);
}
}
第2步:
class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
// regex from https://stackoverflow.com/a/3809435/9438149
final RegExp _urlRegExp = RegExp(
r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)',
caseSensitive: false,
);
@override
TextSpan build(String data, {TextStyle? textStyle, SpecialTextGestureTapCallback? onTap}) {
// This is where we detect and make links clickable
}
}
基本上,此类会遍历您的文本,查找 URL,并将它们包装在
GestureDetector
中,以便它们可以单击。点击链接后,它将在浏览器中打开。
第3步
要使用它,只需用以下内容替换常规 TextFormField 即可:
ClickableLinkTextFormField(
initialValue: 'Check out https://flutter.dev',
onChanged: (value) {},
// ...
)
正则表达式可能需要根据您正在处理的链接类型进行调整。
这是一个完整的可运行代码片段
import 'package:flutter/material.dart';
import 'package:extended_text_field/extended_text_field.dart';
import 'package:url_launcher/url_launcher.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
ClickableLinkTextFormField(
initialValue: 'This is a clickable link: https://flutter.dev\n',
onChanged: (value) {},
onSaved: (value) {
print('Saved: $value');
return null;
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
decoration: InputDecoration(
labelText: 'Note',
border: OutlineInputBorder(),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
},
child: Text('Submit'),
),
],
),
),
),
);
}
}
class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
final RegExp _urlRegExp = RegExp(
// https://stackoverflow.com/a/3809435/9438149
r'[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)',
caseSensitive: false,
);
@override
TextSpan build(String data,
{TextStyle? textStyle, SpecialTextGestureTapCallback? onTap}) {
final List<InlineSpan> spans = [];
final List<String> lines = data.split('\n');
for (int i = 0; i < lines.length; i++) {
if (i > 0) {
spans.add(TextSpan(text: '\n'));
}
final List<String> words = lines[i].split(' ');
for (int j = 0; j < words.length; j++) {
final word = words[j];
if (_urlRegExp.hasMatch(word)) {
spans.add(
WidgetSpan(
// Wrap the URL in a GestureDetector to make it clickable
child: GestureDetector(
onTap: () async {
final Uri uri = Uri.parse(word);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
}
},
child: Text(
word,
style: TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
),
),
),
);
} else {
// If the word is not a URL, just add it as a normal TextSpan
spans.add(TextSpan(text: word, style: textStyle));
}
// Add a space after each word, except the last one
if (j < words.length - 1) {
spans.add(TextSpan(text: ' ', style: textStyle));
}
}
}
// Return the TextSpan with all the spans
return TextSpan(children: spans, style: textStyle);
}
@override
SpecialText? createSpecialText(String flag,
{TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
required int index}) {
return null;
}
}
class ClickableLinkTextFormField extends StatefulWidget {
final String initialValue;
final ValueChanged<String>? onChanged;
final void Function(String?)? onSaved;
final String? Function(String?)? validator;
final InputDecoration? decoration;
const ClickableLinkTextFormField({
Key? key,
required this.initialValue,
this.onChanged,
this.onSaved, // No longer required
this.validator,
this.decoration,
}) : super(key: key);
@override
_ClickableLinkTextFormFieldState createState() =>
_ClickableLinkTextFormFieldState();
}
class _ClickableLinkTextFormFieldState
extends State<ClickableLinkTextFormField> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialValue);
}
@override
Widget build(BuildContext context) {
return FormField<String>(
initialValue: widget.initialValue,
validator: widget.validator,
onSaved: widget.onSaved,
builder: (FormFieldState<String> state) {
return ExtendedTextField(
controller: _controller,
onChanged: (value) {
state.didChange(value);
widget.onChanged?.call(value);
},
specialTextSpanBuilder: MySpecialTextSpanBuilder(),
maxLines: null,
decoration: widget.decoration?.copyWith(
errorText: state.hasError ? state.errorText : null,
) ??
InputDecoration(
errorText: state.hasError ? state.errorText : null,
),
);
},
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
extended_text_field
和 url_launcher 添加到您的 pubspec.yaml
。