You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

403 lines
13 KiB

import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:extended_text_field/extended_text_field.dart';
import '../../generated/assets.dart';
import '../../config/emoji_config.dart';
import '../emoji_panel.dart';
import 'more_options_view.dart';
import 'voice_input_view.dart';
/// 表情特殊文本构建器 - 用于在 ExtendedTextField 中显示表情
class EmojiSpecialTextSpanBuilder extends SpecialTextSpanBuilder {
@override
SpecialText? createSpecialText(
String flag, {
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
required int index,
}) {
// 匹配 [emoji:xxx] 格式
if (flag.startsWith('[emoji:')) {
return EmojiSpecialText(
textStyle: textStyle,
onTap: onTap,
);
}
return null;
}
}
/// 表情特殊文本类
class EmojiSpecialText extends SpecialText {
EmojiSpecialText({
TextStyle? textStyle,
SpecialTextGestureTapCallback? onTap,
}) : super(
'[emoji:',
']',
textStyle,
onTap: onTap,
);
@override
InlineSpan finishText() {
// 提取表情ID
final emojiId = toString().replaceAll('[emoji:', '').replaceAll(']', '');
final emoji = EmojiConfig.getEmojiById(emojiId);
if (emoji != null) {
// 返回包含表情图片的 WidgetSpan
return WidgetSpan(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 2.w),
child: Image.asset(
emoji.path,
width: 24.w,
height: 24.w,
fit: BoxFit.contain,
),
),
);
}
// 如果表情不存在,返回普通文本
return TextSpan(
text: toString(),
style: textStyle,
);
}
}
/// 表情文本输入格式化器 - 处理删除时一次性删除整个表情标记
class EmojiTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
// 如果文本长度减少(删除操作)
if (newValue.text.length < oldValue.text.length) {
final oldSelection = oldValue.selection;
final cursorOffset = oldSelection.baseOffset;
final deletedLength = oldValue.text.length - newValue.text.length;
// 在整个文本中查找所有表情标记
final emojiRegex = RegExp(r'\[emoji:\d+\]');
final allMatches = emojiRegex.allMatches(oldValue.text);
// 查找光标所在位置或光标前最近的表情标记
for (final match in allMatches) {
final emojiStart = match.start;
final emojiEnd = match.end;
// 如果光标在表情标记内部(不包括开始位置,包括结束位置)
if (cursorOffset > emojiStart && cursorOffset <= emojiEnd) {
// 删除整个表情标记
final beforeEmoji = oldValue.text.substring(0, emojiStart);
final afterEmoji = oldValue.text.substring(emojiEnd);
final newText = beforeEmoji + afterEmoji;
// 删除表情后,光标保持在删除操作发生的位置
// 跟删除普通文本一样:删除后光标停留在删除位置
// 光标位置 = 删除位置 - 表情长度
final emojiLength = emojiEnd - emojiStart;
int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length);
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: newCursorOffset),
);
}
// 如果光标刚好在表情标记的开始位置,且删除的是表情标记内部的字符
if (cursorOffset == emojiStart) {
// 检查删除的字符是否在表情标记范围内
if (cursorOffset + deletedLength <= emojiEnd) {
// 删除整个表情标记
final beforeEmoji = oldValue.text.substring(0, emojiStart);
final afterEmoji = oldValue.text.substring(emojiEnd);
final newText = beforeEmoji + afterEmoji;
// 删除表情后,光标保持在删除位置
// 光标位置 = 删除位置 - 表情长度
final emojiLength = emojiEnd - emojiStart;
int newCursorOffset = (cursorOffset - emojiLength).clamp(0, newText.length);
return TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: newCursorOffset),
);
}
}
}
}
return newValue;
}
}
class ChatInputBar extends StatefulWidget {
final ValueChanged<String> onSendMessage;
final ValueChanged<List<String>>? onImageSelected;
final Function(String filePath, int seconds)? onVoiceRecorded;
final VoidCallback? onVoiceCall; // 语音通话回调
final Future<void> Function()? onVideoCall; // 视频通话回调
final VoidCallback? onGiftTap; // 礼物按钮回调
const ChatInputBar({
required this.onSendMessage,
this.onImageSelected,
this.onVoiceRecorded,
this.onVoiceCall,
this.onVideoCall,
this.onGiftTap,
super.key,
});
@override
State<ChatInputBar> createState() => _ChatInputBarState();
}
class _ChatInputBarState extends State<ChatInputBar> {
final TextEditingController _textController = TextEditingController();
final FocusNode _focusNode = FocusNode();
bool _isMoreOptionsVisible = false;
bool _isVoiceVisible = false;
bool _isEmojiVisible = false;
void _handleSendMessage() {
if (_textController.text.isNotEmpty) {
widget.onSendMessage(_textController.text);
_textController.clear();
}
}
// 切换更多选项的显示状态
void _toggleMoreOptions() {
print('📷 [ChatInputBar] 更多选项(图片)按钮被点击');
setState(() {
_isMoreOptionsVisible = !_isMoreOptionsVisible;
if (_isMoreOptionsVisible) {
_isVoiceVisible = false;
_isEmojiVisible = false;
}
// 收起键盘
FocusManager.instance.primaryFocus?.unfocus();
});
}
void _handleImageTap(List<String> imagePaths) {
// 将图片路径列表传递给父组件
if (widget.onImageSelected != null) {
widget.onImageSelected!(imagePaths);
}
}
void _handleCameraTap(String imagePath) {
// 将单个图片路径包装成列表传递给父组件
if (widget.onImageSelected != null) {
widget.onImageSelected!([imagePath]);
}
}
void _toggleVoiceOptions() {
setState(() {
_isVoiceVisible = !_isVoiceVisible;
if (_isVoiceVisible) {
_isMoreOptionsVisible = false;
_isEmojiVisible = false;
}
FocusManager.instance.primaryFocus?.unfocus();
});
}
void _toggleEmojiPanel() {
setState(() {
_isEmojiVisible = !_isEmojiVisible;
if (_isEmojiVisible) {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
}
FocusManager.instance.primaryFocus?.unfocus();
});
}
// 关闭所有控制面板
void _closeAllPanels() {
if (!mounted) return;
if (_isMoreOptionsVisible || _isVoiceVisible || _isEmojiVisible) {
setState(() {
_isMoreOptionsVisible = false;
_isVoiceVisible = false;
_isEmojiVisible = false;
});
}
}
@override
void initState() {
super.initState();
// 监听输入框焦点变化
_focusNode.addListener(() {
if (_focusNode.hasFocus && mounted) {
// 输入框获得焦点(键盘弹起),关闭所有控制面板
_closeAllPanels();
}
});
}
@override
void dispose() {
_focusNode.dispose();
_textController.dispose();
super.dispose();
}
void _handleEmojiSelected(EmojiItem emoji) {
// 将表情添加到输入框
final currentText = _textController.text;
final emojiText = '[emoji:${emoji.id}]';
final newText = currentText + emojiText;
_textController.text = newText;
// 将光标移到末尾
_textController.selection = TextSelection.fromPosition(
TextPosition(offset: newText.length),
);
setState(() {}); // 刷新显示
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 16.w),
color: Colors.white,
child: Column(
children: [
SizedBox(height: 10.h),
Row(
children: [
Expanded(
child: Container(
height: 40.h,
decoration: BoxDecoration(
color: Color(0xffF5F5F5),
borderRadius: BorderRadius.circular(5.h),
),
padding: EdgeInsets.symmetric(horizontal: 16.w),
child: ExtendedTextField(
controller: _textController,
focusNode: _focusNode,
specialTextSpanBuilder: EmojiSpecialTextSpanBuilder(),
inputFormatters: [
EmojiTextInputFormatter(),
],
decoration: InputDecoration(
border: InputBorder.none,
hintText: "请输入聊天内容~",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Colors.grey,
),
),
style: TextStyle(
fontSize: 14.sp,
color: Colors.black,
),
onChanged: (value) {
setState(() {}); // 刷新以更新表情显示
},
),
),
),
SizedBox(width: 12.w),
// 发送按钮
Container(
padding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 8.h,
),
decoration: BoxDecoration(
color: Color.fromRGBO(117, 98, 249, 1),
borderRadius: BorderRadius.circular(5.h),
),
child: Text(
"发送",
style: TextStyle(
fontSize: 14.sp,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
).onTap(_handleSendMessage),
],
),
SizedBox(height: 12.h),
// 底部工具栏
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// 语音消息按钮
Image.asset(
Assets.imagesAudio,
width: 24.w,
height: 24.w,
).onTap(_toggleVoiceOptions),
// 语音通话按钮(暂时隐藏)
// Image.asset(
// Assets.imagesSendCall,
// width: 24.w,
// height: 24.w,
// ).onTap(() {
// widget.onVoiceCall?.call();
// }),
// 视频通话按钮(暂时隐藏)
Image.asset(
Assets.imagesVideoCall,
width: 24.w,
height: 24.w,
).onTap(() async {
await widget.onVideoCall?.call();
}),
// 礼物按钮
Image.asset(Assets.imagesGift, width: 24.w, height: 24.w).onTap(() {
widget.onGiftTap?.call();
}),
// 表情按钮
Image.asset(Assets.imagesEmoji, width: 24.w, height: 24.w).onTap(_toggleEmojiPanel),
// 更多按钮
Image.asset(
Assets.imagesAdd,
width: 24.w,
height: 24.w,
).onTap(_toggleMoreOptions),
],
),
],
),
),
// 更多选项展开视图(支持图片)
MoreOptionsView(
isVisible: _isMoreOptionsVisible,
onImageSelected: _handleImageTap,
onCameraSelected: _handleCameraTap,
),
// 语音输入展开视图(与 MoreOptionsView 相同的展开方式)
VoiceInputView(
isVisible: _isVoiceVisible,
onVoiceRecorded: widget.onVoiceRecorded,
),
// 表情面板
EmojiPanel(
isVisible: _isEmojiVisible,
onEmojiSelected: _handleEmojiSelected,
),
],
);
}
}