675 lines
18 KiB
Dart
675 lines
18 KiB
Dart
import 'dart:async';
|
|
import 'package:chat/configs/app_colors.dart';
|
|
import 'package:chat/routes/contact_routes.dart';
|
|
import 'package:chat/services/tim/conversation_service.dart';
|
|
import 'package:chat/utils/sound_record.dart';
|
|
import 'package:chat/utils/ui_tools.dart';
|
|
import 'package:chat/views/conversation/widgets/tim_emoji_panel.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
// import 'package:flutter_baidu_mapapi_search/flutter_baidu_mapapi_search.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
import 'package:tencent_im_sdk_plugin/enum/conversation_type.dart';
|
|
import 'package:tencent_im_sdk_plugin/models/v2_tim_conversation.dart';
|
|
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
|
|
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
|
|
|
|
class MessageField extends StatefulWidget {
|
|
final V2TimConversation conversation;
|
|
|
|
const MessageField(this.conversation, {Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<MessageField> createState() => _MessageFieldState();
|
|
}
|
|
|
|
class _MessageFieldState extends State<MessageField> {
|
|
final TextEditingController _controller = TextEditingController();
|
|
|
|
bool isVoice = false;
|
|
String _sendText = '';
|
|
double paddingBottom = 0.0;
|
|
String soundTipsText = "手指上滑,取消发送";
|
|
bool isRecording = false;
|
|
bool isInit = false;
|
|
bool isCancelSend = false;
|
|
DateTime startTime = DateTime.now();
|
|
List<StreamSubscription<Object>> subscriptions = [];
|
|
|
|
OverlayEntry? overlayEntry;
|
|
String voiceIcon = "assets/chats/voice_volume_1.png";
|
|
final FocusNode _focusNode = FocusNode();
|
|
bool showMore = false;
|
|
bool showEmojiPanel = false;
|
|
bool showKeyboard = false;
|
|
double lastkeyboardHeight = 0;
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
for (var subscription in subscriptions) {
|
|
subscription.cancel();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> sendMessage() async {
|
|
var text = _controller.text;
|
|
|
|
if (text.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
TimConversationService.to.sendTextMessage(widget.conversation, text);
|
|
_controller.text = '';
|
|
}
|
|
|
|
/// 发送媒体消息
|
|
Future<void> sendMeidaMessage(AssetEntity asset) async {
|
|
if (asset.type == AssetType.image) {
|
|
TimConversationService.to.sendImageMessage(
|
|
widget.conversation,
|
|
asset,
|
|
);
|
|
} else if (asset.type == AssetType.video) {
|
|
TimConversationService.to.sendVideoMessage(
|
|
widget.conversation,
|
|
asset,
|
|
);
|
|
} else {
|
|
UiTools.toast('暂不支持的类型');
|
|
}
|
|
}
|
|
|
|
hideAllPanel() {
|
|
_focusNode.unfocus();
|
|
setState(() {
|
|
showKeyboard = false;
|
|
showMore = false;
|
|
showEmojiPanel = false;
|
|
});
|
|
}
|
|
|
|
double _getBottomHeight() {
|
|
listenKeyBoardStatus();
|
|
if (showMore || showEmojiPanel) {
|
|
return 248.0;
|
|
}
|
|
// 在文本框多行拓展时增加保护区高度
|
|
else if (_controller.text.length >= 46 && showKeyboard == false) {
|
|
return 25;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
listenKeyBoardStatus() {
|
|
final currentKeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
|
// 键盘弹出
|
|
if (currentKeyboardHeight - lastkeyboardHeight > 0) {
|
|
// 保证弹出时showKeyboard为true
|
|
setState(() {
|
|
showKeyboard = true;
|
|
});
|
|
|
|
/// 键盘收回
|
|
} else if (currentKeyboardHeight - lastkeyboardHeight < 0) {}
|
|
|
|
lastkeyboardHeight = MediaQuery.of(context).viewInsets.bottom;
|
|
}
|
|
|
|
_openMore() {
|
|
if (showMore) {
|
|
_focusNode.requestFocus();
|
|
} else {
|
|
_focusNode.unfocus();
|
|
}
|
|
setState(() {
|
|
showKeyboard = showMore;
|
|
showEmojiPanel = false;
|
|
showMore = !showMore;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Divider(height: 0),
|
|
Container(
|
|
padding: EdgeInsets.only(
|
|
left: 8,
|
|
right: 8,
|
|
top: 8,
|
|
bottom: 8 + Get.mediaQuery.viewInsets.bottom,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
_voiceButton(),
|
|
const SizedBox(width: 8),
|
|
Expanded(child: _inputArea()),
|
|
const SizedBox(width: 8),
|
|
_emojiButton(),
|
|
const SizedBox(width: 8),
|
|
_sendButton()
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 0),
|
|
AnimatedContainer(
|
|
height: _getBottomHeight(),
|
|
duration: const Duration(milliseconds: 150),
|
|
padding: showEmojiPanel
|
|
? const EdgeInsets.all(0)
|
|
: const EdgeInsets.all(16),
|
|
child: _getBottomContainer(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _actionWidget(
|
|
String text, {
|
|
required IconData icon,
|
|
VoidCallback? onTap,
|
|
}) {
|
|
return InkWell(
|
|
onTap: () {
|
|
onTap?.call();
|
|
},
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(15),
|
|
decoration: BoxDecoration(
|
|
color: AppColors.white,
|
|
borderRadius: BorderRadius.circular(5),
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: AppColors.unactive,
|
|
),
|
|
),
|
|
const SizedBox(height: 3),
|
|
Text(
|
|
text,
|
|
style: const TextStyle(
|
|
color: AppColors.unactive,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
_openEmojiPanel() {
|
|
if (showEmojiPanel) {
|
|
_focusNode.requestFocus();
|
|
} else {
|
|
_focusNode.unfocus();
|
|
}
|
|
setState(() {
|
|
showKeyboard = showEmojiPanel;
|
|
showMore = false;
|
|
showEmojiPanel = !showEmojiPanel;
|
|
});
|
|
}
|
|
|
|
Widget _emojiButton() {
|
|
return InkWell(
|
|
onTap: () {
|
|
_openEmojiPanel();
|
|
},
|
|
child: const Icon(
|
|
Icons.emoji_emotions_outlined,
|
|
size: 28,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _voiceButton() {
|
|
if (!isVoice) {
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
isVoice = true;
|
|
});
|
|
},
|
|
child: const Icon(
|
|
Icons.mic_outlined,
|
|
size: 28,
|
|
),
|
|
);
|
|
} else {
|
|
return InkWell(
|
|
onTap: () {
|
|
setState(() {
|
|
isVoice = false;
|
|
});
|
|
},
|
|
child: const Icon(
|
|
Icons.keyboard_outlined,
|
|
size: 28,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _inputArea() {
|
|
if (isVoice) {
|
|
return GestureDetector(
|
|
onTapDown: (_) async {
|
|
if (!isInit) {
|
|
var result = await Permission.microphone.request().isGranted;
|
|
if (result) initRecordSound();
|
|
}
|
|
},
|
|
onLongPressStart: onLongPressStart,
|
|
onLongPressMoveUpdate: onLongPressUpdate,
|
|
onLongPressEnd: onLongPressEnd,
|
|
onLongPressCancel: onLonePressCancel,
|
|
child: Container(
|
|
alignment: Alignment.center,
|
|
padding: const EdgeInsets.only(
|
|
left: 8.0,
|
|
right: 8.0,
|
|
top: 8.0,
|
|
bottom: 8.0,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.white,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(
|
|
4.0,
|
|
),
|
|
),
|
|
),
|
|
child: const Text('按住说话'),
|
|
),
|
|
);
|
|
} else {
|
|
return Container(
|
|
padding: const EdgeInsets.only(
|
|
left: 8.0,
|
|
right: 8.0,
|
|
top: 8.0,
|
|
bottom: 8.0,
|
|
),
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.white,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(
|
|
4.0,
|
|
),
|
|
),
|
|
),
|
|
child: TextField(
|
|
controller: _controller,
|
|
focusNode: _focusNode,
|
|
decoration: null,
|
|
onChanged: (e) {
|
|
setState(() {
|
|
_sendText = e;
|
|
});
|
|
},
|
|
onTap: () {
|
|
showKeyboard = true;
|
|
showMore = false;
|
|
showEmojiPanel = false;
|
|
},
|
|
onSubmitted: (value) {
|
|
sendMessage();
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _sendButton() {
|
|
if (_sendText.isEmpty) {
|
|
return InkWell(
|
|
onTap: () {
|
|
_openMore();
|
|
},
|
|
child: const Icon(
|
|
Icons.add_circle_outline_outlined,
|
|
size: 28,
|
|
color: AppColors.active,
|
|
),
|
|
);
|
|
} else {
|
|
return InkWell(
|
|
onTap: () {
|
|
sendMessage();
|
|
},
|
|
child: Container(
|
|
height: 28.0,
|
|
width: 54.0,
|
|
alignment: Alignment.center,
|
|
decoration: const BoxDecoration(
|
|
color: AppColors.primary,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(
|
|
8.0,
|
|
),
|
|
),
|
|
),
|
|
child: const Text(
|
|
'发送',
|
|
style: TextStyle(
|
|
letterSpacing: 2,
|
|
color: AppColors.white,
|
|
fontSize: 14.0,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
initRecordSound() {
|
|
final responseSubscription = SoundPlayer.responseListener((recordResponse) {
|
|
final status = recordResponse.msg;
|
|
if (status == "onStop") {
|
|
if (!isCancelSend) {
|
|
final soundPath = recordResponse.path;
|
|
final recordDuration = recordResponse.audioTimeLength;
|
|
TimConversationService.to.sendSoundMessage(
|
|
widget.conversation, soundPath!, recordDuration!.toInt());
|
|
}
|
|
} else if (status == "onStart") {
|
|
// print("start record");
|
|
setState(() {
|
|
isRecording = true;
|
|
});
|
|
} else {
|
|
// print(status);
|
|
}
|
|
});
|
|
final amplitutdeResponseSubscription =
|
|
SoundPlayer.responseFromAmplitudeListener((recordResponse) {
|
|
final voiceData = double.parse(recordResponse.msg!);
|
|
setState(() {
|
|
if (voiceData > 0 && voiceData < 0.1) {
|
|
voiceIcon = "assets/chats/voice_volume_2.png";
|
|
} else if (voiceData > 0.2 && voiceData < 0.3) {
|
|
voiceIcon = "assets/chats/voice_volume_3.png";
|
|
} else if (voiceData > 0.3 && voiceData < 0.4) {
|
|
voiceIcon = "assets/chats/voice_volume_4.png";
|
|
} else if (voiceData > 0.4 && voiceData < 0.5) {
|
|
voiceIcon = "assets/chats/voice_volume_5.png";
|
|
} else if (voiceData > 0.5 && voiceData < 0.6) {
|
|
voiceIcon = "assets/chats/voice_volume_6.png";
|
|
} else if (voiceData > 0.6 && voiceData < 0.7) {
|
|
voiceIcon = "assets/chats/voice_volume_7.png";
|
|
} else if (voiceData > 0.7 && voiceData < 1) {
|
|
voiceIcon = "assets/chats/voice_volume_7.png";
|
|
} else {
|
|
voiceIcon = "assets/chats/voice_volume_1.png";
|
|
}
|
|
if (overlayEntry != null) {
|
|
overlayEntry!.markNeedsBuild();
|
|
}
|
|
});
|
|
});
|
|
subscriptions = [responseSubscription, amplitutdeResponseSubscription];
|
|
SoundPlayer.initSoundPlayer();
|
|
isInit = true;
|
|
}
|
|
|
|
buildOverLayView(BuildContext context) {
|
|
if (overlayEntry == null) {
|
|
overlayEntry = OverlayEntry(builder: (content) {
|
|
return Positioned(
|
|
top: MediaQuery.of(context).size.height * 0.5 - 80,
|
|
left: MediaQuery.of(context).size.width * 0.5 - 80,
|
|
child: Material(
|
|
type: MaterialType.transparency,
|
|
child: Center(
|
|
child: Opacity(
|
|
opacity: 0.8,
|
|
child: Container(
|
|
width: 160,
|
|
height: 160,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xff77797A),
|
|
borderRadius: BorderRadius.all(Radius.circular(20.0)),
|
|
),
|
|
child: Column(
|
|
children: <Widget>[
|
|
Container(
|
|
margin: const EdgeInsets.only(top: 10),
|
|
child: Image.asset(
|
|
voiceIcon,
|
|
width: 100,
|
|
height: 100,
|
|
// package: 'flutter_plugin_record',
|
|
),
|
|
),
|
|
Text(
|
|
soundTipsText,
|
|
style: const TextStyle(
|
|
fontStyle: FontStyle.normal,
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
Overlay.of(context)!.insert(overlayEntry!);
|
|
}
|
|
}
|
|
|
|
onLongPressStart(_) {
|
|
if (isInit) {
|
|
startTime = DateTime.now();
|
|
SoundPlayer.startRecord();
|
|
buildOverLayView(context);
|
|
}
|
|
}
|
|
|
|
onLongPressUpdate(e) {
|
|
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
|
double dy = e.localPosition.dy;
|
|
if (dy.abs() > height) {
|
|
if (mounted && soundTipsText != '松开取消') {
|
|
setState(() {
|
|
soundTipsText = '松开取消';
|
|
});
|
|
}
|
|
} else {
|
|
if (mounted && soundTipsText == '松开取消') {
|
|
setState(() {
|
|
soundTipsText = '手指上滑,取消发送';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
onLongPressEnd(e) {
|
|
double dy = e.localPosition.dy;
|
|
// 此高度为 160为录音取消组件距离顶部的预留距离
|
|
double height = MediaQuery.of(context).size.height * 0.5 - 240;
|
|
if (dy.abs() > height) {
|
|
isCancelSend = true;
|
|
} else {
|
|
isCancelSend = false;
|
|
}
|
|
if (overlayEntry != null) {
|
|
overlayEntry!.remove();
|
|
overlayEntry = null;
|
|
}
|
|
// Did not receive onStop from FlutterPluginRecord if the duration is too short.
|
|
if (DateTime.now().difference(startTime).inSeconds < 1) {
|
|
isCancelSend = true;
|
|
UiTools.toast('说话时间太短!');
|
|
}
|
|
stop();
|
|
}
|
|
|
|
onLonePressCancel() {
|
|
if (isRecording) {
|
|
isCancelSend = true;
|
|
if (overlayEntry != null) {
|
|
overlayEntry!.remove();
|
|
overlayEntry = null;
|
|
}
|
|
stop();
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
setState(() {
|
|
isRecording = false;
|
|
});
|
|
SoundPlayer.stopRecord();
|
|
setState(() {
|
|
soundTipsText = '手指上滑,取消发送';
|
|
});
|
|
}
|
|
|
|
_getMoreActions() {
|
|
var list = List<Widget>.empty(growable: true);
|
|
list.add(
|
|
_actionWidget('相册', icon: Icons.photo, onTap: () async {
|
|
var result = await AssetPicker.pickAssets(
|
|
context,
|
|
pickerConfig: const AssetPickerConfig(
|
|
maxAssets: 9,
|
|
requestType: RequestType.common,
|
|
),
|
|
);
|
|
if (result == null) {
|
|
return;
|
|
}
|
|
for (var asset in result) {
|
|
sendMeidaMessage(asset);
|
|
}
|
|
}),
|
|
);
|
|
list.add(
|
|
_actionWidget('拍照', icon: Icons.photo_camera, onTap: () async {
|
|
var asset = await CameraPicker.pickFromCamera(
|
|
context,
|
|
pickerConfig: const CameraPickerConfig(
|
|
enableRecording: true,
|
|
),
|
|
);
|
|
if (asset == null) {
|
|
return;
|
|
}
|
|
sendMeidaMessage(asset);
|
|
}),
|
|
);
|
|
if (widget.conversation.type == ConversationType.V2TIM_C2C) {
|
|
list.add(
|
|
_actionWidget(
|
|
'视频通话',
|
|
icon: Icons.videocam,
|
|
onTap: () {
|
|
// ImTools.showTrtcMessage(widget.conversation.userID!);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
list.add(
|
|
_actionWidget(
|
|
'位置',
|
|
icon: Icons.pin_drop,
|
|
onTap: () {
|
|
// Get.toNamed(ImRoutes.conversationMap)?.then((value) {
|
|
// var _bmfPoiInfo = value['result'] as BMFPoiInfo;
|
|
// var snapshot = value['snapshot'] as Uint8List?;
|
|
// var model = LocationModel(
|
|
// name: _bmfPoiInfo.name!,
|
|
// address: _bmfPoiInfo.address!,
|
|
// list: snapshot!,
|
|
// latitude: _bmfPoiInfo.pt?.latitude ?? 0,
|
|
// longitude: _bmfPoiInfo.pt?.longitude ?? 0,
|
|
// );
|
|
// TimConversationService.to.sendLocationMessage(
|
|
// widget.conversation,
|
|
// model,
|
|
// );
|
|
// });
|
|
},
|
|
),
|
|
);
|
|
list.add(
|
|
_actionWidget(
|
|
'名片',
|
|
icon: Icons.person,
|
|
onTap: () {
|
|
Get.toNamed(
|
|
ContactRoutes.friend,
|
|
arguments: {
|
|
'name_card': true,
|
|
},
|
|
)?.then((value) {
|
|
var model = value?['result'];
|
|
if (model != null) {
|
|
TimConversationService.to.sendCustomMessage(
|
|
widget.conversation,
|
|
model,
|
|
'NAME_CARD',
|
|
);
|
|
}
|
|
});
|
|
},
|
|
),
|
|
);
|
|
list.add(
|
|
_actionWidget(
|
|
'文件',
|
|
icon: Icons.folder,
|
|
onTap: () async {
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
|
if (result != null) {
|
|
TimConversationService.to.sendFileMessage(
|
|
widget.conversation,
|
|
result.names.first!,
|
|
result.paths.first!,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
);
|
|
return list;
|
|
}
|
|
|
|
Widget _getBottomContainer() {
|
|
if (showEmojiPanel) {
|
|
return EmojiPanel(
|
|
onTapEmoji: (unicode) {
|
|
final oldText = _controller.text;
|
|
final newText = String.fromCharCode(unicode);
|
|
_controller.text = "$oldText$newText";
|
|
setState(() {
|
|
_sendText = _controller.text;
|
|
});
|
|
},
|
|
);
|
|
}
|
|
if (showMore) {
|
|
return GridView.count(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
crossAxisCount: 4,
|
|
children: _getMoreActions(),
|
|
);
|
|
}
|
|
return Container();
|
|
}
|
|
}
|