Project Icon

extended_text_field

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

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

extended_text_field

pub package GitHub stars GitHub forks GitHub license GitHub issues flutter-candies

语言: 英文 | 简体中文

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

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,
        );

  @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();
  }

查看完整演示

项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号