|
|
|
@ -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<ChatInputBar> { |
|
|
|
setState(() {}); // 刷新显示 |
|
|
|
} |
|
|
|
|
|
|
|
/// 构建输入框内容(文本+表情) |
|
|
|
List<Widget> _buildInputContentWidgets() { |
|
|
|
final List<Widget> 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<ChatInputBar> { |
|
|
|
child: Stack( |
|
|
|
children: [ |
|
|
|
// 真实的输入框 |
|
|
|
TextField( |
|
|
|
ExtendedTextField( |
|
|
|
controller: _textController, |
|
|
|
focusNode: _focusNode, |
|
|
|
decoration: InputDecoration( |
|
|
|
@ -229,31 +175,19 @@ class _ChatInputBarState extends State<ChatInputBar> { |
|
|
|
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<ChatInputBar> { |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 表情特殊文本构建器 - 参考 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); |
|
|
|
} |
|
|
|
} |