{"id":13491325,"url":"https://github.com/fluttercandies/extended_text_field","last_synced_at":"2025-05-14T23:02:25.688Z","repository":{"id":44437226,"uuid":"183596732","full_name":"fluttercandies/extended_text_field","owner":"fluttercandies","description":"extended official text field to quickly build special text like inline image, @somebody, custom background etc.","archived":false,"fork":false,"pushed_at":"2024-12-19T05:30:09.000Z","size":7731,"stargazers_count":586,"open_issues_count":26,"forks_count":149,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-13T21:33:55.878Z","etag":null,"topics":["flutter","inline-image","text-field","toolbar","widget-span"],"latest_commit_sha":null,"homepage":"","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fluttercandies.png","metadata":{"files":{"readme":"README-ZH.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":"CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"custom":"http://zmtzawqlp.gitee.io/my_images/images/qrcode.png"}},"created_at":"2019-04-26T09:10:28.000Z","updated_at":"2025-04-04T04:07:22.000Z","dependencies_parsed_at":"2023-02-16T14:45:40.325Z","dependency_job_id":"86eca7fe-0522-4f79-afec-5be4d772871b","html_url":"https://github.com/fluttercandies/extended_text_field","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercandies%2Fextended_text_field","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercandies%2Fextended_text_field/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercandies%2Fextended_text_field/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fluttercandies%2Fextended_text_field/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fluttercandies","download_url":"https://codeload.github.com/fluttercandies/extended_text_field/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254243353,"owners_count":22038044,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flutter","inline-image","text-field","toolbar","widget-span"],"created_at":"2024-07-31T19:00:55.654Z","updated_at":"2025-05-14T23:02:25.671Z","avatar_url":"https://github.com/fluttercandies.png","language":"Dart","funding_links":["http://zmtzawqlp.gitee.io/my_images/images/qrcode.png"],"categories":["Dart"],"sub_categories":[],"readme":"# extended_text_field\n\n[![pub package](https://img.shields.io/pub/v/extended_text_field.svg)](https://pub.dartlang.org/packages/extended_text_field) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/network)  [![GitHub license](https://img.shields.io/github/license/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE)  [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/issues) \u003ca href=\"https://qm.qq.com/q/ZyJbSVjfSU\"\u003e![FlutterCandies QQ 群](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffluttercandies%2F.github%2Frefs%2Fheads%2Fmain%2Fdata.yml\u0026query=%24.qq_group_number\u0026label=QQ%E7%BE%A4\u0026logo=qq\u0026color=1DACE8)\n\n文档语言: [English](README.md) | 中文简体\n\n官方输入框的扩展组件，支持图片，@某人，自定义文字背景。也支持自定义菜单和选择器。\n\n[ExtendedTextField 在线 Demo](https://fluttercandies.github.io/extended_text_field/)\n\nExtendedTextField  是 Flutter 官方 TextField  的三方扩展库，主要扩展功能如下:\n\n| 功能                       | ExtendedTextField                    | TextField                                |\n| -------------------------- | ------------------------------------ | ---------------------------------------- |\n| 图文混合                   | 支持，可以实现图文混合显示           | 仅支持显示文本，但在选择文本时会出现问题 |\n| 支持复制真实值             | 支持，可以复制出文本的真实值         | 不支持                                   |\n| 根据文本格式快速构建富文本 | 支持，可以根据文本格式快速构建富文本 | 不支持                                   |\n\n\u003e 已支持 `HarmonyOS`. 请使用最新的带有 `ohos` 标志的版本. 你可以在 `Versions` 签查找.\n\n```yaml\ndependencies:\n  extended_text_field: 11.0.1-ohos\n```\n\n\n- [extended\\_text\\_field](#extended_text_field)\n  - [限制](#限制)\n  - [特殊文本](#特殊文本)\n    - [创建特殊文本](#创建特殊文本)\n    - [特殊文本Builder](#特殊文本builder)\n  - [图片](#图片)\n    - [ImageSpan](#imagespan)\n    - [缓存图片](#缓存图片)\n  - [文本选择控制器](#文本选择控制器)\n  - [WidgetSpan](#widgetspan)\n  - [阻止系统键盘](#阻止系统键盘)\n    - [TextInputBindingMixin](#textinputbindingmixin)\n    - [TextInputFocusNode](#textinputfocusnode)\n    - [CustomKeyboard](#customkeyboard)\n  - [☕️Buy me a coffee](#️buy-me-a-coffee)\n\n## 限制\n\n- 不支持TextDirection.rtl，从右向左.\n\n- 不支持obscureText为true.\n\n## 特殊文本\n\n![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field.gif)\n\n### 创建特殊文本\n\nextended_text 帮助将字符串文本快速转换为特殊的TextSpan\n\n下面的例子告诉你怎么创建一个@xxx\n\n具体思路是对字符串进行进栈遍历，通过判断flag来判定是否是一个特殊字符。\n例子：@zmtzawqlp ，以@开头并且以空格结束，我们就认为它是一个@的特殊文本\n\n```dart\nclass AtText extends SpecialText {\n  static const String flag = \"@\";\n  final int start;\n\n  /// whether show background for @somebody\n  final bool showAtBackground;\n\n  AtText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,\n      {this.showAtBackground: false, this.start})\n      : super(\n          flag,\n          \" \",\n          textStyle,\n        );\n\n  @override\n  InlineSpan finishText() {\n    TextStyle textStyle =\n        this.textStyle?.copyWith(color: Colors.blue, fontSize: 16.0);\n\n    final String atText = toString();\n\n    return showAtBackground\n        ? BackgroundTextSpan(\n            background: Paint()..color = Colors.blue.withOpacity(0.15),\n            text: atText,\n            actualText: atText,\n            start: start,\n\n            ///caret can move into special text\n            deleteAll: true,\n            style: textStyle,\n            recognizer: (TapGestureRecognizer()\n              ..onTap = () {\n                if (onTap != null) onTap(atText);\n              }))\n        : SpecialTextSpan(\n            text: atText,\n            actualText: atText,\n            start: start,\n            style: textStyle,\n            recognizer: (TapGestureRecognizer()\n              ..onTap = () {\n                if (onTap != null) onTap(atText);\n              }));\n  }\n}\n\n```\n\n### 特殊文本Builder\n\n创建属于你自己规则的Builder，上面说了你可以继承SpecialText来定义各种各样的特殊文本。\n- build 方法中，是通过具体思路是对字符串进行进栈遍历，通过判断flag来判定是否是一个特殊文本。\n  感兴趣的，可以看一下SpecialTextSpanBuilder里面build方法的实现，当然你也可以写出属于自己的build逻辑\n- createSpecialText 通过判断flag来判定是否是一个特殊文本\n\n```dart\nclass MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {\n  /// whether show background for @somebody\n  final bool showAtBackground;\n  final BuilderType type;\n  MySpecialTextSpanBuilder(\n      {this.showAtBackground: false, this.type: BuilderType.extendedText});\n\n  @override\n  TextSpan build(String data, {TextStyle textStyle, onTap}) {\n    var textSpan = super.build(data, textStyle: textStyle, onTap: onTap);\n    return textSpan;\n  }\n\n  @override\n  SpecialText createSpecialText(String flag,\n      {TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {\n    if (flag == null || flag == \"\") return null;\n\n    ///index is end index of start flag, so text start index should be index-(flag.length-1)\n    if (isStart(flag, AtText.flag)) {\n      return AtText(textStyle, onTap,\n          start: index - (AtText.flag.length - 1),\n          showAtBackground: showAtBackground,\n          type: type);\n    } else if (isStart(flag, EmojiText.flag)) {\n      return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));\n    } else if (isStart(flag, DollarText.flag)) {\n      return DollarText(textStyle, onTap,\n          start: index - (DollarText.flag.length - 1), type: type);\n    }\n    return null;\n  }\n}\n```\n其实你也不是一定要用这套代码将字符串转换为TextSpan，你可以有自己的方法，给最后的TextSpan就可以了。\n\n## 图片\n\n![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/extended_text_field_image.gif)\n\n### ImageSpan\n\n使用ImageSpan 展示图片\n\n```dart\nImageSpan(\n    ImageProvider image, {\n    Key key,\n    @required double imageWidth,\n    @required double imageHeight,\n    EdgeInsets margin,\n    int start: 0,\n    ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,\n    String actualText,\n    TextBaseline baseline,\n    TextStyle style,\n    BoxFit fit: BoxFit.scaleDown,\n    ImageLoadingBuilder loadingBuilder,\n    ImageFrameBuilder frameBuilder,\n    String semanticLabel,\n    bool excludeFromSemantics = false,\n    Color color,\n    BlendMode colorBlendMode,\n    AlignmentGeometry imageAlignment = Alignment.center,\n    ImageRepeat repeat = ImageRepeat.noRepeat,\n    Rect centerSlice,\n    bool matchTextDirection = false,\n    bool gaplessPlayback = false,\n    FilterQuality filterQuality = FilterQuality.low,\n  })\n\nImageSpan(AssetImage(\"xxx.jpg\"),\n        imageWidth: size,\n        imageHeight: size,\n        margin: EdgeInsets.only(left: 2.0, bottom: 0.0, right: 2.0));\n  }\n```\n\n| 参数        | 描述                                                              | 默认             |\n| ----------- | ----------------------------------------------------------------- | ---------------- |\n| image       | 图片展示的Provider(ImageProvider)                                 | -                |\n| imageWidth  | 宽度，不包括 margin                                               | 必填             |\n| imageHeight | 高度，不包括 margin                                               | 必填             |\n| margin      | 图片的margin                                                      | -                |\n| actualText  | 真实的文本,当你开启文本选择功能的时候，必须设置,比如图片\"\\[love\\] | 空占位符'\\uFFFC' |\n| start       | 在文本字符串中的开始位置,当你开启文本选择功能的时候，必须设置     | 0                |\n\n### 缓存图片\n\n你可以用ExtendedNetworkImageProvider来缓存文本中的图片，使用clearDiskCachedImages方法来清掉本地缓存\n\n引入 extended_image_library\n\n```dart\ndependencies:\n  extended_image_library: ^0.1.4\n```\n\n```dart\nExtendedNetworkImageProvider(\n  this.url, {\n  this.scale = 1.0,\n  this.headers,\n  this.cache: false,\n  this.retries = 3,\n  this.timeLimit,\n  this.timeRetry = const Duration(milliseconds: 100),\n  CancellationToken cancelToken,\n})  : assert(url != null),\n      assert(scale != null),\n      cancelToken = cancelToken ?? CancellationToken();\n```\n\n| 参数        | 描述                | 默认                |\n| ----------- | ------------------- | ------------------- |\n| url         | 网络请求地址        | required            |\n| scale       | ImageInfo中的scale  | 1.0                 |\n| headers     | HttpClient的headers | -                   |\n| cache       | 是否缓存到本地      | false               |\n| retries     | 请求尝试次数        | 3                   |\n| timeLimit   | 请求超时            | -                   |\n| timeRetry   | 请求重试间隔        | milliseconds: 100   |\n| cancelToken | 用于取消请求的Token | CancellationToken() |\n\n```dart\n/// Clear the disk cache directory then return if it succeed.\n///  \u003cparam name=\"duration\"\u003etimespan to compute whether file has expired or not\u003c/param\u003e\nFuture\u003cbool\u003e clearDiskCachedImages({Duration duration}) async\n```\n\n## 文本选择控制器\n\n![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/custom_toolbar.gif)\n\n提供了默认的控制器MaterialExtendedTextSelectionControls/CupertinoExtendedTextSelectionControls\n\n通过重写 [ExtendedTextField.extendedContextMenuBuilder] 和 [TextSelectionControls] 来自定义菜单和选择器。\n\n```dart\nconst double _kHandleSize = 22.0;\n\n/// Android Material styled text selection controls.\nclass MyTextSelectionControls extends TextSelectionControls\n    with TextSelectionHandleControls {\n  static Widget defaultContextMenuBuilder(\n      BuildContext context, ExtendedEditableTextState editableTextState) {\n    return AdaptiveTextSelectionToolbar.buttonItems(\n      buttonItems: \u003cContextMenuButtonItem\u003e[\n        ...editableTextState.contextMenuButtonItems,\n        ContextMenuButtonItem(\n          onPressed: () {\n            launchUrl(\n              Uri.parse(\n                'mailto:zmtzawqlp@live.com?subject=extended_text_share\u0026body=${editableTextState.textEditingValue.text}',\n              ),\n            );\n            editableTextState.hideToolbar(true);\n            editableTextState.textEditingValue\n                .copyWith(selection: const TextSelection.collapsed(offset: 0));\n          },\n          type: ContextMenuButtonType.custom,\n          label: 'like',\n        ),\n      ],\n      anchors: editableTextState.contextMenuAnchors,\n    );\n    // return AdaptiveTextSelectionToolbar.editableText(\n    //   editableTextState: editableTextState,\n    // );\n  }\n\n  /// Returns the size of the Material handle.\n  @override\n  Size getHandleSize(double textLineHeight) =\u003e\n      const Size(_kHandleSize, _kHandleSize);\n\n  /// Builder for material-style text selection handles.\n  @override\n  Widget buildHandle(\n      BuildContext context, TextSelectionHandleType type, double textLineHeight,\n      [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {\n    final Widget handle = SizedBox(\n      width: _kHandleSize,\n      height: _kHandleSize,\n      child: Image.asset(\n        'assets/40.png',\n      ),\n    );\n\n    // [handle] is a circle, with a rectangle in the top left quadrant of that\n    // circle (an onion pointing to 10:30). We rotate [handle] to point\n    // straight up or up-right depending on the handle type.\n    switch (type) {\n      case TextSelectionHandleType.left: // points up-right\n        return Transform.rotate(\n          angle: math.pi / 4.0,\n          child: handle,\n        );\n      case TextSelectionHandleType.right: // points up-left\n        return Transform.rotate(\n          angle: -math.pi / 4.0,\n          child: handle,\n        );\n      case TextSelectionHandleType.collapsed: // points up\n        return handle;\n    }\n  }\n\n  /// Gets anchor for material-style text selection handles.\n  ///\n  /// See [TextSelectionControls.getHandleAnchor].\n  @override\n  Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,\n      [double? startGlyphHeight, double? endGlyphHeight]) {\n    switch (type) {\n      case TextSelectionHandleType.left:\n        return const Offset(_kHandleSize, 0);\n      case TextSelectionHandleType.right:\n        return Offset.zero;\n      default:\n        return const Offset(_kHandleSize / 2, -4);\n    }\n  }\n\n  @override\n  bool canSelectAll(TextSelectionDelegate delegate) {\n    // Android allows SelectAll when selection is not collapsed, unless\n    // everything has already been selected.\n    final TextEditingValue value = delegate.textEditingValue;\n    return delegate.selectAllEnabled \u0026\u0026\n        value.text.isNotEmpty \u0026\u0026\n        !(value.selection.start == 0 \u0026\u0026\n            value.selection.end == value.text.length);\n  }\n}\n\n```\n\n## WidgetSpan\n\n![](https://github.com/fluttercandies/Flutter_Candies/blob/master/gif/extended_text_field/widget_span.gif)\n\nExtendedWidgetSpan 支持选择以及hitTest, 所以你可以在输入框中加入任何的widget。\n\n```dart\nclass EmailText extends SpecialText {\n  final TextEditingController controller;\n  final int start;\n  final BuildContext context;\n  EmailText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,\n      {this.start, this.controller, this.context, String startFlag})\n      : super(startFlag, \" \", textStyle, onTap: onTap);\n\n  @override\n  bool isEnd(String value) {\n    var index = value.indexOf(\"@\");\n    var index1 = value.indexOf(\".\");\n\n    return index \u003e= 0 \u0026\u0026\n        index1 \u003e= 0 \u0026\u0026\n        index1 \u003e index + 1 \u0026\u0026\n        super.isEnd(value);\n  }\n\n  @override\n  InlineSpan finishText() {\n    final String text = toString();\n\n    return ExtendedWidgetSpan(\n      actualText: text,\n      start: start,\n      alignment: ui.PlaceholderAlignment.middle,\n      child: GestureDetector(\n        child: Padding(\n          padding: EdgeInsets.only(right: 5.0, top: 2.0, bottom: 2.0),\n          child: ClipRRect(\n              borderRadius: BorderRadius.all(Radius.circular(5.0)),\n              child: Container(\n                padding: EdgeInsets.all(5.0),\n                color: Colors.orange,\n                child: Row(\n                  mainAxisAlignment: MainAxisAlignment.start,\n                  mainAxisSize: MainAxisSize.min,\n                  children: \u003cWidget\u003e[\n                    Text(\n                      text.trim(),\n                      //style: textStyle?.copyWith(color: Colors.orange),\n                    ),\n                    SizedBox(\n                      width: 5.0,\n                    ),\n                    InkWell(\n                      child: Icon(\n                        Icons.close,\n                        size: 15.0,\n                      ),\n                      onTap: () {\n                        controller.value = controller.value.copyWith(\n                            text: controller.text\n                                .replaceRange(start, start + text.length, \"\"),\n                            selection: TextSelection.fromPosition(\n                                TextPosition(offset: start)));\n                      },\n                    )\n                  ],\n                ),\n              )),\n        ),\n        onTap: () {\n          showDialog(\n              context: context,\n              barrierDismissible: true,\n              builder: (c) {\n                TextEditingController textEditingController =\n                    TextEditingController()..text = text.trim();\n                return Column(\n                  children: \u003cWidget\u003e[\n                    Expanded(\n                      child: Container(),\n                    ),\n                    Material(\n                        child: Padding(\n                      padding: EdgeInsets.all(10.0),\n                      child: TextField(\n                        controller: textEditingController,\n                        decoration: InputDecoration(\n                            suffixIcon: FlatButton(\n                          child: Text(\"OK\"),\n                          onPressed: () {\n                            controller.value = controller.value.copyWith(\n                                text: controller.text.replaceRange(\n                                    start,\n                                    start + text.length,\n                                    textEditingController.text + \" \"),\n                                selection: TextSelection.fromPosition(\n                                    TextPosition(\n                                        offset: start +\n                                            (textEditingController.text + \" \")\n                                                .length)));\n\n                            Navigator.pop(context);\n                          },\n                        )),\n                      ),\n                    )),\n                    Expanded(\n                      child: Container(),\n                    )\n                  ],\n                );\n              });\n        },\n      ),\n      deleteAll: true,\n    );\n  }\n}\n```\n\n## 阻止系统键盘\n\n我们不需要代码侵入到 [ExtendedTextField] 或者 [TextField] 当中， 就可以阻止系统键盘弹出，\n\n### TextInputBindingMixin\n\n我们通过阻止 Flutter Framework 发送 `TextInput.show` 到 Flutter 引擎来阻止系统键盘弹出\n\n你可以直接使用 [TextInputBinding].\n\n``` dart\nvoid main() {\n  TextInputBinding();\n  runApp(const MyApp());\n}\n```\n\n或者你如果有其他的 `binding`，你可以这样。\n\n``` dart\n class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {\n }\n\n void main() {\n   YourBinding();\n   runApp(const MyApp());\n }\n```\n\n或者你需要重载 `ignoreTextInputShow` 方法，你可以这样。\n\n``` dart\n class YourBinding extends TextInputBinding {\n   @override\n   // ignore: unnecessary_overrides\n   bool ignoreTextInputShow() {\n     // you can override it base on your case\n     // if NoKeyboardFocusNode is not enough\n     return super.ignoreTextInputShow();\n   }\n }\n\n void main() {\n   YourBinding();\n   runApp(const MyApp());\n }\n```\n\n### TextInputFocusNode\n\n把 [TextInputFocusNode]  传递给 [ExtendedTextField] 或者 [TextField]。\n\n\n``` dart\nfinal TextInputFocusNode _focusNode = TextInputFocusNode();\n\n  @override\n  Widget build(BuildContext context) {\n    return ExtendedTextField(\n      // request keyboard if need\n      focusNode: _focusNode..debugLabel = 'ExtendedTextField',\n    );\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return TextField(\n      // request keyboard if need\n      focusNode: _focusNode..debugLabel = 'CustomTextField',\n    );\n  }\n```\n\n我们通过当前的 `FocusNode` 是否是 [TextInputFocusNode],来决定是否阻止系统键盘弹出的。\n\n``` dart\n  final FocusNode? focus = FocusManager.instance.primaryFocus;\n  if (focus != null \u0026\u0026\n      focus is TextInputFocusNode \u0026\u0026\n      focus.ignoreSystemKeyboardShow) {\n    return true;\n  }\n```\n### CustomKeyboard\n\n你可以通过当前焦点的变化的时候，来显示或者隐藏自定义的键盘。\n\n当你的自定义键盘可以关闭而不让焦点失去，你应该在 [ExtendedTextField] 或者 [TextField]\n的 `onTap` 事件中，再次判断键盘是否显示。\n\n``` dart\n  @override\n  void initState() {\n    super.initState();\n    _focusNode.addListener(_handleFocusChanged);\n  }\n\n  void _onTextFiledTap() {\n    if (_bottomSheetController == null) {\n      _handleFocusChanged();\n    }\n  }\n\n  void _handleFocusChanged() {\n    if (_focusNode.hasFocus) {\n      // just demo, you can define your custom keyboard as you want\n      _bottomSheetController = showBottomSheet\u003cvoid\u003e(\n          context: FocusManager.instance.primaryFocus!.context!,\n          // set false, if don't want to drag to close custom keyboard\n          enableDrag: true,\n          builder: (BuildContext b) {\n            // your custom keyboard\n            return Container();\n          });\n      // maybe drag close\n      _bottomSheetController?.closed.whenComplete(() {\n        _bottomSheetController = null;\n      });\n    } else {\n      _bottomSheetController?.close();\n      _bottomSheetController = null;\n    }\n  }\n\n  @override\n  void dispose() {\n    _focusNode.removeListener(_handleFocusChanged);\n    super.dispose();\n  }\n```\n\n\n查看 [完整的例子](https://github.com/fluttercandies/extended_text_field/tree/master/example/lib/pages/simple/no_keyboard.dart)\n\n## ☕️Buy me a coffee\n\n![img](http://zmtzawqlp.gitee.io/my_images/images/qrcode.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluttercandies%2Fextended_text_field","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffluttercandies%2Fextended_text_field","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffluttercandies%2Fextended_text_field/lists"}