extended_text_field

extended_text_field

Flutter扩展文本输入组件 支持内联图片和富文本

extended_text_field是Flutter官方TextField的扩展插件。它支持内联图片与文本混排、复制真实文本值、快速构建富文本等功能。此外还提供自定义选择工具栏和句柄、WidgetSpan支持、阻止系统键盘弹出等特性。该插件为开发者提供了更灵活强大的文本输入方案。

Flutter文本编辑特殊文本自定义选择工具栏内联图像Github开源项目

extended_text_field

pub package GitHub stars GitHub forks GitHub license GitHub issues <a target="_blank" href="https://jq.qq.com/?_wv=1027&k=5bcc0gy"><img border="0" src="https://yellow-cdn.veclightyear.com/835a84d5/b63ff1fe-18b4-44c0-bd6b-f03ac7b2b0ce.png" alt="flutter-candies" title="flutter-candies"></a>

语言: 英文 | 简体中文

扩展官方文本字段以快速构建特殊文本,如内联图像、@某人、自定义背景等。它还支持构建自定义选择工具栏和句柄。

ExtendedTextField的Web演示

ExtendedTextField是Flutter官方TextField组件的第三方扩展库。主要扩展功能如下:

功能ExtendedTextFieldTextField
内联图像和文本混合支持,允许显示内联图像和混合文本仅支持显示文本,但文本选择存在问题
复制实际值支持,可以复制文本的实际值不支持
快速构建富文本支持,可以基于文本格式快速构建富文本不支持

支持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, ); 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}); TextSpan build(String data, {TextStyle textStyle, onTap}) { var textSpan = super.build(data, textStyle: textStyle, onTap: onTap); return textSpan; } 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, double imageWidth, 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句柄的大小。 Size getHandleSize(double textLineHeight) => const Size(_kHandleSize, _kHandleSize); /// Material风格文本选择句柄的构建器。 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]。 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); } } 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); bool isEnd(String value) { var index = value.indexOf("@"); var index1 = value.indexOf("."); return index >= 0 && index1 >= 0 && index1 > index + 1 && super.isEnd(value); } 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 { // ignore: unnecessary_overrides bool ignoreTextInputShow() { // 你可以根据你的情况覆盖它 // 如果 NoKeyboardFocusNode 不够用的话 return super.ignoreTextInputShow(); } } void main() { YourBinding(); runApp(const MyApp()); }

TextInputFocusNode

你应该将 [TextInputFocusNode] 传递给 [ExtendedTextField] 或 [TextField]。

final TextInputFocusNode _focusNode = TextInputFocusNode(); Widget build(BuildContext context) { return ExtendedTextField( // 需要时请求键盘 focusNode: _focusNode..debugLabel = 'ExtendedTextField', ); } 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 事件中显示自定义键盘。

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; } } void dispose() { _focusNode.removeListener(_handleFocusChanged); super.dispose(); }

查看完整演示

编辑推荐精选

TRAE编程

TRAE编程

AI辅助编程,代码自动修复

Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。

AI工具TraeAI IDE协作生产力转型热门
蛙蛙写作

蛙蛙写作

AI小说写作助手,一站式润色、改写、扩写

蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。

AI辅助写作AI工具蛙蛙写作AI写作工具学术助手办公助手营销助手AI助手
问小白

问小白

全能AI智能助手,随时解答生活与工作的多样问题

问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。

热门AI助手AI对话AI工具聊天机器人
Transly

Transly

实时语音翻译/同声传译工具

Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。

讯飞智文

讯飞智文

一键生成PPT和Word,让学习生活更轻松

讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。

AI办公办公工具AI工具讯飞智文AI在线生成PPTAI撰写助手多语种文档生成AI自动配图热门
讯飞星火

讯飞星火

深度推理能力全新升级,全面对标OpenAI o1

科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。

热门AI开发模型训练AI工具讯飞星火大模型智能问答内容创作多语种支持智慧生活
Spark-TTS

Spark-TTS

一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型

Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。

咔片PPT

咔片PPT

AI助力,做PPT更简单!

咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。

讯飞绘文

讯飞绘文

选题、配图、成文,一站式创作,让内容运营更高效

讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。

热门AI辅助写作AI工具讯飞绘文内容运营AI创作个性化文章多平台分发AI助手
材料星

材料星

专业的AI公文写作平台,公文写作神器

AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。

下拉加载更多