搜尋套件、儲存庫與 APP 分類

extended_text_field 封面
套件詳情v17.0.0

extended_text_field

擴展的官方文字欄位,可快速建立包含內嵌圖片、@某人、自訂背景等特殊文字。它也支援建立自訂選擇工具列與處理常式。

AndroidiOSWebmacOSWindowsLinux
近 30 天下載
3.5萬
喜歡數
308
Pub 分數
150

安裝套件

flutter pub add extended_text_field

套件介紹

根據套件公開資料產生的選型評估,僅供參考。

ExtendedTextField 是一個強大的 Flutter 套件,透過支援內嵌圖片、提及(@someone)和自訂背景等豐富文字功能,增強了標準文字欄位。它支援建立自訂選擇工具列和觸控把手,提供對使用者互動更大的控制。專為尋求進階文字編輯功能的開發者設計,簡化了在多個平台建立互動式且視覺豐富的文字輸入的過程。

適用情境

  • 建立具有提及功能的聊天介面
  • 將圖像嵌入文字中
  • 建構豐富的筆記應用
  • 實現自定義文字選擇菜單
  • 開發社交媒體文字欄位
  • 新增動態文字格式

主要特色

  • 支援內嵌圖片
  • 處理 @提及 和 標籤
  • 可自訂的選擇工具列
  • 可擴充的文字跨度類型
  • 跨平台相容性
  • 可設定的觸控把手

適合對象

  • 開發富文本編輯器的開發者
  • 需要互動式文字輸入的應用程式
  • 聊天或訊息應用
  • 內容創作工具
  • 以UI為主的文字元件

使用建議

  • 需要理解 TextSpan 和 RichText
  • 可能增加應用複雜性
  • 需要仔細處理文字選擇邏輯
  • 與核心組件相比,文件較少
  • 大量跨度可能帶來的效能影響

原始 README

以下為關聯原始碼儲存庫的 README 原文快照,最新內容請造訪原始碼儲存庫。

extended_text_field

pub package GitHub stars GitHub forks GitHub license GitHub issues FlutterCandies QQ 群

Language: English | 中文简体

Extended official text field to build special text like inline image, @somebody, custom background etc quickly.It also support to build custom seleciton toolbar and handles.

Web demo for ExtendedTextField

ExtendedTextField is a third-party extension library for Flutter's official TextField component. The main extended features are as follows:

Feature ExtendedTextField TextField
Inline images and text mixture Supported, allows displaying inline images and mixed text Only supports displaying text, but have issues with text selection
Copying the actual value Supported, enables copying the actual value of the text Not supported
Quick construction of rich text Supported, enables quick construction of rich text based on text format Not supported

HarmonyOS is supported. Please use the latest version which contains ohos tag. You can check it in Versions tab.

dependencies:
  extended_text_field: 11.0.1-ohos

Please note that the translation provided above is based on the information you provided in the original text.

Limitation

  • Not support: it won't handle special text when TextDirection.rtl.

    Image position calculated by TextPainter is strange.

  • Not support:it won't handle special text when obscureText is true.

Special Text

Create Special Text

extended text helps to convert your text to special textSpan quickly.

for example, follwing code show how to create @xxxx special textSpan.

class AtText extends SpecialText {
  static const String flag = "@";
  final int start;

  /// whether show background for @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,

            ///caret can move into special text
            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

create your SpecialTextSpanBuilder

class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
  /// whether show background for @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 is end index of start flag, so text start index should be 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;
  }
}

Image

ImageSpan

show inline image by using 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));
  }
parameter description default
image The image to display(ImageProvider). -
imageWidth The width of image(not include margin) required
imageHeight The height of image(not include margin) required
margin The margin of image -
actualText Actual text, take care of it when enable selection,something likes "[love]" '\uFFFC'
start Start index of text,take care of it when enable selection. 0
Cache Image

if you want cache the network image, you can use ExtendedNetworkImageProvider and clear them with clearDiskCachedImages

import 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();
parameter description default
url The URL from which the image will be fetched. required
scale The scale to place in the [ImageInfo] object of the image. 1.0
headers The HTTP headers that will be used with [HttpClient.get] to fetch image from network. -
cache whether cache image to local false
retries the time to retry to request 3
timeLimit time limit to request image -
timeRetry the time duration to retry to request milliseconds: 100
cancelToken token to cancel network request CancellationToken()
/// Clear the disk cache directory then return if it succeed.
///  <param name="duration">timespan to compute whether file has expired or not</param>
Future<bool> clearDiskCachedImages({Duration duration}) async

TextSelectionControls

override [ExtendedTextField.extendedContextMenuBuilder] and [TextSelectionControls] to custom your toolbar widget or handle widget

