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 createState() => _MessageFieldState(); } class _MessageFieldState extends State { 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> 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 sendMessage() async { var text = _controller.text; if (text.isEmpty) { return; } TimConversationService.to.sendTextMessage(widget.conversation, text); _controller.text = ''; } /// 发送媒体消息 Future 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: [ 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.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(); } }