extended_text_field
语言: 英文 | 简体中文
扩展官方文本字段以快速构建特殊文本,如内联图像、@某人、自定义背景等。它还支持构建自定义选择工具栏和句柄。
ExtendedTextField是Flutter官方TextField组件的第三方扩展库。主要扩展功能如下:
功能 | ExtendedTextField | TextField |
---|---|---|
内联图像和文本混合 | 支持,允许显示内联图像和混合文本 | 仅支持显示文本,但文本选择存在问题 |
复制实际值 | 支持,可以复制文本的实际值 | 不支持 |
快速构建富文本 | 支持,可以基于文本格式快速构建富文本 | 不支持 |
支持
HarmonyOS
。请使用包含ohos
标签的最新版本。您可以在Versions
标签页中查看。
dependencies:
extended_text_field: 11.0.1-ohos
请注意,以上翻译是基于您提供的原文信息。
限制
-
不支持:当TextDirection.rtl时,不会处理特殊文本。
TextPainter计算的图像位置有问题。
-
不支持:当obscureText为true时,不会处理特殊文本。
特殊文本
创建特殊文本
extended text帮助快速将文本转换为特殊textSpan。
例如,以下代码展示了如何创建@xxxx特殊textSpan。
class AtText extends SpecialText {
static const String flag = "@";
final int start;
/// 是否为@somebody显示背景
final bool showAtBackground;
AtText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
{this.showAtBackground: false, this.start})
: super(
flag,
" ",
textStyle,
);
@override
InlineSpan finishText() {
TextStyle textStyle =
this.textStyle?.copyWith(color: Colors.blue, fontSize: 16.0);
final String atText = toString();
return showAtBackground
? BackgroundTextSpan(
background: Paint()..color = Colors.blue.withOpacity(0.15),
text: atText,
actualText: atText,
start: start,
///光标可以移动到特殊文本中
deleteAll: true,
style: textStyle,
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) onTap(atText);
}))
: SpecialTextSpan(
text: atText,
actualText: atText,
start: start,
style: textStyle,
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) onTap(atText);
}));
}
}
SpecialTextSpanBuilder
创建您的SpecialTextSpanBuilder
class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
/// 是否为@somebody显示背景
final bool showAtBackground;
final BuilderType type;
MySpecialTextSpanBuilder(
{this.showAtBackground: false, this.type: BuilderType.extendedText});
@override
TextSpan build(String data, {TextStyle textStyle, onTap}) {
var textSpan = super.build(data, textStyle: textStyle, onTap: onTap);
return textSpan;
}
@override
SpecialText createSpecialText(String flag,
{TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {
if (flag == null || flag == "") return null;
///index是起始标志的结束索引,所以文本起始索引应为index-(flag.length-1)
if (isStart(flag, AtText.flag)) {
return AtText(textStyle, onTap,
start: index - (AtText.flag.length - 1),
showAtBackground: showAtBackground,
type: type);
} else if (isStart(flag, EmojiText.flag)) {
return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));
} else if (isStart(flag, DollarText.flag)) {
return DollarText(textStyle, onTap,
start: index - (DollarText.flag.length - 1), type: type);
}
return null;
}
}
图像
ImageSpan
通过使用ImageSpan显示内联图像。
ImageSpan(
ImageProvider image, {
Key key,
@required double imageWidth,
@required double imageHeight,
EdgeInsets margin,
int start: 0,
ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,
String actualText,
TextBaseline baseline,
TextStyle style,
BoxFit fit: BoxFit.scaleDown,
ImageLoadingBuilder loadingBuilder,
ImageFrameBuilder frameBuilder,
String semanticLabel,
bool excludeFromSemantics = false,
Color color,
BlendMode colorBlendMode,
AlignmentGeometry imageAlignment = Alignment.center,
ImageRepeat repeat = ImageRepeat.noRepeat,
Rect centerSlice,
bool matchTextDirection = false,
bool gaplessPlayback = false,
FilterQuality filterQuality = FilterQuality.low,
})
ImageSpan(AssetImage("xxx.jpg"),
imageWidth: size,
imageHeight: size,
margin: EdgeInsets.only(left: 2.0, bottom: 0.0, right: 2.0));
}
参数 | 描述 | 默认值 |
---|---|---|
image | 要显示的图像(ImageProvider)。 | - |
imageWidth | 图像宽度(不包括边距) | 必需 |
imageHeight | 图像高度(不包括边距) | 必需 |
margin | 图像的边距 | - |
actualText | 实际文本,启用选择时需要注意,类似于"[love]" | '\uFFFC' |
start | 文本的起始索引,启用选择时需要注意。 | 0 |
缓存图像
如果你想缓存网络图像,可以使用ExtendedNetworkImageProvider,并通过clearDiskCachedImages清除它们
导入extended_image_library
dependencies:
extended_image_library: ^0.1.4
ExtendedNetworkImageProvider(
this.url, {
this.scale = 1.0,
this.headers,
this.cache: false,
this.retries = 3,
this.timeLimit,
this.timeRetry = const Duration(milliseconds: 100),
CancellationToken cancelToken,
}) : assert(url != null),
assert(scale != null),
cancelToken = cancelToken ?? CancellationToken();
参数 | 描述 | 默认值 |
---|---|---|
url | 从中获取图像的URL。 | 必需 |
scale | 放置在图像[ImageInfo]对象中的缩放比例。 | 1.0 |
headers | 用于[HttpClient.get]从网络获取图像的HTTP头。 | - |
cache | 是否将图像缓存到本地 | false |
retries | 重试请求的次数 | 3 |
timeLimit | 请求图像的时间限制 | - |
timeRetry | 重试请求的时间间隔 | 毫秒:100 |
cancelToken | 用于取消网络请求的令牌 | CancellationToken() |
/// 清除磁盘缓存目录,然后返回是否成功。
/// <param name="duration">用于计算文件是否过期的时间跨度</param>
Future<bool> clearDiskCachedImages({Duration duration}) async
TextSelectionControls
重写[ExtendedTextField.extendedContextMenuBuilder]和[TextSelectionControls]以自定义你的工具栏小部件或处理小部件
const double _kHandleSize = 22.0;
/// Android Material风格的文本选择控件。
class MyTextSelectionControls extends TextSelectionControls
with TextSelectionHandleControls {
static Widget defaultContextMenuBuilder(
BuildContext context, ExtendedEditableTextState editableTextState) {
return AdaptiveTextSelectionToolbar.buttonItems(
buttonItems: <ContextMenuButtonItem>[
...editableTextState.contextMenuButtonItems,
ContextMenuButtonItem(
onPressed: () {
launchUrl(
Uri.parse(
'mailto:zmtzawqlp@live.com?subject=extended_text_share&body=${editableTextState.textEditingValue.text}',
),
);
editableTextState.hideToolbar(true);
editableTextState.textEditingValue
.copyWith(selection: const TextSelection.collapsed(offset: 0));
},
type: ContextMenuButtonType.custom,
label: '喜欢',
),
],
anchors: editableTextState.contextMenuAnchors,
);
// return AdaptiveTextSelectionToolbar.editableText(
// editableTextState: editableTextState,
// );
}
/// 返回Material句柄的大小。
@override
Size getHandleSize(double textLineHeight) =>
const Size(_kHandleSize, _kHandleSize);
/// Material风格文本选择句柄的构建器。
@override
Widget buildHandle(
BuildContext context, TextSelectionHandleType type, double textLineHeight,
[VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
final Widget handle = SizedBox(
width: _kHandleSize,
height: _kHandleSize,
child: Image.asset(
'assets/40.png',
),
);
// [handle]是一个圆圈,在该圆圈的左上象限有一个矩形(指向10:30的洋葱)。
// 我们旋转[handle]以根据句柄类型指向正上方或右上方。
switch (type) {
case TextSelectionHandleType.left: // 指向右上方
return Transform.rotate(
angle: math.pi / 4.0,
child: handle,
);
case TextSelectionHandleType.right: // 指向左上方
return Transform.rotate(
angle: -math.pi / 4.0,
child: handle,
);
case TextSelectionHandleType.collapsed: // 指向上方
return handle;
}
}
/// 获取Material风格文本选择句柄的锚点。
///
/// 参见 [TextSelectionControls.getHandleAnchor]。
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,
[double? startGlyphHeight, double? endGlyphHeight]) {
switch (type) {
case TextSelectionHandleType.left:
return const Offset(_kHandleSize, 0);
case TextSelectionHandleType.right:
return Offset.zero;
default:
return const Offset(_kHandleSize / 2, -4);
}
}
@override
bool canSelectAll(TextSelectionDelegate delegate) {
// Android允许在选择未折叠时全选,除非已经选择了所有内容。
final TextEditingValue value = delegate.textEditingValue;
return delegate.selectAllEnabled &&
value.text.isNotEmpty &&
!(value.selection.start == 0 &&
value.selection.end == value.text.length);
}
}
## WidgetSpan
![](https://yellow-cdn.veclightyear.com/835a84d5/f61b139a-efda-433a-b260-7f6da3b4b305.gif)
支持选择和点击测试 ExtendedWidgetSpan,你可以在 ExtendedTextField 中创建任何小部件。
```dart
class EmailText extends SpecialText {
final TextEditingController controller;
final int start;
final BuildContext context;
EmailText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
{this.start, this.controller, this.context, String startFlag})
: super(startFlag, " ", textStyle, onTap: onTap);
@override
bool isEnd(String value) {
var index = value.indexOf("@");
var index1 = value.indexOf(".");
return index >= 0 &&
index1 >= 0 &&
index1 > index + 1 &&
super.isEnd(value);
}
@override
InlineSpan finishText() {
final String text = toString();
return ExtendedWidgetSpan(
actualText: text,
start: start,
alignment: ui.PlaceholderAlignment.middle,
child: GestureDetector(
child: Padding(
padding: EdgeInsets.only(right: 5.0, top: 2.0, bottom: 2.0),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(5.0)),
child: Container(
padding: EdgeInsets.all(5.0),
color: Colors.orange,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
text.trim(),
//style: textStyle?.copyWith(color: Colors.orange),
),
SizedBox(
width: 5.0,
),
InkWell(
child: Icon(
Icons.close,
size: 15.0,
),
onTap: () {
controller.value = controller.value.copyWith(
text: controller.text
.replaceRange(start, start + text.length, ""),
selection: TextSelection.fromPosition(
TextPosition(offset: start)));
},
)
],
),
)),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: true,
builder: (c) {
TextEditingController textEditingController =
TextEditingController()..text = text.trim();
return Column(
children: <Widget>[
Expanded(
child: Container(),
),
Material(
child: Padding(
padding: EdgeInsets.all(10.0),
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
suffixIcon: FlatButton(
child: Text("OK"),
onPressed: () {
controller.value = controller.value.copyWith(
text: controller.text.replaceRange(
start,
start + text.length,
textEditingController.text + " "),
selection: TextSelection.fromPosition(
TextPosition(
offset: start +
(textEditingController.text + " ")
.length)));
Navigator.pop(context);
},
)),
),
)),
Expanded(
child: Container(),
)
],
);
});
},
),
deleteAll: true,
);
}
}
无系统键盘
支持在不侵入任何代码的情况下阻止系统键盘显示,适用于 [ExtendedTextField] 或 [TextField]。
TextInputBindingMixin
我们通过阻止 Flutter Framework 向 Flutter Engine 发送 TextInput.show
消息来防止系统键盘显示。
你可以直接使用 [TextInputBinding]。
void main() {
TextInputBinding();
runApp(const MyApp());
}
或者如果你有其他 binding
,你可以按以下方式操作。
class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
}
void main() {
YourBinding();
runApp(const MyApp());
}
或者如果你需要覆盖 ignoreTextInputShow
,你可以按以下方式操作。
class YourBinding extends TextInputBinding {
@override
// ignore: unnecessary_overrides
bool ignoreTextInputShow() {
// 你可以根据你的情况覆盖它
// 如果 NoKeyboardFocusNode 不够用的话
return super.ignoreTextInputShow();
}
}
void main() {
YourBinding();
runApp(const MyApp());
}
TextInputFocusNode
你应该将 [TextInputFocusNode] 传递给 [ExtendedTextField] 或 [TextField]。
final TextInputFocusNode _focusNode = TextInputFocusNode();
@override
Widget build(BuildContext context) {
return ExtendedTextField(
// 需要时请求键盘
focusNode: _focusNode..debugLabel = 'ExtendedTextField',
);
}
@override
Widget build(BuildContext context) {
return TextField(
// 需要时请求键盘
focusNode: _focusNode..debugLabel = 'CustomTextField',
);
}
我们根据当前焦点是 [TextInputFocusNode] 且 ignoreSystemKeyboardShow
为 true 来防止系统键盘显示。
final FocusNode? focus = FocusManager.instance.primaryFocus;
if (focus != null &&
focus is TextInputFocusNode &&
focus.ignoreSystemKeyboardShow) {
return true;
}
自定义键盘
当 [TextInputFocusNode] 焦点改变时显示/隐藏你的自定义键盘。
如果你的自定义键盘可以在不失去焦点的情况下关闭,你还需要处理在 [ExtendedTextField] 或 [TextField] 的 onTap
事件中显示自定义键盘。
@override
void initState() {
super.initState();
_focusNode.addListener(_handleFocusChanged);
}
void _onTextFiledTap() {
if (_bottomSheetController == null) {
_handleFocusChanged();
}
}
void _handleFocusChanged() {
if (_focusNode.hasFocus) {
// 只是演示,你可以根据需要定义你的自定义键盘
_bottomSheetController = showBottomSheet<void>(
context: FocusManager.instance.primaryFocus!.context!,
// 如果不想通过拖动来关闭自定义键盘,设置为 false
enableDrag: true,
builder: (BuildContext b) {
// 你的自定义键盘
return Container();
});
// 可能通过拖动关闭
_bottomSheetController?.closed.whenComplete(() {
_bottomSheetController = null;
});
} else {
_bottomSheetController?.close();
_bottomSheetController = null;
}
}
@override
void dispose() {
_focusNode.removeListener(_handleFocusChanged);
super.dispose();
}
查看完整演示