Flutter高仿微信系列共59篇,从Flutter客户端、Kotlin客户端、Web服务器、数据库表结构、Xmpp即时通讯服务器、视频通话服务器、腾讯云服务器全面讲解。
详情请查看
效果图:
实现代码:
单聊包含:文本、表情、语音、图片、小视频、红包、转账、视频通话、语音通话功能,有4个widget:
home_chat_page.dart、chat_add_view.dart、chat_content_view.dart、chat_voice_view.dart
home_chat_page.dart实现:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/8/24 14:48 * Description : 单聊页面 */ class HomeChatPage extends StatefulWidget { String toChatId; String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT); HomeChatPage({required this.toChatId}); @override _HomeChatPageState createState() => _HomeChatPageState(toChatId); } class _HomeChatPageState extends State<HomeChatPage> with TickerProviderStateMixin { String _toChatId; _HomeChatPageState(this._toChatId); //好友账户 UserBean? _otherUserBean; //我的账户 UserBean? _meUserBean; List<String> addTimeList = []; List<ChatBean> items = []; ScrollController _controller = ScrollController(); var chatEvent; //每页13条 static const PAGE_SIZE = 13; //当前页 var PAGE_NUM = 1; //从那一条开始(为保证最新的先显示, 先查询最后的,并且不能用desc查询) var startNum = 0; //总共多少条 var CHAT_TOTAL = 0; @override void initState() { super.initState(); AppManager.getInstance().toChatId = _toChatId; _checkAvailable(); _updateChatStatus(); chatEvent = eventBus.on<ChatBean>((chatBean) { if(mounted){ setState(() { chatBean as ChatBean; items.add(chatBean); }); } }); chatEvent = eventBus.on<RedPacketBean>((redPacketBean) { setState(() { _updateRedpacketBalance(redPacketBean); }); }); loadUserBean(); loadAllChat(); jumpToBottom(400); // 监听滚动事件 _controller.addListener((){ if(_controller.position.pixels>_controller.position.maxScrollExtent-40){ } }); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { _initScreenUtil(context); }); } //更新红包金额 void _updateRedpacketBalance(RedPacketBean redPacketBean){ LogUtils.d("home_chat_page 的金额:${redPacketBean?.position}"); ChatBean chatBean = items[redPacketBean.position??0]; String messageId = chatBean.messageId??""; int isClick = 1; chatBean.isClick = isClick; ChatRepository.getInstance().updateChatRedPacketStatus(messageId, isClick); setState(() { }); Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.account, toUser: widget.toChatId, balance: "${chatBean.content}", addTime: chatBean.addTime??"",))); } @override void dispose() { super.dispose(); eventBus.off(chatEvent); AppManager.getInstance().toChatId = ""; } final controller = TextEditingController(); final FocusNode _chatContentFocus = FocusNode(); @override Widget build(BuildContext context) { if(!isLoadMore){ //每次发送消息滚动到底部0.1秒 jumpToBottom(100); } return Scaffold( appBar: WnAppBar.getAppBar(context, Text("${_otherUserBean?.nickName??""}")), bottomNavigationBar: Text(""),//占用底部位置 body: GestureDetector( onTap: (){ _processClickBlank(); }, child: Container( child: Column( children: <Widget>[ Expanded( child: RefreshIndicator( displacement: 2, onRefresh: _onRefresh, child: ListView.builder( controller: _controller, itemBuilder: (BuildContext context, int index) { return ChatContentView(items: items,account: widget.account, chatBean: items[index], index: index, meUserBean: _meUserBean,addTimeList: addTimeList, otherUserBean: _otherUserBean, deleteCallback: (data){ setState(() { items.remove(items[index]); }); },clickVoiceCallback: (data){ //点击播放录音,暂停后再播放 for(int i = 0; i < items.length; i++){ if(i == index){ items[i].isPlayVoice = true; } else { items[i].isPlayVoice = false; } } setState(() { }); }, refreshTransfer: (position){ LogUtils.d("回调刷新:${position}"); _refreshTransfer(position); },); }, itemCount: items.length, ) ), ), Divider(height: 12.0, color: Color(0xFFF7F8F8),), Container( padding: EdgeInsets.only(top: 5.0, bottom: 5.0, right: 2.0, left: 2.0), color: Color(0xFFF3F3F3), width: double.infinity, child: Row( children: <Widget>[ Container( margin: EdgeInsets.symmetric(horizontal: 2.0), child: IconButton( //按下语音说活 icon: isPressVoice ? Image.asset("assets/chat/button_keyboard.png"):Image.asset("assets/chat/button_voice.png"), onPressed: () =>{ _processPressVoice() } ), //触发发送消息事件执行的函数_handleSubmitted ), Expanded( child: Stack( children: [ Offstage( offstage: !isPressVoice, child: ChatVoiceView( refreshMediaCallback: (type, mediaURL, thumbnailFileName, second, messageId){ _refreshMedia(type, mediaURL, thumbnailFileName, mediaSecond: second, messageId: messageId); }, sendMedialCallback: (type, mediaURL, second, messageId){ _sendMedia(type, mediaURL, mediaSecond: second, messageId: messageId); }, stopPlayVoiceCallback: (){ hidePlayVoiceList(); }, ), ), Offstage( offstage: isPressVoice, child: Container( padding: EdgeInsets.only(top: 8.0, bottom: 8.0, left: 8.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Colors.white), child: TextField( controller: controller, focusNode: _chatContentFocus, decoration: InputDecoration.collapsed(hintText: null), autocorrect: true, //是否自动更正 autofocus: false, maxLines: 5, minLines: 1, textAlign: TextAlign.start, style: TextStyle(color: Colors.black, fontSize: 20), cursorColor: Colors.green, onTap: (){ //点击编辑框 jumpToBottom(400); hideEmoji = true; hideAdd = true; setState(() { }); }, onChanged: (value){ //录入文字 setState(() { if(value.length>0){ hideSend = false; hideAddIcon = true; } else { hideSend = true; hideAddIcon = false; } }); }, onSubmitted: _handleSubmitted, enabled: true, //是否禁用 ), ), ), ], ), ), Container( child: IconButton( icon: Image.asset("assets/chat/button_emoji.png"), onPressed: () => _processEmoji()), ), Offstage( offstage: hideAddIcon, child: Container( //margin: EdgeInsets.only(right: 4.0), child: IconButton( //添加按钮 icon: Image.asset("assets/chat/button_add.png"), onPressed: () => { _processAdd() } ), ), ), Offstage( offstage: hideSend, child: Container( margin: EdgeInsets.symmetric(horizontal: 4.0), child: IconButton( //发送按钮 icon: new Icon(Icons.send), //发送按钮图标 onPressed: () => _handleSubmitted( controller.text)), //触发发送消息事件执行的函数_handleSubmitted ), ), ], ), ), Offstage( offstage: hideAdd, child: ChatAddView( viewType: CommonUtils.VIEW_TYPE_SINGLE_CHAT, toChatId: widget.toChatId, refreshMediaCallback: (type, mediaURL, thumbnailFileName, second, messageId){ _refreshMedia(type, mediaURL, thumbnailFileName, mediaSecond: second , messageId: messageId); }, sendMedialCallback: (type, mediaURL, second, messageId){ _sendMedia(type, mediaURL, mediaSecond: second, messageId: messageId); }, refreshRedpacketAndTransfer: (type, text){ _refreshRedpacketAndTransfer(type, text); }, ), ), Offstage( offstage: hideEmoji, child: getEmojiWidget(), ), ], ), ), ), ); } //进入聊天页面,把聊天状态更新为已读 void _updateChatStatus() async{ int newMessageCount = await ChatRepository.getInstance().getAllChatUnReadByAccount(_toChatId)??0; if(newMessageCount >= 0){ await ChatRepository.getInstance().updateChatReadByAccount(_toChatId); Map<String, Object> result = HashMap<String, Object>(); result["from_account"] = _toChatId; eventBus.emit(BaseEvent(BaseEvent.TYPE_UPDATE_CHAT_STATUS, result: result)); } } // 下拉刷新 Future<void> _onRefresh() async{ //延迟0.02秒 await Future.delayed(Duration(milliseconds:20),(){ if(startNum >= PAGE_SIZE){ startNum = CHAT_TOTAL - PAGE_SIZE * PAGE_NUM; _loadMoreData(widget.account, widget.toChatId, startNum, PAGE_SIZE); } else if(startNum > 0 && startNum < PAGE_SIZE){ //不够1页数据,查询全部,然后就不能下一页 _loadMoreData(widget.account, widget.toChatId, 0, startNum); startNum = 0; } }); } bool isLoadMore = false; //上拉加载更多数据 void _loadMoreData(String fromAccount, String toAccount, int sNum , int pageSize){ isLoadMore = true; ChatRepository.getInstance().findAllChatByAccountPage(fromAccount, toAccount, sNum, pageSize).then((chatList) { if(startNum > 0){ PAGE_NUM++; } Timer(Duration(milliseconds: 100),() => _controller.jumpTo(AppManager.getInstance().getHeight(context)/3)); setState(() { items.insertAll(0, chatList??[]); }); Timer(Duration(milliseconds: 100),() => isLoadMore = false); }); } //检查状态, 如果不可以,先登录 void _checkAvailable() async{ var isAvailable = await XmppManager.getInstance().isAvailable(); if(!isAvailable){ String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT); String password = SpUtils.getString(CommonUtils.LOGIN_PASSWORD); XmppManager.getInstance().connect(account, password); } } //加载聊天信息 void loadAllChat() async { CHAT_TOTAL = await ChatRepository.getInstance().getChatCountByAccount(widget.account, widget.toChatId)??0; startNum = CHAT_TOTAL - PAGE_SIZE * PAGE_NUM; ChatRepository.getInstance().findAllChatByAccountPage(widget.account, widget.toChatId, startNum, CHAT_TOTAL).then((chatList) { if(startNum > 0){ PAGE_NUM++; } setState(() { items = chatList??[]; }); }); } //加载我的、好友信息 void loadUserBean() async { _otherUserBean = await UserRepository.getInstance().findUserByAccount(_toChatId); _meUserBean = await UserRepository.getInstance().findUserByAccount(widget.account); } //发送消息 _sendMessage(var message){ int id = DateTime.now().millisecondsSinceEpoch; String account = SpUtils.getString(CommonUtils.LOGIN_ACCOUNT); String toJid = "${widget.toChatId}@wangning"; XmppManager.getInstance().sendMessageWithType(toJid, message, "$account", id); Map<String, Object> result = HashMap<String, Object>(); eventBus.emit(BaseEvent(BaseEvent.TYPE_NEW_MESSAGE, result: result)); } //默认滚动到底部 void jumpToBottom(int milliseconds){ if (items.length > 0) { Timer(Duration(milliseconds: milliseconds), () => _controller.jumpTo(_controller.position.maxScrollExtent)); } } //隐藏播放列表,停止播放录音 hidePlayVoiceList(){ for(int i = 0; i < items.length;i++){ items[i].isPlayVoice = false; } AudioPlayer.getInstance().stop(); setState(() { }); } //刷新多媒体(图片、语音、小视频) (先刷新本地,然后小视频压缩完成再慢慢发送) void _refreshMedia(int type, String mediaURL, String thumbnailFileName, {int mediaSecond=0, String messageId = "" }) async { bool isNetwork = await CommonNetwork.isNetwork(); if(!isNetwork) { CommonUtils.showNetworkError(context); return; } bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId); if(deleteContacts){ WnBaseDialog.showAddFriendsDialog(context, widget.toChatId); return; } String addTime = WnDateUtils.getCurrentTime(); //先刷新本地聊天 ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId,addTime:addTime,messageId: messageId,isRead: 1); chatBean.contentType = type; if(type == CommonUtils.CHAT_CONTENT_TYPE_VOICE){ chatBean.voiceLocal = mediaURL; chatBean.second = mediaSecond; //状态变更,向聊天记录中插入新记录 setState(() { items.add(chatBean); }); ChatRepository.getInstance().insertChat(chatBean); } else if(type == CommonUtils.CHAT_CONTENT_TYPE_IMG){ chatBean.imgPathLocal = mediaURL; //状态变更,向聊天记录中插入新记录 setState(() { items.add(chatBean); }); ChatRepository.getInstance().insertChat(chatBean); } else if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){ //小视频会刷新本地2次。但messageId都是一样的 chatBean.videoLocal = mediaURL; chatBean.imgPathLocal = thumbnailFileName; chatBean.second = mediaSecond; ChatBean? localChatBean = await ChatRepository.getInstance().findChatByMessageId(messageId); //状态变更,向聊天记录中插入新记录 if(localChatBean == null){ items.add(chatBean); ChatRepository.getInstance().insertChat(chatBean); } else { chatBean.id = localChatBean.id; ChatRepository.getInstance().updateChat(chatBean); //如果已经存在,先删除在添加 for(int i = 0; i < items.length; i++){ ChatBean item = items[i]; if(item.messageId == messageId){ items.remove(item); break; } } items.add(chatBean); } setState(() { }); } //LogUtils.d("滚动到底部3"); jumpToBottom(100); } //发送多媒体(图片、语音、小视频) void _sendMedia(int type, String mediaURL, {int mediaSecond = 0, String messageId = ""}) async { bool isNetwork = await CommonNetwork.isNetwork(); if(!isNetwork) { return; } bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId); if(deleteContacts){ return; } //上传文件 ChatBean serverChatBean; String message = ""; ChatSendBean chatSendBean = ChatSendBean(); chatSendBean.contentType = type; chatSendBean.messageId = messageId; chatSendBean.addTime = WnDateUtils.getCurrentTime(); if(type == CommonUtils.CHAT_CONTENT_TYPE_IMG){ //图片 serverChatBean = await UploadUtils.getInstance().uploadChatImage(widget.account, widget.toChatId, mediaURL); chatSendBean.content = serverChatBean.imgPath??""; } else if(type == CommonUtils.CHAT_CONTENT_TYPE_VOICE){ //语音 serverChatBean = await UploadUtils.getInstance().uploadChatVoice(widget.account, widget.toChatId, mediaURL); chatSendBean.content = serverChatBean.voice??""; chatSendBean.second = mediaSecond; } else if(type == CommonUtils.CHAT_CONTENT_TYPE_VIDEO){ //小视频 serverChatBean = await UploadUtils.getInstance().uploadChatVideo(widget.account, widget.toChatId, mediaURL); message = "${type}${CommonUtils.CHAT_MESSAGE_SPILE}${serverChatBean.video}"; chatSendBean.content = serverChatBean.video??""; chatSendBean.second = mediaSecond; } else { return ; } message = jsonEncode(chatSendBean); _sendMessage(message); } //是否隐藏文件 bool hideAdd = true; //是否隐藏emoji表情 bool hideEmoji = true; //是否隐藏发送按钮 bool hideSend = true; //是否隐藏添加按钮 bool hideAddIcon = false; //是否按下语音说话 bool isPressVoice = false; //点击空白地方,隐藏文件、emoji void _processClickBlank(){ setState(() { hideAdd = true; hideEmoji = true; _chatContentFocus.unfocus(); // 失去焦点 }); } //按下录音 void _processPressVoice(){ setState(() { isPressVoice = !isPressVoice; hideEmoji = true; hideAdd = true; _processFocus(); }); } //点击emoji表情 void _processEmoji(){ setState(() { hideEmoji = !hideEmoji; isPressVoice = false; hideAdd = true; _processFocus(); }); } //点击+按钮 void _processAdd(){ setState(() { hideAdd = !hideAdd; isPressVoice = false; hideEmoji = true; _processFocus(); }); } //处理焦点 void _processFocus(){ if(!hideAdd || !hideEmoji || isPressVoice){ _chatContentFocus.unfocus(); // 失去焦点 } else { FocusScope.of(context).requestFocus(_chatContentFocus); // 获取焦点 } } emoticonClick(String name){ controller.text = name; } ///选中表情 _onEmojiSelected(Emoji emoji) { controller ..text += emoji.emoji ..selection = TextSelection.fromPosition(TextPosition(offset: controller.text.length)); hideAddIcon = true; hideSend = false; setState(() { }); } ///表情删除按钮 _onBackspacePressed() { controller ..text = controller.text.characters.skipLast(1).toString() ..selection = TextSelection.fromPosition( TextPosition(offset: controller.text.length)); if (controller.text.isNotEmpty) { setState(() { }); } } //是否已经删除联系人 Future<bool> isDeleteContacts(String fromAccount, String toAccount) async { bool delete = false; ContactsBean? contactsBean = await ContactsRepository.getInstance().findContactByFromOrToAccount(fromAccount, toAccount); if(contactsBean != null){ delete = (contactsBean.type == ContactsBean.typeDelete); } return Future.value(delete); } //定义发送文本事件的处理函数 void _handleSubmitted(String text) async { if (text.length > 0) { bool isNetwork = await CommonNetwork.isNetwork(); if(!isNetwork) { CommonUtils.showNetworkError(context); return; } bool deleteContacts = await isDeleteContacts(widget.account, widget.toChatId); if(deleteContacts){ WnBaseDialog.showAddFriendsDialog(context, widget.toChatId); return; } int contentType = CommonUtils.CHAT_CONTENT_TYPE_TEXT; String addTime = WnDateUtils.getCurrentTime(); String messageId = UUID.getUUID(); ChatSendBean chatSendBean = ChatSendBean(); chatSendBean.contentType = contentType; chatSendBean.content = text; chatSendBean.addTime = addTime; chatSendBean.second = 0; chatSendBean.messageId = messageId; String message = jsonEncode(chatSendBean); _sendMessage(message); controller.clear(); //清空输入框 ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId, content: text,contentType: contentType, addTime: addTime, isRead: 1, messageId: messageId); LogUtils.d("插入数据:${chatBean.toJson()}"); //状态变更,向聊天记录中插入新记录 setState(() { hideAddIcon = false; hideSend = true; items.add(chatBean); }); await ChatRepository.getInstance().insertChat(chatBean); jumpToBottom(100); } } //Emoji表情控件 Widget getEmojiWidget(){ return SizedBox( height: 200.0, width: 1000.0, child: EmojiPicker( onEmojiSelected: (Category category, Emoji emoji) { _onEmojiSelected(emoji); }, onBackspacePressed: _onBackspacePressed, config: const Config( columns: 7, emojiSizeMax: 25.0, verticalSpacing: 0, horizontalSpacing: 0, initCategory: Category.RECENT, bgColor: Color(0xFFF2F2F2), indicatorColor: Color(0xff65DAC5), iconColor: Colors.orange, iconColorSelected: Color(0xff65DAC5), progressIndicatorColor: Color(0xff65DAC5), backspaceColor: Color(0xff65DAC5), showRecentsTab: true, recentsLimit: 28, categoryIcons: CategoryIcons(), buttonMode: ButtonMode.MATERIAL)), ); } /**刷新红包、转账 *@contentType 类型 *@text 内容 */ void _refreshRedpacketAndTransfer(int contentType, String text) async { if (text.length > 0) { bool isNetwork = await CommonNetwork.isNetwork(); if(!isNetwork) { CommonUtils.showNetworkError(context); return; } String messageId = UUID.getUUID(); String addTime = WnDateUtils.getCurrentTime(); ChatSendBean chatSendBean = ChatSendBean(); chatSendBean.contentType = contentType; chatSendBean.content = text; chatSendBean.addTime = addTime; chatSendBean.second = 0; chatSendBean.messageId = messageId; String message = jsonEncode(chatSendBean); _sendMessage(message); controller.clear(); //清空输入框 ChatBean chatBean = ChatBean(fromAccount: widget.account, toAccount: widget.toChatId, content: text,contentType: contentType, addTime: addTime, isRead: 1, messageId: messageId); await ChatRepository.getInstance().insertChat(chatBean); //状态变更,向聊天记录中插入新记录 setState(() { items.add(chatBean); }); jumpToBottom(100); } } //刷新转账 void _refreshTransfer(int position) async { ChatBean chatBean = items[position]; chatBean.isClick = 1; setState(() { }); } void _initScreenUtil(BuildContext context) { ScreenUtil.init( BoxConstraints( maxWidth: MediaQuery.of(context).size.width, maxHeight: MediaQuery.of(context).size.height), designSize: const Size(375, 812), context: context); } }
chat_add_view.dart实现:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/9/24 14:46 * Description : 聊天点击+按钮 * 1单聊支持:相册、拍照、视频通话、语音通话、红包、转账 * 2群聊支持:相册、拍照 */ class ChatAddView extends StatefulWidget{ //刷新列表 final refreshMediaCallback; //发送信息 final sendMedialCallback; //聊天id final String toChatId; //刷新红包、转账 final refreshRedpacketAndTransfer; //1单聊, 2群聊 final int viewType; ChatAddView({required this.viewType, required this.toChatId, required this.refreshMediaCallback, required this.sendMedialCallback, required this.refreshRedpacketAndTransfer}); @override State<StatefulWidget> createState() => _ChatAddState(); } class _ChatAddState extends State<ChatAddView>{ @override Widget build(BuildContext context) { return getAddWidget(); } //相册 List ablums = [CommonUtils.getBaseIconUrlPng("wc_chat_album_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_album_selected")]; int ablumsPosition = 0; //拍照 List takePhotos = [CommonUtils.getBaseIconUrlPng("wc_chat_video_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_video_selected")]; int takePhotosPosition = 0; //视频通话 List videoCalls = [CommonUtils.getBaseIconUrlPng("wc_chat_video_call_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_video_call_selected")]; int videoCallsPosition = 0; //语音通话 List voiceCalls = [CommonUtils.getBaseIconUrlPng("wc_chat_voice_call_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_voice_call_selected")]; int voiceCallsPosition = 0; //红包 List redPackets = [CommonUtils.getBaseIconUrlPng("wc_chat_redpacket_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_redpacket_selected")]; int redPacketsPosition = 0; //转账 List transfers = [CommonUtils.getBaseIconUrlPng("wc_chat_transfer_normal"), CommonUtils.getBaseIconUrlPng("wc_chat_transfer_selected")]; int transfersPosition = 0; //相册 final TYPE_ABLUM = 1; //拍照 final TYPE_TAKE_PHOTO = 2; //视频通话 final TYPE_VIDEO_CALL = 3; //语音通话 final TYPE_VOICE_CALL = 4; //红包 final TYPE_RED_PACKET = 5; //转账 final TYPE_TRANSFER = 6; //改变状态 void _changeStatus(int type, int position){ if(type == TYPE_ABLUM){ ablumsPosition = position; ablums[ablumsPosition]; } else if(type == TYPE_TAKE_PHOTO){ takePhotosPosition = position; takePhotos[takePhotosPosition]; } else if(type == TYPE_VIDEO_CALL){ videoCallsPosition = position; videoCalls[videoCallsPosition]; } else if(type == TYPE_VOICE_CALL){ voiceCallsPosition = position; voiceCalls[voiceCallsPosition]; } else if(type == TYPE_RED_PACKET){ redPacketsPosition = position; redPackets[redPacketsPosition]; } else if(type == TYPE_TRANSFER){ transfersPosition = position; transfers[transfersPosition]; } setState(() { }); } Widget getAddWidget(){ return Container( //margin: EdgeInsets.only(top: 40, bottom: AppManager.getInstance().getBottom(context) + 20), alignment: Alignment.center, child: Center( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceAround, //交叉轴的布局方式,对于column来说就是水平方向的布局方式 crossAxisAlignment: CrossAxisAlignment.center, //就是字child的垂直布局方向,向上还是向下 verticalDirection: VerticalDirection.down, children: [ _buildBottomItem(TYPE_ABLUM), _buildBottomItem(TYPE_TAKE_PHOTO), Offstage( offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT, child: _buildBottomItem(TYPE_VIDEO_CALL), ), Offstage( offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT, child: _buildBottomItem(TYPE_VOICE_CALL), ), ], ), Offstage( offstage: widget.viewType == CommonUtils.VIEW_TYPE_GROUP_CHAT, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, //交叉轴的布局方式,对于column来说就是水平方向的布局方式 crossAxisAlignment: CrossAxisAlignment.center, //就是字child的垂直布局方向,向上还是向下 verticalDirection: VerticalDirection.down, children: [ _buildBottomItem(TYPE_RED_PACKET), _buildBottomItem(TYPE_TRANSFER), _buildBottomItem(-1), _buildBottomItem(-1), ], ), ), ], ), ), ); } Widget _buildBottomItem(int type){ return Container( alignment: Alignment.center, margin: EdgeInsets.only(top: 10, bottom: 10), child: GestureDetector( child: _getBottomWidget(type), onTap: (){ _changeStatus(type,0); if(type == TYPE_ABLUM){ _openAblumPermission(); } else if(type == TYPE_TAKE_PHOTO){ _takePhotoPermission(); } else if (type == TYPE_VIDEO_CALL){ _openVideoCall(); } else if (type == TYPE_VOICE_CALL){ _openVoiceCall(); } else if (type == TYPE_RED_PACKET){ _openRedPacket(); } else if (type == TYPE_TRANSFER){ _openTransfer(); } }, onTapCancel: (){ _changeStatus(type,0); }, onTapDown: (data){ _changeStatus(type,1); }, ), ); } Widget _getBottomWidget(int type){ if(type == TYPE_ABLUM){ return Column( children: [ Image.asset(ablums[ablumsPosition], width: 50, height: 50,), const Text("相册"), ], ); } else if(type == TYPE_TAKE_PHOTO){ return Column( children: [ Image.asset(takePhotos[takePhotosPosition], width: 50, height: 50,), const Text("拍照"), ], ); } else if(type == TYPE_VIDEO_CALL){ return Column( children: [ Image.asset(videoCalls[videoCallsPosition], width: 50, height: 50,), const Text("视频通话"), ], ); } else if(type == TYPE_VOICE_CALL){ return Column( children: [ Image.asset(voiceCalls[voiceCallsPosition], width: 50, height: 50,), const Text("语音通话"), ], ); } else if(type == TYPE_RED_PACKET){ return Column( children: [ Image.asset(redPackets[redPacketsPosition], width: 50, height: 50,), const Text("红包"), ], ); } else if(type == TYPE_TRANSFER){ return Column( children: [ Image.asset(transfers[transfersPosition], width: 50, height: 50,), const Text("转账"), ], ); } else { //空白占位符 return Column( children: [ Container( width: 50, height: 50, ), Text(""), ], ); } } //打开相册权限 void _openAblumPermission() async { bool isPhotosGranted = await Permission.photos.isGranted; bool isPhotosDenied = await Permission.photos.isDenied; if(isPhotosGranted){ _openAblum(); } else { if(isPhotosDenied){ _openAblum(); } else { //跳转到设置页面提示 _showPhotosConfirmationAlert(context); } } } // 为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许无他相机使用 _showPhotosConfirmationAlert(BuildContext context) { showPlatformDialog( context: context, builder: (_) => BasicDialogAlert( title: Text("无法使用相册"), content: Text("为编辑照片,请前往设备中的【设置】> 【隐私】> 【照片】中允许${AppManager.getInstance().appName}使用"), actions: <Widget>[ BasicDialogAction( title: Text("知道了"), onPressed: () { Navigator.pop(context); }, ), BasicDialogAction( title: Text("去设置"), onPressed: () { // 跳转到系统设置页 AppSettings.openAppSettings(); }, ), ], ), ); } //打开相册 void _openAblum() { LogUtils.d("打开相册"); List<AssetEntity> selectedAssets = []; AssetPicker.pickAssets( context, pickerConfig: AssetPickerConfig( maxAssets: 1, selectedAssets: selectedAssets, ), ).then((imageList) { if(imageList == null){ return; } imageList as List<AssetEntity>; for(int i = 0; i < imageList.length; i++){ AssetEntity ae = imageList[i]; ae.file.then((file) async { String resultFilePath = file?.path??""; _processVideoAndPicture(resultFilePath); }); } }); } //拍照权限 _takePhotoPermission() async{ bool isCameraGranted = await Permission.camera.isGranted; bool isCameraDenied = await Permission.camera.isDenied; bool isMicrophoneGranted = await Permission.microphone.isGranted; bool isMicrophoneDenied = await Permission.microphone.isDenied; LogUtils.d("拍照:${isCameraGranted}, ${isCameraDenied} , ${isMicrophoneGranted} , ${isMicrophoneDenied}"); //如果2个权限都同意,直接打开 if(isCameraGranted && isMicrophoneGranted){ _takePhoto(); } else if(isCameraDenied && isMicrophoneDenied){ //如果2个权限都拒绝,直接打开 _takePhoto(); } else if(!isCameraGranted && isMicrophoneGranted){ _takePhoto(); } else if(isCameraGranted && !isCameraDenied){ //提示设置麦克风权限 String title = "无法使用麦克风"; String content = "为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用"; WnBaseDialog.showPermissionDialog(context, title: title, content: content); } else if(!isCameraDenied){ String title = "无法使用相机"; String content = "为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许${AppManager.getInstance().appName}使用"; WnBaseDialog.showPermissionDialog(context, title: title, content: content); } else if(!isMicrophoneDenied){ //提示设置麦克风权限 LogUtils.d("拍照7"); String title = "无法使用麦克风"; String content = "为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用"; WnBaseDialog.showPermissionDialog(context, title: title, content: content); } } //拍照 void _takePhoto(){ LogUtils.d("拍照"); Feedback.forTap(context); CameraPicker.pickFromCamera( context, pickerConfig: const CameraPickerConfig(enableRecording: true, textDelegate: CameraPickerTextDelegate()), useRootNavigator: false ).then((resultAssetEntity) { resultAssetEntity?.file.then((resultFile) { LogUtils.d("2拍照返回:${resultFile?.path}"); _processVideoAndPicture(resultFile?.path??""); }); }); } //打开红包 void _openRedPacket() async { var balanceStr = await Navigator.push(context, MaterialPageRoute(builder: (context) => RedPacketWidget())); if(balanceStr == null){ return; } widget.refreshRedpacketAndTransfer(CommonUtils.CHAT_CONTENT_TYPE_REDPACKET, balanceStr); } //打开转账 void _openTransfer() async { var balanceStr = await Navigator.push(context, MaterialPageRoute(builder: (context) => PaymentTransfer(toUser: widget.toChatId,))); if(balanceStr == null){ return; } widget.refreshRedpacketAndTransfer(CommonUtils.CHAT_CONTENT_TYPE_TRANSFER, balanceStr); } //打开语音通话 void _openVoiceCall() async{ Navigator.push(context, MaterialPageRoute(builder: (context) => VideoCallWidget(videoPeerId: widget.toChatId, mediaFlag: CommonUtils.MEDIA_FLAG_VOICE,))); } //打开视频通话 void _openVideoCall(){ Navigator.push(context, MaterialPageRoute(builder: (context) => VideoCallWidget(videoPeerId: widget.toChatId, mediaFlag: CommonUtils.MEDIA_FLAG_VIDEO,))); } //处理图片和小视频(相册、拍照) void _processVideoAndPicture(String resultFilePath) async { if(resultFilePath == null || "" == resultFilePath){ return; } String messageId = UUID.getUUID(); if(CommonUtils.isImage(resultFilePath)){ //压缩图片完成再发送 String compressImagePath = await CompressImageUtils.compressFile(fileName: resultFilePath); widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_IMG, compressImagePath,0 ,messageId); widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_IMG, compressImagePath, "",0, messageId); } else if(CommonUtils.isVideo(resultFilePath)){ /** * 小视频发送流程,因为小视频比较大,压缩时间比较长。发送的视频,先本地优先显示,查看播放小视频 * 1、先复制一份小视频 * 2、生成缩略图显示 * 3、压缩小视频 * 4、删除复制的小视频 */ //_testmp4(resultFilePath); //最大100M的视频, 不是1024*1024*500 , 使用1000*100*500 int maxSize = 100000000; int fileSize = File(resultFilePath).lengthSync(); if(fileSize > maxSize){ CommonToast.show(context, "上传视频大小不能超过100M", duration: 3); return ; } //小视频生成缩略图大概5秒左右,先刷新站位图 widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, "", 0, messageId); String videoFormat = await getVideoFormat(resultFilePath); File srcFile = File(resultFilePath); String newVideoFileName = await FileUtils.getBaseFile("new_${DateUtil.getNowDateMs()}.mp4"); srcFile.copySync(newVideoFileName); String thumbnailFileName = await FileUtils.getBaseFile("thum_${DateUtil.getNowDateMs()}.png"); //生成缩略图 await VideoThumbnail.thumbnailFile(video: resultFilePath, thumbnailPath: thumbnailFileName); //获取视频时间 int second = await getVideoTime(resultFilePath); //int size = File(resultFilePath).lengthSync(); //先刷新 widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, resultFilePath, thumbnailFileName, second, messageId); //压缩完成再发送 MediaInfo? mediaInfo = await CompressVideoUtils.compressVideo(newVideoFileName); String compressVideoPath = mediaInfo?.path??""; //int csecond = await getVideoTime(resultFilePath); //int csize = File(compressVideoPath).lengthSync(); widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_VIDEO, compressVideoPath, second, messageId); } } //获取视频格式 Future<String> getVideoFormat(String resultFilePath) async { String videoFormat = ""; final FlutterFFprobe _flutterFFprobe = FlutterFFprobe(); MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath); if (info.getStreams() != null) { List<StreamInformation>? streams = info.getStreams(); if (streams != null && streams.length > 0) { for (var stream in streams) { videoFormat = stream.getAllProperties()['codec_tag_string']; } } } return videoFormat; } //获取视频时间 Future<int> getVideoTime(String resultFilePath) async { int time = 0; final FlutterFFprobe _flutterFFprobe = FlutterFFprobe(); MediaInformation info = await _flutterFFprobe.getMediaInformation(resultFilePath); if (info.getStreams() != null) { String duration = info.getMediaProperties()?['duration']; String size = info.getMediaProperties()?['size']; double durationDouble = double.parse(duration); time = durationDouble.toInt(); LogUtils.d("多媒体文件大小:${size}"); } return time; } void _testmp4(String resultFilePath){ final FlutterFFprobe _flutterFFprobe = new FlutterFFprobe(); _flutterFFprobe.getMediaInformation(resultFilePath).then((info) { LogUtils.d("测试视频信息:Media Information"); LogUtils.d("测试视频信息:Path: ${info.getMediaProperties()?['filename']}"); LogUtils.d("测试视频信息:Format: ${info.getMediaProperties()?['format_name']}"); LogUtils.d("测试视频信息:Duration: ${info.getMediaProperties()?['duration']}"); LogUtils.d("测试视频信息:Start time: ${info.getMediaProperties()?['start_time']}"); LogUtils.d("测试视频信息:Bitrate: ${info.getMediaProperties()?['bit_rate']}"); //LogUtils.d("测试视频信息:CodecTagString: ${info.getMediaProperties()?['codec_tag_string']}"); Map<dynamic, dynamic> tags = info.getMediaProperties()?['tags']; /*if (tags != null) { tags.forEach((key, value) { LogUtils.d("Tag: " + key + ":" + value + "\n"); }); }*/ if (info.getStreams() != null) { List<StreamInformation>? streams = info.getStreams(); if (streams != null && streams.length > 0) { for (var stream in streams) { LogUtils.d("测试视频信息:Stream id: ${stream.getAllProperties()['index']}"); LogUtils.d("Stream type: ${stream.getAllProperties()['codec_type']}"); LogUtils.d("Stream codec: ${stream.getAllProperties()['codec_name']}"); LogUtils.d("Stream full codec: ${stream.getAllProperties()['codec_long_name']}"); LogUtils.d("Stream format: ${stream.getAllProperties()['pix_fmt']}"); LogUtils.d("Stream width: ${stream.getAllProperties()['width']}"); LogUtils.d("Stream height: ${stream.getAllProperties()['height']}"); LogUtils.d("Stream bitrate: ${stream.getAllProperties()['bit_rate']}"); LogUtils.d("Stream sample rate: ${stream.getAllProperties()['sample_rate']}"); LogUtils.d("Stream sample format: ${stream.getAllProperties()['sample_fmt']}"); LogUtils.d("Stream channel layout: ${stream.getAllProperties()['channel_layout']}"); LogUtils.d("Stream sar: ${stream.getAllProperties()['sample_aspect_ratio']}"); LogUtils.d("Stream dar: ${stream.getAllProperties()['display_aspect_ratio']}"); LogUtils.d("Stream average frame rate: ${stream.getAllProperties()['avg_frame_rate']}"); LogUtils.d("Stream real frame rate: ${stream.getAllProperties()['r_frame_rate']}"); LogUtils.d("Stream time base: ${stream.getAllProperties()['time_base']}"); LogUtils.d("测试视频信息:Stream codec time base: ${stream.getAllProperties()['codec_time_base']}"); LogUtils.d("A测试视频信息:Stream codec_tag_string: ${stream.getAllProperties()['codec_tag_string']}"); /*Map<dynamic, dynamic> tags = stream.getAllProperties()['tags']; if (tags != null) { tags.forEach((key, value) { LogUtils.d("Stream tag: " + key + ":" + value + "\n"); }); }*/ } } } }); } }
chat_content_view.dart实现:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/9/24 12:09 * Description : 单聊内容控件 */ class ChatContentView extends StatefulWidget { final List<ChatBean> items; final ChatBean chatBean; final int index; final UserBean? otherUserBean; final UserBean? meUserBean; final String account; final deleteCallback; final List<String>? addTimeList; //点击语音播放回调 final clickVoiceCallback; //点击领取转账,刷新页面 final refreshTransfer; ChatContentView({required this.items, required this.account, required this.chatBean, required this.index, required this.meUserBean, required this.otherUserBean, this.addTimeList, required this.deleteCallback, required this.clickVoiceCallback, required this.refreshTransfer}); @override State<ChatContentView> createState() => _ChatContentViewState(); } class _ChatContentViewState extends State<ChatContentView> { //判断是否已经存在转换好的时间 @override void initState() { super.initState(); } void goNewFriends(String account) async{ UserBean? userBean = await UserRepository.getInstance().findUserByAccount(account); if(userBean != null){ Navigator.push(context,MaterialPageRoute(builder: (context)=>AddFriends(userBean: userBean!,))); } else { userBean = await UserRepository.getInstance().getUserServer(account); Navigator.push(context,MaterialPageRoute(builder: (context)=>AddFriends(userBean: userBean!,))); } } @override Widget build(BuildContext context) { String addTimeResult = _getAddTime("${widget.chatBean.addTime}"); bool isExistTime = isExistAddTime(addTimeResult); if(!isExistTime){ widget.addTimeList?.add(addTimeResult); } //如果是最后一个,清除标志 if(widget.index == widget.items.length -1){ widget.addTimeList?.clear(); } return Column( children: [ Offstage( offstage: isExistTime, child: Container( margin: EdgeInsets.only(top: 12), child: Text("${addTimeResult}"), ), ), Container( child: widget.account == widget.chatBean.fromAccount ? fromAccountWidget() : toAccountWidget(), ) ], ); } //小视频缩略图 Widget getCommonThumbnail(int second){ return CommonThumbnailWidget( padding: EdgeInsets.only( top: 0.0, right: (widget.account == widget.chatBean.fromAccount ? 0.0 : 5.0), left: (widget.account == widget.chatBean.toAccount ? 2.0 : 0.0)), image: widget.chatBean.imgPathLocal??"", second: second, onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) => VideoPlayLocalPreview(widget.chatBean.videoLocal!))); }); } //显示我的 Widget fromAccountWidget(){ return Container( margin: EdgeInsets.only(top: 8.0, left: 68.0, right: 8), padding: EdgeInsets.all(2.0), child: Row( children: <Widget>[ Expanded( child: GestureDetector( onLongPress: (){ _showDeleteDialog(widget.chatBean); }, onTap: () { }, child: Stack( alignment: AlignmentDirectional.bottomEnd, children: [ //文本 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TEXT?meTextWidget():Container(), //语言 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VOICE?meVoiceWidget():Container(), //图片 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_IMG?CommonUtils.showBaseImage(widget.chatBean.imgPathLocal??"", width:100, height:200, angle:1, onPressed: (data){ Navigator.push(context,MaterialPageRoute(builder: (context)=>CommonImagePreview(fileName: data))); }):Container(), //小视频 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VIDEO?getCommonThumbnail(widget.chatBean.second??0):Container(), //红包 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_REDPACKET?meRedpacketWidget():Container(), //转账 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TRANSFER?meTransferWidget():Container(), ], ), ), ), //userImage Container( padding: EdgeInsets.only(left: 6, right: 6), child: GestureDetector( onTap: (){ Navigator.push(context,MaterialPageRoute(builder: (context)=>ContactsDetails(toChatId: widget.chatBean.fromAccount??""))); }, child: CommonAvatarView.showBaseImage(widget.meUserBean?.avatar??"", 38, 38), ), ), ], ), ); } //显示好友 Widget toAccountWidget(){ return Container( margin: EdgeInsets.only(top: 8.0, right: 68.0), padding: EdgeInsets.all(2.0), child: Row( //crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ //userImage, Container( margin: EdgeInsets.only(left: 6, right: 6), child: GestureDetector( onTap : (){ Navigator.push(context,MaterialPageRoute(builder: (context)=>ContactsDetails(toChatId: widget.otherUserBean?.account??""))); }, child: CommonAvatarView.showBaseImage(widget.otherUserBean?.avatar??"", 38, 38), ), ), Expanded( child: GestureDetector( onLongPress: (){ _showDeleteDialog(widget.chatBean); }, onTap: () { }, child: Stack( alignment: AlignmentDirectional.centerStart, children: [ //文本 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TEXT?toTextWidget():Container(), //语音 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VOICE?toVoiceWidget():Container(), //图片 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_IMG?CommonUtils.showBaseImage(widget.chatBean.imgPathLocal??"", width:100, height:200, angle:1, onPressed: (data){ Navigator.push(context,MaterialPageRoute(builder: (context)=>CommonImagePreview(fileName: data))); }):Container(), //小视频 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_VIDEO?getCommonThumbnail(widget.chatBean.second??0):Container(), //红包 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_REDPACKET?toRedpacketWidget():Container(), //转账 widget.chatBean.contentType == CommonUtils.CHAT_CONTENT_TYPE_TRANSFER?toTransferWidget():Container(), ], ) ), ), /**/ ], ), ); } //打开红包对话框 void _onOpenRedpacket(){ } //朋友的文本 Widget toTextWidget(){ return Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFFEDEDED)), child: Text( widget.chatBean.content??"", textAlign: TextAlign.left, style: TextStyle(color: Colors.black, fontSize: 20.0), ), ) ], ); } //朋友的语音 Widget toVoiceWidget(){ return InkWell( onTap: () { setState(() { widget.chatBean.isPlayVoice = true; }); LogUtils.d("点击语音"); AudioPlayer.getInstance().playLocal(widget.chatBean.voiceLocal??"", callback: (data){ LogUtils.d("录音回调:${data}"); setState(() { widget.chatBean.isPlayVoice = false; }); }); }, child : Container( width: 120, height: 40, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 2.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFFEDEDED)), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ widget.chatBean.isPlayVoice?Image.asset("assets/chat/wn_chat_other_animator.gif", height: 34,):Image.asset("assets/chat/wn_chat_other_volume_3.png", height: 34,), SizedBox(width: 4,), Text("${widget.chatBean.second}''"), ], ), ) ); } //朋友的红包 Widget toRedpacketWidget(){ return GestureDetector( onTap: (){ if(widget.chatBean.isClick == 1){ Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.meUserBean?.account??"", toUser: widget.otherUserBean?.account??"", balance: widget.chatBean?.content??"", addTime: widget.chatBean.addTime??"",))); } else { showRedPacket(context, _onOpenRedpacket, widget.otherUserBean?.account, widget.chatBean?.content??"", widget.index); } }, child: Opacity( opacity: widget.chatBean.isClick == 1 ? 0.6 :1, child: Container( child: Stack( children: [ toRedpacketBackground(), Positioned( left: 38, top: 20, child:CommonUtils.getBaseIconPng("wc_redpacket_icon", width: 40, height: 40), ), Positioned( left: 88, top: 30, child: Text("恭喜发财,大吉大利", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 88, top: 50, child: Container( margin: EdgeInsets.only(top:10), width: 120, height: 1, color: Colors.white, ), ), Positioned( left: 38, bottom: 14, child:Text("私人红包", style: TextStyle(fontSize:12, color: Colors.white38),), ), ], ), ), ), ); } //处理转账 void _processTransferDetails() async{ var data = await Navigator.push(context, MaterialPageRoute(builder: (context) => TransferDetails(toUser: widget.chatBean?.toAccount??"", balance: double.parse(widget.chatBean?.content??""), messageId: widget.chatBean?.messageId??""))); if(data != null && data > 0){ widget.refreshTransfer(widget.index); } } //朋友的转账 Widget toTransferWidget(){ return GestureDetector( onTap: (){ _processTransferDetails(); }, child: Opacity( opacity: widget.chatBean.isClick == 1 ? 0.6 :1, child: Container( child: Stack( children: [ toRedpacketBackground(), Positioned( left: 42, top: 20, child:CommonUtils.getBaseIconPng("wc_chat_transfer_icon", width: 40, height: 40), ), Positioned( left: 98, top: 14, child: Text("¥${double.parse(widget.chatBean.content??'0').toStringAsFixed(2)}", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 98, top: 40, child: Text("请收款", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 98, top: 54, child: Container( margin: EdgeInsets.only(top:10), width: 120, height: 1, color: Colors.white, ), ), Positioned( left: 38, bottom: 14, child:Text("私人转账", style: TextStyle(fontSize: 12, color: Colors.white38),), ), ], ), ), ), ); } Widget toRedpacketBackground(){ return CustomPaint( painter: RedPacketOther( strokeColor: Color(0xFFf58220), paintingStyle: PaintingStyle.fill, ), child: Container( height: 100, width: 280, ), ); } //我的文本 Widget meTextWidget(){ return Column( // Column被Expanded包裹起来,使其内部文本可自动换行 crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[ Container( padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 10.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Color(0xFF9EEA6A),), child: Text( widget.chatBean.content??"", textAlign: TextAlign.left, style: TextStyle(color: Colors.black, fontSize: 20.0), ), ) ], ); } //我的语言 Widget meVoiceWidget(){ return InkWell( onTap: () { widget.clickVoiceCallback(true); setState(() { widget.chatBean.isPlayVoice = true; }); //点击语音 AudioPlayer.getInstance().playLocal(widget.chatBean.voiceLocal??"", callback: (data){ //录音回调 setState(() { widget.chatBean.isPlayVoice = false; }); }); }, child : Container( width: 120, height: 40, padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 2.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(1.0),),color: Color(0xFF9EEA6A),), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Text("${widget.chatBean.second}''"), SizedBox(width: 4,), widget.chatBean.isPlayVoice?Image.asset("assets/chat/wn_chat_me_animator.gif", height: 24,):Image.asset("assets/chat/wn_chat_me_volume_3.png", height: 24,), ], ), ) ); } //我的红包 Widget meRedpacketWidget(){ return GestureDetector( onTap: (){ //点击红包 Navigator.push(context, MaterialPageRoute(builder: (context) => ReceiveRedpacketSuccess(fromUser: widget.meUserBean?.account??"", toUser: widget.otherUserBean?.account??"", balance: widget.chatBean?.content??"", addTime: widget.chatBean.addTime??"",))); }, child: Container( child: Stack( children: [ meRedpacketBackground(), Positioned( left: 20, top: 20, child:CommonUtils.getBaseIconPng("wc_redpacket_icon", width: 40, height: 40), ), Positioned( left: 70, top: 30, child: Text("恭喜发财,大吉大利", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 70, top: 50, child: Container( margin: EdgeInsets.only(top:10), width: 120, height: 1, color: Colors.white, ), ), Positioned( left: 20, bottom: 14, child:Text("私人红包", style: TextStyle(fontSize: 12, color: Colors.white38),), ), ], ), ), ); } //我的转账 Widget meTransferWidget(){ return GestureDetector( onTap: (){ //点击转账 Navigator.push(context, MaterialPageRoute(builder: (context) => TransferDetails(toUser: widget.otherUserBean?.account??"", balance: double.parse(widget.chatBean?.content??""), messageId: widget.chatBean?.messageId??""))); }, child: Container( child: Stack( children: [ meRedpacketBackground(), Positioned( left: 20, top: 20, child:CommonUtils.getBaseIconPng("wc_chat_transfer_icon", width: 40, height: 40), ), Positioned( left: 70, top: 14, child: Text("¥${double.parse(widget.chatBean.content??'0').toStringAsFixed(2)}", style: TextStyle(fontSize: 18, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 70, top: 40, child: Text("你发起了一笔转账", style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.bold),), ), Positioned( left: 70, top: 54, child: Container( margin: EdgeInsets.only(top:10), width: 120, height: 1, color: Colors.white, ), ), Positioned( left: 20, bottom: 14, child:Text("私人转账", style: TextStyle(fontSize: 12, color: Colors.white38),), ), ], ), ), ); } Widget meRedpacketBackground(){ return CustomPaint( painter: RedPacketMe( strokeColor: Color(0xFFf58220), paintingStyle: PaintingStyle.fill, ), child: Container( height: 100, width: 280, ), ); } bool isExistAddTime(String addTimeResult){ return widget.addTimeList?.contains(addTimeResult)??false; } String _getAddTime(String addTime){ return WnTimeUtils.timeUtils(startTime: addTime); } //删除对话框 Future<void> _showDeleteDialog(ChatBean chatBean) async { return showDialog<Null>( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text('确定要删除该消息吗?', style: new TextStyle(fontSize: 17.0)), actions: <Widget>[ MaterialButton( child: Text('取消'), onPressed: (){ LogUtils.d("确定取消"); Navigator.of(context).pop(); }, ), MaterialButton( child: Text('确定'), onPressed: (){ LogUtils.d("确定删除"); Navigator.pop(context); //_deleteContacts(contactsBeanComb); _deleteChatBean(chatBean); }, ) ], ); } ); } //删除消息 _deleteChatBean(ChatBean chatBean) async{ int id = chatBean.id??0; await ChatRepository.getInstance().deleteChatById(id); widget.deleteCallback(true); } }
chat_voice_view.dart实现:
/** * Author : wangning * Email : maoning20080809@163.com * Date : 2022/9/24 12:21 * Description : 语音动画 */ class ChatVoiceView extends StatefulWidget{ //刷新列表 final refreshMediaCallback; //发送信息 final sendMedialCallback; //停止语音播放 final stopPlayVoiceCallback; ChatVoiceView({required this.refreshMediaCallback, required this.sendMedialCallback, required this.stopPlayVoiceCallback}); @override State<StatefulWidget> createState() => ChatVoiceState(); } class ChatVoiceState extends State<ChatVoiceView>{ // 倒计时总时长 int _countTotal = 60; double starty = 0.0; double offset = 0.0; bool isUp = false; String textShow = "按住 说话"; String toastShow = "手指上滑,取消发送"; String voiceIco = CommonUtils.getChatUrlPng("voice_volume_1"); ///默认隐藏状态 bool voiceState = true; Timer? _timer; int _count = 0; //录音总数 int _soundSecond = 0; OverlayEntry? overlayEntry; final _audioRecorder = Record(); @override void initState() { super.initState(); } @override void dispose() { super.dispose(); _audioRecorder.dispose(); _timer?.cancel(); } @override Widget build(BuildContext context) { return getPressVoiceWidget(); } //打开录音权限 void _openMicrophonePermission(details) async { bool isMicrophoneGranted = await Permission.microphone.isGranted; bool isMicrophoneDenied = await Permission.microphone.isDenied; if(isMicrophoneGranted){ _onLongPressStart(details); } else { if(isMicrophoneDenied){ PermissionUtils.requestMicrophonePermission(); } else { //跳转到设置页面提示 _showMicrophoneConfirmationAlert(context); } } } //无法使用相机 // 为正常拍摄,请前往设备中的【设置】> 【隐私】> 【相机】中允许无他相机使用 _showMicrophoneConfirmationAlert(BuildContext context) { showPlatformDialog( context: context, builder: (_) => BasicDialogAlert( title: Text("无法使用麦克风"), content: Text("为正常录制声音,请前往设备中的【设置】> 【隐私】> 【麦克风】中允许${AppManager.getInstance().appName}使用"), actions: <Widget>[ BasicDialogAction( title: Text("知道了"), onPressed: () { Navigator.pop(context); }, ), BasicDialogAction( title: Text("去设置"), onPressed: () { // 跳转到系统设置页 AppSettings.openAppSettings(); }, ), ], ), ); } void _onLongPressStart(details){ starty = details.globalPosition.dy; _timer = Timer.periodic(Duration(milliseconds: 1000), (t) { _count++; if (_count == _countTotal) { hideVoiceView(); } }); showVoiceView(); } void _onLongPressEnd() async { bool isMicrophoneGranted = await Permission.microphone.isGranted; if(isMicrophoneGranted){ hideVoiceView(); } } void _onLongPressMoveUpdate(details) async { bool isMicrophoneGranted = await Permission.microphone.isGranted; if(isMicrophoneGranted){ offset = details.globalPosition.dy; moveVoiceView(); } } Widget getPressVoiceWidget() { return GestureDetector( onLongPressStart: (details) { _openMicrophonePermission(details); }, onLongPressEnd: (details) { _onLongPressEnd(); }, onLongPressMoveUpdate: (details) { _onLongPressMoveUpdate(details); }, child: Container( alignment: Alignment.bottomCenter, padding: EdgeInsets.only(top: 10.0, bottom: 10.0), decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(5.0),),color: Colors.black12), width: double.infinity, child: Text("按住 说话"), ), ); } ///显示录音悬浮布局 buildOverLayView(BuildContext context) { if (overlayEntry == null) { overlayEntry = new OverlayEntry(builder: (content) { return CustomOverlay( icon: Column( children: <Widget>[ Container( margin: const EdgeInsets.only(top: 10), child: _countTotal - _count < 11 ? Center( child: Padding( padding: const EdgeInsets.only(bottom: 15.0), child: Text( (_countTotal - _count).toString(), style: TextStyle( fontSize: 70.0, color: Colors.white, ), ), ), ) : new Image.asset( voiceIco, width: 100, height: 100, //package: 'flutter_plugin_record', ), ), Container( // padding: const EdgeInsets.only(right: 20, left: 20, top: 0), child: Text( toastShow, style: TextStyle( fontStyle: FontStyle.normal, color: Colors.white, fontSize: 14, ), ), ) ], ), ); }); Overlay.of(context)!.insert(overlayEntry!); } } showVoiceView() { setState(() { textShow = "松开结束"; voiceState = false; }); ///显示录音悬浮布局 buildOverLayView(context); hidePlayVoiceList(); start(); playAnimation(); } //隐藏播放列表,停止播放录音 hidePlayVoiceList(){ widget.stopPlayVoiceCallback(); } hideVoiceView() async { if (_timer!.isActive) { if (_count < 1) { CommonToast.showView( context: context, msg: '说话时间太短', icon: Text( '!', style: TextStyle(fontSize: 80, color: Colors.white), )); isUp = true; } _timer?.cancel(); _soundSecond = _count; _count = 0; } setState(() { textShow = "按住 说话"; voiceState = true; }); stop(); if (overlayEntry != null) { overlayEntry?.remove(); overlayEntry = null; } if (isUp) { LogUtils.d("取消发送"); File file = File(fileName); if(fileName != null && file.existsSync()){ file.deleteSync(); } } else { LogUtils.d("进行发送 ${_soundSecond}"); await _audioRecorder.stop(); String messageId = UUID.getUUID(); widget.refreshMediaCallback(CommonUtils.CHAT_CONTENT_TYPE_VOICE, fileName, "", _soundSecond, messageId); widget.sendMedialCallback(CommonUtils.CHAT_CONTENT_TYPE_VOICE, fileName, _soundSecond, messageId); } } moveVoiceView() { setState(() { isUp = starty - offset > 100 ? true : false; if (isUp) { textShow = "松开手指,取消发送"; toastShow = textShow; } else { textShow = "松开结束"; toastShow = "手指上滑,取消发送"; } }); } String fileName = ""; ///开始语音录制的方法 void start() async { try { fileName = await FileUtils.getBaseFile("voice_${DateUtil.getNowDateMs()}.m4a"); File file = File(fileName); if(!file.existsSync()){ file.createSync(); } if (await _audioRecorder.hasPermission()) { final isSupported = await _audioRecorder.isEncoderSupported( AudioEncoder.aacLc, ); LogUtils.d("录音文件:${fileName}"); await _audioRecorder.start(path: fileName, encoder: AudioEncoder.aacLc, bitRate: 262144, samplingRate: 48000); } } catch (e) { LogUtils.d("${e}"); } } Timer? periodicTimer; //播放录音动画 void playAnimation(){ int i =0; periodicTimer = Timer.periodic( const Duration(milliseconds: 150),(timer) { i++; if (i == 1) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_2"); } else if (i == 2) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_3"); } else if (i == 3) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_4"); } else if (i == 4) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_5"); } else if (i == 5) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_6"); } else if (i == 6) { voiceIco = CommonUtils.getChatUrlPng("voice_volume_7"); } else { i = 0; } if (overlayEntry != null) { overlayEntry!.markNeedsBuild(); } }, ); } ///停止语音录制的方法 void stop() async{ await _audioRecorder.stop(); periodicTimer?.cancel(); } }