const double _kHandleSize = 22.0;

/// Android Material styled text selection controls.
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:xxx@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: 'like',
        ),
      ],
      anchors: editableTextState.contextMenuAnchors,
    );
    // return AdaptiveTextSelectionToolbar.editableText(
    //   editableTextState: editableTextState,
    // );
  }

  /// Returns the size of the Material handle.
  @override
  Size getHandleSize(double textLineHeight) =>
      const Size(_kHandleSize, _kHandleSize);

  /// Builder for material-style text selection handles.
  @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] is a circle, with a rectangle in the top left quadrant of that
    // circle (an onion pointing to 10:30). We rotate [handle] to point
    // straight up or up-right depending on the handle type.
    switch (type) {
      case TextSelectionHandleType.left: // points up-right
        return Transform.rotate(
          angle: math.pi / 4.0,
          child: handle,
        );
      case TextSelectionHandleType.right: // points up-left
        return Transform.rotate(
          angle: -math.pi / 4.0,
          child: handle,
        );
      case TextSelectionHandleType.collapsed: // points up
        return handle;
    }
  }

  /// Gets anchor for material-style text selection handles.
  ///
  /// See [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 allows SelectAll when selection is not collapsed, unless
    // everything has already been selected.
    final TextEditingValue value = delegate.textEditingValue;
    return delegate.selectAllEnabled &&
        value.text.isNotEmpty &&
        !(value.selection.start == 0 &&
            value.selection.end == value.text.length);
  }
}

WidgetSpan

support to select and hitTest ExtendedWidgetSpan, you can create any widget in ExtendedTextField.

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

NoSystemKeyboard

support to prevent system keyboard show without any code intrusion for [ExtendedTextField] or [TextField].

TextInputBindingMixin

we prevent system keyboard show by stop Flutter Framework send TextInput.show message to Flutter Engine.

you can use [TextInputBinding] directly.

void main() {
  TextInputBinding();
  runApp(const MyApp());
}

or if you have other binding you can do as following.

 class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
 }

 void main() {
   YourBinding();
   runApp(const MyApp());
 }

or you need to override ignoreTextInputShow, you can do as following.

 class YourBinding extends TextInputBinding {
   @override
   // ignore: unnecessary_overrides
   bool ignoreTextInputShow() {
     // you can override it base on your case
     // if NoKeyboardFocusNode is not enough
     return super.ignoreTextInputShow();
   }
 }

 void main() {
   YourBinding();
   runApp(const MyApp());
 }
TextInputFocusNode

you should pass the [TextInputFocusNode] into [ExtendedTextField] or [TextField].

final TextInputFocusNode _focusNode = TextInputFocusNode();

  @override
  Widget build(BuildContext context) {
    return ExtendedTextField(
      // request keyboard if need
      focusNode: _focusNode..debugLabel = 'ExtendedTextField',
    );
  }

  @override
  Widget build(BuildContext context) {
    return TextField(
      // request keyboard if need
      focusNode: _focusNode..debugLabel = 'CustomTextField',
    );
  }

we prevent system keyboard show base on current focus is [TextInputFocusNode] and ignoreSystemKeyboardShow is true。

  final FocusNode? focus = FocusManager.instance.primaryFocus;
  if (focus != null &&
      focus is TextInputFocusNode &&
      focus.ignoreSystemKeyboardShow) {
    return true;
  }

### CustomKeyboard

show/hide your custom keyboard on [TextInputFocusNode] focus is changed.

if your custom keyboard can be close without unFocus, you need also handle 
show custom keyboard when [ExtendedTextField] or [TextField] `onTap`.

``` dart
  @override
  void initState() {
    super.initState();
    _focusNode.addListener(_handleFocusChanged);
  }

  void _onTextFiledTap() {
    if (_bottomSheetController == null) {
      _handleFocusChanged();
    }
  }

  void _handleFocusChanged() {
    if (_focusNode.hasFocus) {
      // just demo, you can define your custom keyboard as you want
      _bottomSheetController = showBottomSheet<void>(
          context: FocusManager.instance.primaryFocus!.context!,
          // set false, if don't want to drag to close custom keyboard
          enableDrag: true,
          builder: (BuildContext b) {
            // your custom keyboard
            return Container();
          });
      // maybe drag close
      _bottomSheetController?.closed.whenComplete(() {
        _bottomSheetController = null;
      });
    } else {
      _bottomSheetController?.close();
      _bottomSheetController = null;
    }
  }

  @override
  void dispose() {
    _focusNode.removeListener(_handleFocusChanged);
    super.dispose();
  }

see Full Demo

所屬分類

依賴

依賴清單 1 項

原始碼儲存庫

使用此套件的專案