From a78693c6e7e0ae240725d6dbbbb070347e1473aa Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Sat, 10 Jan 2026 23:34:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=E6=9B=BF=E6=8D=A2=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E8=BE=93=E5=85=A5=E6=A1=86=E4=B8=BA=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=AD=97=E6=AE=B5=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=A1=A8=E6=83=85=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成 extended_text 和 extended_text_field 依赖包 - 移除自定义表情渲染逻辑和 _buildInputContentWidgets 方法 - 将 TextField 替换为 ExtendedTextField 组件 - 添加 MySpecialTextSpanBuilder 类处理特殊文本标记 - 实现 MyEmojiText 类解析 [emoji:id] 格式并渲染表情图片 - 配置 ImageSpan 显示表情图像并设置合适的尺寸 - 添加输入格式化器过滤零宽空格字符 --- lib/widget/message/chat_input_bar.dart | 168 ++++++++++++++----------- pubspec.yaml | 1 + 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/lib/widget/message/chat_input_bar.dart b/lib/widget/message/chat_input_bar.dart index bfbd66a..1bc1875 100644 --- a/lib/widget/message/chat_input_bar.dart +++ b/lib/widget/message/chat_input_bar.dart @@ -1,5 +1,8 @@ import 'package:dating_touchme_app/extension/ex_widget.dart'; +import 'package:extended_text/extended_text.dart'; +import 'package:extended_text_field/extended_text_field.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../generated/assets.dart'; @@ -137,63 +140,6 @@ class _ChatInputBarState extends State { setState(() {}); // 刷新显示 } - /// 构建输入框内容(文本+表情) - List _buildInputContentWidgets() { - final List widgets = []; - final text = _textController.text; - final RegExp emojiRegex = RegExp(r'\[emoji:(\d+)\]'); - - int lastMatchEnd = 0; - final matches = emojiRegex.allMatches(text); - - for (final match in matches) { - // 添加表情之前的文本 - if (match.start > lastMatchEnd) { - final textPart = text.substring(lastMatchEnd, match.start); - widgets.add( - Text( - textPart, - style: TextStyle(fontSize: 14.sp, color: Colors.black), - ), - ); - } - - // 添加表情图片 - final emojiId = match.group(1); - if (emojiId != null) { - final emoji = EmojiConfig.getEmojiById(emojiId); - if (emoji != null) { - widgets.add( - Padding( - padding: EdgeInsets.symmetric(horizontal: 2.w), - child: Image.asset( - emoji.path, - width: 24.w, - height: 24.w, - fit: BoxFit.contain, - ), - ), - ); - } - } - - lastMatchEnd = match.end; - } - - // 添加剩余的文本 - if (lastMatchEnd < text.length) { - final textPart = text.substring(lastMatchEnd); - widgets.add( - Text( - textPart, - style: TextStyle(fontSize: 14.sp, color: Colors.black), - ), - ); - } - - return widgets; - } - @override Widget build(BuildContext context) { return Column( @@ -218,7 +164,7 @@ class _ChatInputBarState extends State { child: Stack( children: [ // 真实的输入框 - TextField( + ExtendedTextField( controller: _textController, focusNode: _focusNode, decoration: InputDecoration( @@ -229,31 +175,19 @@ class _ChatInputBarState extends State { color: Colors.grey, ), ), + inputFormatters: [ + // 可以添加其他格式化器,但不要添加过滤Unicode的规则 + FilteringTextInputFormatter.deny(RegExp(r'[\u200B]')), // 仅示例:过滤零宽空格 + ], + specialTextSpanBuilder: MySpecialTextSpanBuilder(), style: TextStyle( fontSize: 14.sp, - color: _textController.text.contains('[emoji:') - ? Colors.transparent - : Colors.black, + color: Colors.black, // 文字始终显示为黑色,specialTextSpanBuilder 会处理表情 ), onChanged: (value) { setState(() {}); // 刷新以更新表情显示 }, ), - // 表情显示层 - if (_textController.text.contains('[emoji:')) - Positioned.fill( - child: IgnorePointer( - child: Align( - alignment: Alignment.centerLeft, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: _buildInputContentWidgets(), - ), - ), - ), - ), - ), ], ), ), @@ -344,3 +278,85 @@ class _ChatInputBarState extends State { ); } } + + +/// 表情特殊文本构建器 - 参考 TIMUIKitTextField 的实现 +class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder { + @override + SpecialText? createSpecialText( + String flag, { + TextStyle? textStyle, + SpecialTextGestureTapCallback? onTap, + required int index, + }) { + if (flag.isEmpty) { + return null; + } + + // index 是 start flag 的结束位置,所以文本开始位置应该是 index - (flag.length - 1) + // 使用 '[' 作为 startFlag,参考 EmojiText.flag = '[' + if (isStart(flag, MyEmojiText.flag)) { + return MyEmojiText( + textStyle, + start: index - (MyEmojiText.flag.length - 1), + ); + } + return null; + } +} + +/// 表情特殊文本类 - 参考 TIMUIKitTextField 的 EmojiText 实现 +class MyEmojiText extends SpecialText { + static const String flag = '['; + final int start; + + MyEmojiText( + TextStyle? textStyle, { + required this.start, + }) : super(MyEmojiText.flag, ']', textStyle); + + @override + InlineSpan finishText() { + // toString() 返回完整的匹配文本,例如 "[emoji:11]" + final String key = toString(); + + // 检查是否是我们的表情格式 [emoji:数字] + final RegExp emojiPattern = RegExp(r'^\[emoji:(\d+)\]$'); + final match = emojiPattern.firstMatch(key); + + if (match != null && match.groupCount > 0) { + final emojiId = match.group(1); + if (emojiId != null && emojiId.isNotEmpty) { + final emoji = EmojiConfig.getEmojiById(emojiId); + if (emoji != null && emoji.path.isNotEmpty) { + // 使用 ImageSpan,完全参考 TIMUIKitTextField 的实现 + double size = 16; + final TextStyle ts = textStyle!; + if (ts.fontSize != null) { + // 参考 TIMUIKitTextField: size = ts.fontSize! * 1.44 + // 但我们使用 flutter_screenutil,所以需要适配 + // 如果 fontSize 已经是适配后的值(如 14.sp),则直接计算 + size = ts.fontSize! * 1.44; + // 将 sp 单位转换为实际像素值,因为 ImageSpan 需要像素值 + // flutter_screenutil 的 .sp 会返回像素值,所以这里直接使用 + } else { + // 如果没有设置 fontSize,使用默认值 14.sp * 1.44 + size = 14.w * 1.44; + } + + return ImageSpan( + AssetImage(emoji.path), + actualText: key, + imageWidth: size, + imageHeight: size, + start: start, + margin: const EdgeInsets.all(0), // 零边距避免空格问题 + ); + } + } + } + + // 如果匹配失败或表情不存在,显示原始文本 + return TextSpan(text: key, style: textStyle); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 58be826..378288a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -81,6 +81,7 @@ dependencies: ota_update: ^7.1.0 flutter_local_notifications: ^19.5.0 app_badge_plus: ^1.2.6 + extended_text: ^15.0.2 extended_text_field: ^16.0.2 emoji_text_field: ^1.0.0