You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter的TextFormField中嵌入可退格删除的图片

Flutter中实现可嵌入图片且支持退格删除的输入框

以下是几种可行的实现方案,满足将图片作为聊天内容一部分、支持退格删除的需求:

方案一:自定义TextEditingController + 富文本渲染

通过自定义TextEditingController维护文本与图片的混合数据源,重写富文本渲染逻辑,并处理退格事件:

  1. 定义数据源结构
    创建类管理输入内容的分段(文本或图片):
class InputSegment {
  final bool isImage;
  final String? text;
  final ImageProvider? image;

  InputSegment.text(this.text) : isImage = false, image = null;
  InputSegment.image(this.image) : isImage = true, text = null;
}

class InputContent {
  final List<InputSegment> segments = [];
}
  1. 自定义TextEditingController
    重写buildTextSpan方法将图片分段渲染为WidgetSpan,同时实现退格处理逻辑:
class RichInputController extends TextEditingController {
  final InputContent content = InputContent();

  @override
  TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {
    final children = <InlineSpan>[];
    for (var segment in content.segments) {
      if (segment.isImage && segment.image != null) {
        children.add(WidgetSpan(
          child: Container(
            width: 40,
            height: 40,
            margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.circular(8),
              image: DecorationImage(image: segment.image!, fit: BoxFit.cover),
            ),
          ),
        ));
      } else if (!segment.isImage && segment.text != null) {
        children.add(TextSpan(text: segment.text, style: style));
      }
    }
    return TextSpan(style: style, children: children);
  }

  // 处理退格逻辑
  void handleBackspace() {
    if (content.segments.isEmpty) return;
    final last = content.segments.last;
    if (last.isImage) {
      content.segments.removeLast();
    } else if (last.text!.length > 1) {
      content.segments.removeLast();
      content.segments.add(InputSegment.text(last.text!.substring(0, last.text!.length - 1)));
    } else {
      content.segments.removeLast();
    }
    notifyListeners();
  }

  // 添加文本分段
  void addText(String text) {
    if (content.segments.isNotEmpty && !content.segments.last.isImage) {
      final lastText = content.segments.last.text!;
      content.segments.removeLast();
      content.segments.add(InputSegment.text(lastText + text));
    } else {
      content.segments.add(InputSegment.text(text));
    }
    notifyListeners();
  }

  // 添加图片分段
  void addImage(ImageProvider image) {
    content.segments.add(InputSegment.image(image));
    notifyListeners();
  }
}
  1. 在TextFormField中使用
    监听键盘退格事件,同步文本输入与自定义数据源:
final _richController = RichInputController();
final _focusNode = FocusNode();

@override
Widget build(BuildContext context) {
  return TextFormField(
    controller: _richController,
    focusNode: _focusNode,
    decoration: const InputDecoration(hintText: "输入消息或插入图片"),
    onChanged: (value) {
      // 过滤占位符,仅处理实际输入文本
      final pureText = value.replaceAll(RegExp(r'\[img-\d+\]'), '');
      if (pureText.isNotEmpty) {
        _richController.addText(pureText);
        // 重置controller的text为占位符,避免显示重复内容
        _richController.text = _richController.content.segments.map((s) {
          return s.isImage ? '[img-${DateTime.now().microsecondsSinceEpoch}]' : s.text!;
        }).join();
      }
    },
    onKeyEvent: (node, event) {
      if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) {
        _richController.handleBackspace();
        // 更新controller的text占位符
        _richController.text = _richController.content.segments.map((s) {
          return s.isImage ? '[img-${DateTime.now().microsecondsSinceEpoch}]' : s.text!;
        }).join();
        return KeyEventResult.handled;
      }
      return KeyEventResult.ignored;
    },
  );
}

方案二:基于flutter_quill快速实现

flutter_quill作为成熟的富文本编辑器,原生支持图片嵌入,只需自定义退格逻辑即可满足需求:

  1. 添加依赖
    在pubspec.yaml中添加:
dependencies:
  flutter_quill: ^latest_version
  1. 初始化控制器并配置编辑器
final _quillController = QuillController.basic();
final _focusNode = FocusNode();

@override
Widget build(BuildContext context) {
  return QuillEditor(
    controller: _quillController,
    focusNode: _focusNode,
    autoFocus: false,
    readOnly: false,
    expands: false,
    padding: EdgeInsets.zero,
    customStyles: DefaultStyles(
      paragraph: DefaultTextBlockStyle(
        const TextStyle(color: Colors.black, fontSize: 16),
        EdgeInsets.zero,
        EdgeInsets.zero,
        null,
      ),
    ),
    onKeyEvent: (event) {
      if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.backspace) {
        final selection = _quillController.selection;
        if (selection.isCollapsed) {
          final pos = selection.baseOffset;
          final node = _quillController.document.queryNode(pos);
          // 判断当前节点是否为图片嵌入
          if (node is ImageEmbed) {
            // 删除图片节点,需根据节点长度调整偏移
            _quillController.delete(pos - 1, pos + 1);
            return KeyEventResult.handled;
          }
        }
      }
      return KeyEventResult.ignored;
    },
  );
}
  1. 插入图片的方法
// 插入本地图片
void insertLocalImage() {
  _quillController.insertImage(
    FileImage(File('path/to/image.png')),
    width: 40,
    height: 40,
  );
}

// 插入网络图片
void insertNetworkImage() {
  _quillController.insertImage(
    NetworkImage('https://example.com/image.png'),
    width: 40,
    height: 40,
  );
}

方案三:自定义混合输入组件(高度定制)

若需完全自定义UI,可通过Wrap组件混合TextField与图片Widget,手动维护输入内容列表:

  • 用Wrap包裹多个子组件,每个组件为TextField(文本段)或Image(图片段)
  • 监听退格事件,当当前TextField为空时,删除前一个组件
  • 文本输入时动态更新TextField内容,或添加新的TextField段

此方案需处理光标切换、内容拼接等细节,适合对UI有极致定制需求的场景。

内容的提问来源于stack exchange,提问作者Sindu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 20:54:53