Files
zh-chat-flutter/lib/views/conversation/widgets/message_field.dart
2022-10-26 14:53:22 +08:00

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();
}
}